今天,我们要来讲讲排序问题,这次讲的排序算法主要是归并排序,堆排序和桶排序。

归并排序

归并一词在中文的含义就是“合并,并入”的意思,在数据结构里面就是将两个或者两个以上的有序数组列表合并成一个的意思。

归并排序就是利用归并思想实现排序的方法。它的原理就是假设有n个初始数据,即n个有序子序列,它们的长度都是1,将它们两两合并,得到了n/2个长度为2的有序子序 列。再两两合并,如此重复,知道得到一个长度为n的有序子序列为止,这种排序方法称之为2路归并排序。

老规矩,上伪代码吧:

void MergeSort (int SR[ ] , int TR[ ], int s , int t ){int m ;int TR2[MAXSIZE + 1]if (s == t):TR1[s] = SR[s]else:{m = (m + s) / 2MSort(SR , TR2 , s , m)MSort(SR , TR2 , s + 1 , m)Merge(TR2 , TR2 ,s ,m , t) }}

简单讲一下,这段函数利用到了递归,假设初始子序列有10个关键字,第一个MSort就是将初始子序列下标0-4的关键字归并到TR2,第二个MSort就是将初始子序列下标5-9的关键字归并到TR2,可以认为将子序列分割成了两块,那么Merge函数就负责把这两块分割的子序列归并成一个完整的子序列。由于是递归,所以我们会一直分割分割直到分割成大小为1的子序列为止,然后再递归回来归并成一个完整有序的子序列。

再讲下Merge函数是怎么归并的,因为在归并前,因为是递归,两个子函数就已经是有序的了,我们假设到最后一种情况,两个有序的,长度为5的有序子序列进行归并,设第一个子序列下标为i,第二个下标为j。

    if  (SR[i] < SR[j]):TR[k] = SR[i]i ++else:TR[k] = SR[j]j ++

差不多就是这样的。

接下来我们来谈谈对比排序的复杂度吧。

首先分析时间复杂度,一趟归并排序需要将SR[1]-SR[n]中长度为h的相邻元素两两归并,并将结果放在SR[1]-SR[n]中,所以需要将序列全部扫描一遍,时间复杂度为O(n),再根据完全二叉树的深度可知,整个归并排序需要进行O(lgn)次,所以总的时间复杂度为O(nlgn),这是归并排序最好最坏的平均性能。

至于空间复杂度,由于再归并过程中需要和原子序列相同大小的存储空间存放归并结果并且需要深度为lgn的栈空间,所以空间复杂度为O(n+lgn)。

那能不能优化呢?当然可以啊,试试能不能将递归换成迭代就行了,具体代码我就不打了(lan不解释!~!),因为非递归函数在空间上不需要(lgn)的栈空间,所以空间复杂度是O(n),时间复杂度上也有所提升,可以说在归并算法中,我们应该尽量避免使用递归方法。

下面做一道真题:

Leetcode : 88. Merge Sorted Array (Easy)

给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。

说明:

初始化 nums1 和 nums2 的元素数量分别为 m 和 n。

你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例:

输入:

nums1 = [1,2,3,0,0,0], m = 3

nums2 = [2,5,6], n = 3

输出: [1,2,2,3,5,6]

class Solution:def merge(self, nums1: 'List[int]', m: 'int', nums2: 'List[int]', n: 'int') -> 'None':"""Do not return anything, modify nums1 in-place instead."""for x in nums2:i = m-1while i>-1:if nums1[i]>x:nums1[i+1] = nums1[i]i-=1else:nums1[i+1] = xbreakif i == -1:nums1[0] = xm+=1

堆排序

说到堆排序,让我们先想一个问题,如果在待排序的n个子序列中寻找一个最小的子序列,需要n-1次比较没错吧,但是我们本没有将这次排序的结果过程保存下来,那么在下次排序中,比如说找到第二小的子序列,是不是又需要走一遍?这样很浪费资源和时间。

那么我们做到每次选择到最小记录的同时,比较结果并对其他记录做出相应的调整,这样我们的效率就大大提高了。

而我们的堆排序,就是对相对简单的堆排序的一种改进,而且效果非常明显。

首先我们要明白,堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右节点的值,称之为大顶堆;每个结点的值都小于或等于其左右节点的值,称之为小顶堆。

堆排序就是利用堆进行排序的方法(假设使用大顶堆):将待排序的序列组成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点了,我们将它移走(其实就是将他和数组的末尾元素交换),然后将剩余的n-1个元素继续构成一个大顶堆,这样就会得到n个元素中的第二大的值,如此反复执行,我们就能得到一个有序序列了。

相信大家对堆排序的基本思想有一定的理解了,不过实现它还需要解决两个问题:

1.如何由一个无序序列构成一个堆?

2.在输出顶堆元素后,剩下的元素如何形成一个新的堆?

解决这两个问题嘛,老规矩,上代码!!

void HeapSort(SqList *L){int i ;for(i = L->length/2;i>0;i--)HeapAdjust(L,i,L->length);for(i = L->length;i>0;i--)swap(L,l,i);HeapAdjust(L,i,i-1);}

这里可以看到有两个循环,第一个循环是将现有的序列构成一个大顶堆,第二个循环就是将每个最大值的根节点和末尾元素交换,并且将剩余元素构成大顶堆。

现在我们来看看关键元素HeapAdjust(堆调整)是怎样实现的吧!

void HeapAdjust(SqList *L ,int s , int m){int temp , j;temp = L->r[s];for( j = 2*s , j <= m ,j *= 2 ){if (j < m && L->r[j] < L->r[j+1])++j;if (temp >= L->r[j])break;L->r[s] = L->r[j];s = j;}L->r[s] = temp}

代码不长吧,相信大家都能看得懂,我这边就讲几个我认为的难点吧。

首先j变量为什么从2*s开始呢?因为这是二叉树的性质,因为我们的二叉树都是完全二叉树,所以它的节点为s,那么它的左孩子一定是2s,右孩子一定是2*s+1。

我用简洁的话讲一下基本思想:先将结点的左右孩子进行比较,如果右孩子大,j++。将比较完后较大的数和双亲进行比较,如L->r[j] > L->r[s],将他们exchange即可。

接下来我们讨论下堆排序的效率吧!~!

它的消耗主要是在初始建堆和重新建堆的反复筛选上面。在构建堆以后,我们从二叉树最下层最右边的非终端节点开始,将它对其孩子进行比较和有必要的互换。所以对每个非终端节点来说,最多进行两次比较和互换操作,所以整个构建堆的时间复杂度为O(n)。

在正式排序时,第 i 次取顶记录重建堆需要的时间是O(logi)(根据完全二叉树某个节点到根节点的距离为[logi]+1)。因此,重建堆的时间复杂度为O(nlogn)。

所以总体来说,堆排序的时间复杂度为O(nlogn)。

下面是真题时间:

Leetocde : 215. Kth Largest Element in an Array (Medium)

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2

输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4

输出: 4

class Solution(object):def findKthLargest(self, nums, k):""":type nums: List[int]:type k: int:rtype: int"""length = len(nums)# 构建大顶堆for i in reversed(range(0, length / 2)):self.adjustHeap(nums, i, length)count = 0for j in reversed(range(0,length)):count += 1if count == k:return nums[0]self.swap(nums,0,j)length -= 1self.adjustHeap(nums,0,length)def adjustHeap(self,nums,i,length):while True:k = i*2+1if k >= length:returnif k + 1 < length and nums[k+1] > nums[k]:k += 1if nums[k] > nums[i]:self.swap(nums,i,k)i = kelse:returndef swap(self,nums,i,j):temp = nums[i]nums[i] = nums[j]nums[j] = temp

桶排序

到目前为止,我们已经介绍了归并排序和堆排序这两种比较排序算法,我们知道,在最坏情况下任何比较排序算法都要做O(nlgn)次比较,而归并排序和堆排序都是渐进最优的比较排序算法。

接下来我要介绍是是线性时间排序,它是一种非比较排序,它最主要的优点就是稳定。下面我来讲几种典型的线性时间排序:

计数排序:

计数排序的基本思想是对于每个输入元素,确定小于x的元素的个数,利用这一信息,我们可以直接把x放到它输出数组的位置上了。

基数排序:

基数排序是一种很古老的排序了,我们举个栗子就很明白了,比如我们要排序10个三位数,那我们可以先按最低有效位进行排序来解决卡片的排序问题。即先比较个位数,再比较十位数,最后比较百位数就行了!~!

以上两种线性时间算法都比较简单,所以我就不展开讲了,接下来我要重点讲一下一种运用比较多是算法--桶排序算法

桶排序是假设数据服从均匀分布,在平均情况下,它们的时间代价为O(n)。

桶排序是将[0,1]区间划分为n个大小相同的子区间,或者称之为桶,然后将n个输入分别输入到对应的桶中,然后我们对每个桶中的元素进行排序,遍历每个桶,按照次序,将桶中的元素输出即可。

再举一个通俗栗子,我们有100个元素,其中每个元素x都满足(1 <= x <= 1000) 求其中个数最多的元素解决这个问题那我们只需要准备1000个桶,每个桶的编号为1,2,3,4,……1000即可,然后遍历元素,将每个元素的个数最高者输出即可!~!

真题时间到了!~!

Leetcode : 347. Top K Frequent Elements (Medium)

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2

输出: [1,2]

示例 2:

输入: nums = [1], k = 1

输出: [1]

class Solution(object):def topKFrequent(self, nums, k):""":type nums: List[int]:type k: int:rtype: List[int]"""a = {}for i in nums:if i in a.keys():a[i] += 1else:a[i] = 1b = sorted(a.items(),key=lambda x:x[1],reverse = True)result = []for i in range(k):result.append(b[i][0])return result

不多就这样了,希望本文能够帮到你!~!

最后打个小广告,我的公众号,喜欢写点学习中的小心得,不介意可以关注下!~!

堆排序时间复杂度_leetcode刷题(二):排序算法(归并排序,堆排序,桶排序)...相关推荐

  1. 排序算法——归并排序和桶排序

    文章目录 1. 归并排序 1.1 归并排序的思想 1.2归并排序的过程 1.3 归并排序的难点 1.4 归并排序源代码 1.5 归并排序性能分析 2.桶排序(基数排序) 2.1 基数排序的思想 2.2 ...

  2. js排序算法详解-桶排序

    全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-桶排序 一看到这个名字就会觉得奇特,几个意思,我排序还要再准备几个桶不成?还真别说,想用桶排序还得真准备几个桶 ...

  3. 【排序算法】图解桶排序

    目录 前言 桶排序思想 桶排序算法分析 时间复杂度分析 桶排序适用情况 实现一个桶排序 结语 前言 在数据结构与算法的排序中,我们很多人可能更多的熟悉冒泡排序.快速排序.归并排序.可能对堆排序.桶排序 ...

  4. 排序算法(五)——堆排序算法详解及Python实现

    本文目录 一.简介 二.算法介绍 三.代码实现 排序算法系列--相关文章 一.简介 堆排序(Heap Sort)算法,属于选择排序类,不稳定排序,时间复杂度O(nlogn). 堆排序由Floyd和Wi ...

  5. LeetCode刷题笔记(算法思想 四)

    LeetCode刷题笔记(算法思想 四) 七.动态规划 斐波那契数列 70. 爬楼梯 198. 打家劫舍 213. 打家劫舍 II 信件错排 母牛生产 矩阵路径 64. 最小路径和 62. 不同路径 ...

  6. 十大排序算法之堆排序

    十大排序算法之堆排序 本文采用Java书写选择排序,其他语言类似可以借鉴着写 思想:所谓堆排序就是通过构造最大堆(升序)或者最小堆(降序)来进行排列的方法.可能有些童鞋不知道何为最大堆,何为最小堆.这 ...

  7. Java常见排序算法之堆排序

    在学习算法的过程中,我们难免会接触很多和排序相关的算法.总而言之,对于任何编程人员来说,基本的排序算法是必须要掌握的. 从今天开始,我们将要进行基本的排序算法的讲解.Are you ready?Let ...

  8. leetcode数组汇总_LeetCode刷题:前言

    LeetCode刷题:前言 前言 作为一个对编程超级不通的小白,在2020年11月开始打算正式的刷LeetCode. (PS:前面有刷过,但是都是随机,看心情乱刷的,刷完后也没有什么感觉,该不会的还是 ...

  9. 排序算法之---堆排序(很重要的一个结构,新手入门必备)

    排序算法之---堆排序(很重要的一个结构,新手入门必备) 先来简单的介绍一下堆结构: 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlo ...

最新文章

  1. 孩子数学成绩不好怎么办_孩子数学成绩不好,家长可以这样做
  2. 《你的灯亮着吗》阅读笔记1
  3. 移动端 c++ 开发_这 10 点值得移动端开发重点学习
  4. mysql 标识符规则_MySQL 标识符到底区分大小写么——官方文档告诉你
  5. swing 显示文件下文件_Linux 文件权限详解
  6. [转]微服务轮子项目(27) -MySQL 主从复制、主从切换、主主复制
  7. 了解 Diffing 算法
  8. TUXEDO运行监控命令概述
  9. 初识等级保护,要点整理
  10. graphpad导出图片不居中_Graphpad如何导出期刊所需图
  11. 联通沃云 服务器使用点滴
  12. 《文献管理与信息分析》课程笔记
  13. 聊天室小程序服务端源码(客户端接下一条)
  14. java实现生成二维码及扫码登录
  15. 未来数字科技趋势分析与前沿热点解读
  16. Karaf-cellar 集群配置
  17. UIColor的RGB定义颜色(灰色)
  18. 没有内存,怎么还能跑程序呢
  19. 蓝牙 UUID识别码对应的服务类型
  20. 手机电路板文件_您的旧手机都去哪了?高价回收的背后有利益推手

热门文章

  1. 关于STM32库中 __IO 修饰符(volatile修饰符,反复无常的意思)
  2. Eclipse插件Ant里运用ftp遇到的问题
  3. SYNCHRONIZE_DRAIN
  4. 用无线卡王蹭网的亲身经历和感想
  5. 【VTK】VTK 之一 环境准备
  6. Knockoutjs之observable和applyBindings的使用
  7. loadrunner 检查点
  8. Spring依赖查找中的常见异常
  9. Spring内建可查找的依赖
  10. java生成二维码打印到浏览器