七种经典的常见排序算法(选择排序、冒泡排序、插入排序、希尔排序、归并排序、快速排序、堆排序)

写在前面

参考源码

花了小半周的时间来总结了常见的的排序算法,由于编程能力的欠缺,整理过程也是磕磕绊绊,好在最后bug都解决完了
本文主要写了七种常见排序算法:选择排序、冒泡排序、插入排序、希尔排序、归并排序、快速排序、堆排序
测试已经全部通过,如果有错误或者可以优化某种算法之类的,请大佬们多多赐教!!!
说明:参考源码中的Visual Studio C++工程文件,主要实现方法在各个.cpp源文件中,DataChecker.cpp相当于对数器的功能,来检测排序算法是否正确,里面的测试次数,数组长度之类的都可以自行改变~

1.选择排序

特点

不稳定

算法步骤

首先在未排序序列中选定第一个元素,然后遍历所有元素与其比较,找到最小(大)元素,交换其两个元素的位置。
再选中第二个元素,往后遍历与其比较,找到最小(大)的元素,交换两个元素的位置。
重复上述步骤,直到所有元素均排序完毕。

复杂度

时间复杂度O(N2)
空间复杂度O(1)

改进

每次循环不仅找到最大值,还可以找到最小值,这样就能降低复杂度。

2.冒泡排序

特点

稳定

算法步骤

第一遍从第一对儿开始比较直到最后一对儿,大的放后面(默认从小到大)。
第二遍还是从第一对儿开始比较,直到倒数第二对儿,因为最大数已经在最后一个位置了。
重上上述步骤

复杂度

时间复杂度O(N2)
空间复杂度O(1)

最好情况

已经是正序排序O(N)

最坏情况

已经是逆序排序O(N2)

改进

改进思路:因为冒泡排序每次都会遍历所有,因此在第二次遍历时,最后两个值已经无须比较(最后一个值已经是全局最大值),此处改进可以减少比较次数
当序列已经是正序排序时间,那么时间复杂度为O(N)

3.插入排序

特点

稳定

算法步骤

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
从第二个数据开始,和第一个数比较,如果小于第一个数则交换位置;第三个数字和前两个数字比较,找到合适的位置,并且插入,使得前三个数字呈升序排序。

复杂度

时间复杂度O(N2)
空间复杂度O(1)

最好情况

内循环执行一次就发现,不需要移动,时间复杂度为O(N)

改进

折半插入
折半插入排序就是用折半查找迅速找到需要插入的地方,而不用从i-1开始逐一向前比较

4.希尔排序

特点:

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
间隔大,移动次数少
间隔小,移动距离短 --------->不稳定排序算法

算法步骤

选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

复杂度

时间复杂度O(N1.3)
空间复杂度O(1)

最好情况

内循环执行一次就发现,不需要移动,时间复杂度为O(N)

改进

间隔序列选用 Knuth序列 排序最快
h=3*h+1< 总长度/3

5.归并排序

特点

作为一种典型的分治思想的算法应用,归并排序的实现有两种方法:
1.自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
2.自下而上的迭代;
稳定排序

算法步骤

申请O(N)空间
设定两个指针,最初位置分别为序列开始与中间位置;
然后将其递归不断进行二分划分区间
将其归并,取两个区间的最小值,然后放置到新的空间上。
最后再给原始序列重新赋值,改变原始序列,即排好序

复杂度

时间复杂度O(NlogN)
空间复杂度O(N)

改进

Timsort(改进的归并排序)的多路归并
TimSort,在小于MIN_MERGE时,用二分插入,而不是快排,这是不同的地方。

6.快速排序

特点

不稳定

算法步骤

从数列中挑出一个元素,称为 pivot;
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
如果pivot前面的数字比它大,pivot后面的数字比它小,交换这两个数字的位置。
在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

复杂度

时间复杂度O(NlogN):
需要不停的切分,切分一次就是分成两半,就是不停地把每一半再分成两半,就是logN
然后每一层都需要进行比较(甚至交换),所以是O(N),因此就是O(NlogN)
空间复杂度:O(logN) 每次递归需要调用一些栈,需要一些空间

最坏情况

每次都是选择在最边缘的数字上,比如选了个最大的数,每次排序,某一侧只有一个数。复杂度就是O(N2)

7.堆排序

特点

大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

TOP最大K问题

创建一个K的小顶堆,只有当下一个数字比堆顶大的时候,将堆顶元素换为该数字,然后重复同样的步骤,遍历所有数字,则堆内的元素则为最小的K个数字。
大小大

TOP最小K问题

创建一个K的大顶堆,只有当下一个数字比堆顶小的时候,将堆顶元素换为该数字,然后重复同样的步骤,遍历所有数字,则堆内的元素则为最小的K个数字。
小大小

算法步骤

创建一个堆 H[0……n-1];

把堆首(最大值)和堆尾互换;

把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;

重复步骤 2,直到堆的尺寸为 1。

复杂度

时间复杂度O(NlogN)
空间复杂度O(1)

4.22更新

最近在做快排题的时候,发现一个问题,也是思考了有两天的时间,今天问了身边的一个大佬总算是解决了。
可以查看这个Blog
具体来说,就是pivot的选择问题和while循环的方向问题,大部分场景都是randomselect一个pivot放到和数组的第一个元素交换,然后进行,这样就避免产生方向问题了。
希望如果有童鞋注意到这个问题,可以在这里寻找到答案。(必要的思考时间还是值得的,这样印象才能更加深刻)

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值