排序算法总结
文章有转载其他大佬博客成分,如有侵权,请联系删除,谢谢
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 空间 | 稳定性 |
---|---|---|---|---|---|
冒泡 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
简单选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
直接插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | O ( n l o g n ) − − O ( n ) O(nlog_n) -- O(n) O(nlogn)−−O(n) | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
堆排序 | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
归并排序 | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
快速排序 | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( n l o g n ) O(nlog_n) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( n l o g n ) − − O ( n ) O(nlog_n) -- O(n) O(nlogn)−−O(n) | 不稳定 |
计数排序 | O ( n + k ) O(n + k) O(n+k) | O ( n + k ) O(n + k) O(n+k) | O ( n + k ) O(n + k) O(n+k) | O ( n + k ) O(n + k) O(n+k) | 稳定 |
桶排序 | O ( n + k ) O(n + k) O(n+k) | O ( n + k ) O(n + k) O(n+k) | O ( n 2 ) O(n^2) O(n2) | O ( n + k ) O(n + k) O(n+k) | 稳定 |
基数排序 | O ( k ∗ N ) O(k*N) O(k∗N) | O ( k ∗ N ) O(k*N) O(k∗N) | O ( k ∗ N ) O(k*N) O(k∗N) | O ( n + k ) O(n + k) O(n+k) | 稳定 |
1. 选择排序
选择排序是一种简单直观的排序算法,它的工作原理是每一次从待排序的数据元素中选出最小(最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法。
选择排序的方法主要有两种,分别是简单选择排序以及堆排序,它们都是从待排序的数据元素中选择合适的元素放到合适的位置来进行排序。
简单排序
基本思想:每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。
- 步骤一: 拿到一个初始序列[8,9,1,3,5,2,7,6], 从首位开始, 设定未这个数为最小数, 然后依次与其他数进行比较, 如果遇到比其小的就与其交换, 经过一轮循环后最小的数就在最始端。
第一轮交换后:
- 步骤二: 重复第一步操作, 直到最后一个数
每一轮交换后的结果:
总结:
时间复杂度: 选择排序不管原始序列是否顺序逆序,都需要遍历数组才能找到峰值元素,比较操作均为 n(n - 1) / 2
次;交换操作最好情况为0次,最坏为(n - 1)次。综上,最好、最坏和平均情况的时间复杂度都为O(n²)
;
稳定性:选择排序在排序过程中,元素两两交换时,相同元素的前后顺序会发生改变 (比如:序列5 5 7 2 9,第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,而冒泡排序不会),所以选择排序是一种不稳定的排序算法
堆排序
前提知识
堆: 是一种数据结构,可以看作是一颗完全二叉树,又具有以下性值
- 大顶堆: 每个节点的值 都 大于或等于其左右子节点的值.
- 小顶堆: 每个节点的值 都 小于或等于其左右子节点的值.
堆排序是 利用堆的数据结构设计的排序算法, 堆排序是一种选择排序, 最好,最坏,平均时间复杂度均为O(nlogn)
, 它也是不稳定排序.
堆排序的基本思想及步骤
堆排序的基本思想是:
- 将待排序序列构造成一个大顶堆, 此时, 整个序列的最大值就是堆顶的根节点
- 将其于末尾元素进行交换, 此时就将最大值放到了序列末尾, 然后将剩余n-1个元素重新构造成大顶堆
- 重复以上两步操作, 最后就能得到一个有序序列
- 步骤一: 拿到一个初始堆[5,6,3,4,8,1,7,9,2], 将其构造成大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
-
给定无序序列结构如下:
-
从最后一个非叶子结点开始, 从左至右,从下至上进行调整。
-
找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换
-
调整堆为大顶堆:交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
- 步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
-
将堆顶元素9和末尾元素4进行交换
-
重新调整结构,使其继续满足堆定义
-
再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
-
继续进行调整,交换,如此反复进行,最终使得整个序列有序
堆排序总结:
a. 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。
其中构建初始堆经推导复杂度为O(n)
,在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]
逐步递减,近似为nlogn
。所以堆排序时间复杂度一般认为就是O(nlogn)
级。
2. 快速排序
快速排序是对冒泡排序的一种改进, 快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-Conquer Method
)。
快速排序的基本思想是:通过在一趟排序找基准值将要排序的数据分割成独立的两部分,其中一部分小于基准值, 一部分大于基准值,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
- 步骤一: 拿到一个初始序列[6,1,2,7,9,3,4,5,10,8], 找一个基准值, 将这个序列分组。
- 找一个基准值, 就以第一位6 作为基准值。 刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即j=10),指向数字8。
- 哨兵i 和 哨兵j 对向移动, 哨兵j 从右往左找一个小于6的数,哨兵i从左往右找一个大于6的数,哨兵i指向了7, 哨兵j指向了5, 然后交换他们。
- 重复上一轮操作, 直到哨兵i 和 哨兵j指向同一个数, 然后让基准值归位到哨兵i, j的位置
- 步骤二: 第一轮分组完成后, 在相对有序的两组数据内在进行快速排序, 分组完成后, 递归快速排序都最小分组, 最后得到有序序列
总结
- 时间复杂度: 快速排序的最差时间复杂度和冒泡排序是一样的都是
O(N2)
,它的平均时间复杂度为O(NlogN)
, 快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。
-
空间复杂度: 快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为
O(logn)
,在最差的情况下,若每次只完成了一个元素,那么空间复杂度为O(n)
。所以我们一般认为快速排序的空间复杂度为O(logn)
。 -
快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。
3. 归并排序
采用了分治策略 就是将原问题分解为一些规模较小的相似子问题,然后递归解决这些子问题,最后合并其结果作为原问题的解。
分解的过程很简单 就是一直递归向下分解 直到一个元素一组为止
基本思想:
-
将待排序数组一直往下分解直到不可分解为止也就是一个数为一个子数组
-
然后对这些子数组层层合并(合并里有排序的过程)得到最后的有序数组
-
合并原理: 要有一个临时数组temp 以及2个索引分别指向2个数组的首地址 让2个索引对应的值进行比较 谁对应的值小 把值小的放入temp 然后当前索引++ 再循环比较 肯定会有一个数组索引先到头 在对另一个数组剩下的元素遍历放入temp
步骤
-
步骤一: 拿到一个无序序列[10,4,5,3,8,2,5,7], 将数组均匀分成两组[10,4,5,3], [8,2,5,7], 将各组序列递进分组, 直至不可分为止
-
步骤二: 相邻组合并排序, 归并各组元素成最终的有序序列
动图展示
总结
- 时间复杂度: 和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是
O(nlogn)
的时间复杂度。代价是需要额外的内存空间。
4. 插入排序
插入排序有直接插入排序和希尔排序, 希尔排序是对插入排序的一种优化
直接插入排序
基本思想
1. 每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。
步骤
- 步骤一: 拿到一个无序序列[4,6,8,5,9], 将首个元素看作有序序列, 依次从左往右拿到一个元素, 对比有序序列, 将元素插入到有序序列的正确位置
- 步骤二: 依次从左往右插入到有序序列中, 得到最后的有序序列[4,5,6,8,9]
希尔排序
基本思想:希尔排序是把序列按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量的逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰好被分为一组,算法便终止。
步骤
- 步骤一: 拿到一个无序序列 [8,9,1,7,2,3,5,4,6,0], 设置初始增量, 根据增量对序列进行分组, 对各组元素分别进行插入排序
-
步骤二: 缩减增量, 对分组排序后的序列再次进行分组, 再对这两组进行直接插入排序
-
步骤三: 重复第二步, 直至增量缩到1, 将此时的序列再按插入排序的方法进行排序
总结
-
时间复杂度: 平均来说插入排序算法的时间复杂度为
O(n^2)
, 希尔排序的时间复杂度为O(N*(logN)2)
, 没有快速排序算法快O(N*(logN))
-
希尔排序非常容易实现,算法代码短而简单。
几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快, 再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当N值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。 当N值减小时每一趟需要和动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。
5. 计数排序
计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序要求输入的数据必须是有确定范围的整数。计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序.
基本思想: 计数排序使用一个额外的数组C,用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),然后进行分配、收集处理.
① 分配。扫描一遍原始数组,以
当前值-minValue
作为下标,将该下标的计数器增1。
② 收集。扫描一遍计数器数组,按顺序把值收集起来。
实现逻辑:
① 找出待排序的数组中最大和最小的元素
minValue
② 统计数组中每个值为n的元素出现的次数,存入数组C的第n-minValue
项,C[n-minValue]
计数+1
③ 对所有的计数累加
④ 反向填充目标数组:从C中第一个元素开始"取出", 取元素下标i+minValue
放入排序好的数组中, 每放入一个, C[i]元素计数-1
复杂度分析:
平均时间复杂度:O(n + k)
最佳时间复杂度:O(n + k)
最差时间复杂度:O(n + k)
空间复杂度:O(n + k)
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O ( n + k ) O(n + k) O(n+k)。在实际工作中,当 k = O ( n ) k=O(n) k=O(n)时,我们一般会采用计数排序,这时的运行时间为 O ( n ) O(n) O(n)。
计数排序需要两个额外的数组用来对元素进行计数和保存排序的输出结果,所以空间复杂度为 O ( k + n ) O(k+n) O(k+n)。
6. 桶排序
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列. 桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间 O ( n ) O(n) O(n)。但桶排序并不是比较排序,他不受到 O ( n l o g n ) O(n log n) O(nlogn)下限的影响。
基本思想: 近乎彻底的分治思想
观察知:数组的元素分布在(0-50)之间,我们可以将其分隔成五个区间分辨是[0-9],[10-19],[20-29],[30-39],[40-49];(桶的个数根据题意自定,只需确定好每个桶的存储范围就好;并非桶越多越好,也并非越少越好总之适当就好);
这五个区间看做五个桶;分别存放符合范围的数字;
将这五个区间分别排序,再输出;
为了使桶排序更加高效,我们需要做到这两点, 同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
1、在额外空间充足的情况下,尽量增大桶的数量;
2、使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中;
实现逻辑:
- 设置一个定量的数组当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里把项目再放回原来的序列中。
复杂度分析:
平均时间复杂度:O(n + k)
最佳时间复杂度:O(n + k)
最差时间复杂度:O(n ^ 2)
空间复杂度:O(n * k)
稳定性:稳定
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
总结: 桶排序是计数排序的变种,它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。把计数排序中相邻的m个”小桶”放到一个”大桶”中,在分完桶后,对每个桶进行排序(一般用快速排序),然后合并成最后的结果。
7. 基数排序
【算法】排序算法之基数排序 - 知乎 (zhihu.com)
原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)
或MSD(Most significant digital)
,LSD
的排序方式由键值的最右边开始,而MSD
则相反,由键值的最左边开始。
MSD
:先从高位开始进行排序,在每个关键字上,可采用计数排序LSD
:先从低位开始进行排序,在每个关键字上,可采用桶排序
实现逻辑:
① 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
② 从最低位开始,依次进行一次排序。
③ 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
分步图示说明:设有数组 array = {53, 3, 542, 748, 14, 214, 154, 63, 616},对其进行基数排序:
在上图中,首先将所有待比较数字统一为统一位数长度,接着从最低位开始,依次进行排序。
- 按照个位数进行排序。
- 按照十位数进行排序。
- 按照百位数进行排序。
排序后,数列就变成了一个有序序列。
复杂度分析:
时间复杂度:O(k*N)
空间复杂度:O(k + N)
稳定性:稳定
设待排序的数组R[1…n],数组中最大的数是d位数,基数为r(如基数为10,即10进制,最大有10种可能,即最多需要10个桶来映射数组元素)。
处理一位数,需要将数组元素映射到r个桶中,映射完成后还需要收集,相当于遍历数组一遍,最多元素数为n,则时间复杂度为 O ( n + r ) O(n+r) O(n+r)。所以,总的时间复杂度为 O ( d ∗ ( n + r ) ) O(d*(n+r)) O(d∗(n+r))。
基数排序过程中,用到一个计数器数组,长度为r,还用到一个 r ∗ n r*n r∗n的二位数组来做为桶,所以空间复杂度为 O ( r ∗ n ) O(r*n) O(r∗n)。
基数排序基于分别排序,分别收集,所以是稳定的。
总结:
基数排序与计数排序、桶排序这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
基数排序不是直接根据元素整体的大小进行元素比较,而是将原始列表元素分成多个部分,对每一部分按一定的规则进行排序,进而形成最终的有序列表。