图片
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内排序:所有排序操作都在内存中完成;
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
时间复杂度: 一个算法执行所耗费的时间。
空间复杂度: 运行完一个程序所需内存的大小。
冒泡排序
冒泡排序是最简单直观的排序方式,通过比较前后两个元素的大小,然后交换位置来实现排序。
每次比较相邻两个数的大小,如果前面的数大于后面的数,则交换两个数的位置(否则不变),向后移动。
改进冒泡排序:
冒泡排序第1次遍历后会将最大值放到最右边,这个最大值也是全局最大值。
同理,当前轮的最大值也都会放在最后,每轮结束后,最大值、次大值。。。都会固定,但是普通版冒泡排序每次都会比较全部元素。可以记录每轮比较后最后一个位置,也可以逆序遍历。
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法步骤:
- 初始序列arr,无序。分成有序区和无序区,有序区初始为0,不断变大;无序区初始为len(arr),不断变小。
- 遍历无序找到最小值,与无序区最左边交换。有序区长度+1。
- 重复第二步。
插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
改进插入排序: 查找插入位置时使用二分查找的方式
希尔排序
希尔排序又称递减增量排序、缩小增量排序,是简单插入排序的改进版,但是是非稳定算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入算法在对几乎已经排好序的数据操作时,效率高,即可达线性排序效率
- 但插入排序一般来说是低效的,因为每次只能将数据移动一位
希尔排序的基本思想是:先将整个待排序列分割成若干个子序列,对若个子序列分别进行插入排序,待整个待排序列基本有序时,对整体进行插入排序。
算法步骤:
- 选择一个增量序列t1,t2,…,tk,其中ti>ti+1,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
其实是两个两个换位置,将整个序列变成基本排好序的
图片
归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
递归版:
迭代版:
快速排序
快速排序是东尼-霍尔发展来的一种排序算法。 平均状态下,排序n个项目要做O(nlogn)次比较,最坏情况下,要做O(n2)次比较,但是比较少见。
事实上,快速排序通常明显比其他O(nlogn)算法速度要快,因为它的内部循环能够再大部分框架上很有效地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
为什么快速排序比其他O(nlogn)排序算法快呢?
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
算法步骤:
- 选择一个基准。
- 将比基准小的数放到基准左边,比基准大的数放到基准右边(相同的数可以放在任意一边)在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
单路快排:从左向右遍历
双路快排:双指针从首尾向中间移动
三路排序:分成小于区、等于区、大于区,不对等于区进行递归操作
堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。
分为两种方法:
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
- 创建一个堆 H[0……n-1]
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序的特征
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。
当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
算法的步骤如下:
1)找出待排序的数组中最大和最小的元素
2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
为了使桶排序更加高效,我们需要做到这两点:
1 在额外空间充足的情况下,尽量增大桶的数量
2 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
- 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
- 什么时候最慢
当输入的数据被分配到了同一个桶中。
示意图:
图片
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序:根据键值的每位数字来分配桶;
计数排序:每个桶只存储单一键值;
桶排序:每个桶存储一定范围的数值;
思路
基数排序是按照低位先排序,然后收集;
再按照高位排序,然后再收集;
依次类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。
最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
图片
总结
按时间复杂度分类:
- O(n2):冒泡排序、选择排序、插入排序
- O(nlogn):希尔排序、归并排序、快速排序、堆排序
- O(n):计数排序、桶排序、基数排序
按稳定性分类
- 稳定:冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序
- 不稳定:选择排序、希尔排序、快速排序、堆排序
按排序方式
- In-Place:冒泡排序、选择排序、插入排序、希尔排序、快速排序、堆排序
- Out-Place:归并排序、计数排序、桶排序、基数排序