排序算法分类
- 内部排序:指在排序期间,元素全部存放在内存中的排序,常见的内部排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、基数排序等。
- 外部排序:指在排序期间,元素无法完全全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序;
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。 常见的非比较类排序算法有:基数排序、计数排序、桶排序等
一般情况下,内部排序算法在执行过程中都要进行两种操作:比较和移动。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。但是,并非所有的内部排序算法都要基于比较操作。
每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序和基数排序五大类,内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。
排序算法 | 时间复杂度(平均) | 时间复杂度(最好) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
插入排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
希尔排序 | O(nlogn) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1)O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n2) | O(n+k) | 稳定 |
基数排序 | O(n∗k) | O(n∗k) | O(n∗k) | O(n+k) | 稳定 |
稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序是否相同。例:如果 a 原本在 b 前面,且 a=b,排序之后 a 仍然在 b 的前面,则表示具有稳定性。
常见时间复杂度大小比较:
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<…<O(2n)<O(n!)O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<…<O(2n)<O(n!)
一、冒泡排序(Bubble Sort)
1、原理
重复地走访要排序的元素,依次比较两个相邻的元素,如果顺序(如从大到小)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。冒泡的意思其实就是每一轮冒泡一个最大的元素就会通过不断比较和交换相邻元素使它转移到最右边。
假如有 10 个小盆友从左到右站成一排,个头不等。老师想让他们按照个头从低到高站好,于是他开始喊口号。 每喊一次,从第一个小盆友开始,相邻的小朋友如果身高不是正序就会两两调换,就这样第一轮个头最高的排到了最右边(冒泡到最右边),第二轮依次这么来,从第一个小朋友开始两两交换,这样次高的小盆友又排到了倒数第二个位置。依次类推。
2、步骤
- ① 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- ② 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- ③ 针对所有的元素重复步骤 ① ~ ②,除了最后一个元素,直到排序完成。
3、动画演示
4、代码实现
1 | def bubbleSort(arr): |
冒泡排序还有一种优化算法,就是立一个 flag,当某一趟序列遍历中元素没有发生交换,则证明该序列已经有序,就不再进行后续的排序。动画演示里就是改进后的算法,改进后的代码如下:
1 | def bubbleSort(arr): |
冒泡排序最快的情况:当输入的数据是正序时;最慢的情况:当输入的数据是反序时。
5、具体示例
未改进版本:
1 | def bubble_sort(arr): |
1 | [3, 38, 5, 44, 15, 36, 26, 27, 2, 46, 4, 19, 47, 48, 50] |
改进版本:
1 | def bubble_sort(arr): |
二、选择排序(Selection Sort)
1、原理
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。可以理解为 一个 0 到 n-1 的迭代,每次向后查找选择一个最小的元素。选择排序是不稳定的排序方法。
假如有 10 个小盆友从左到右站成一排,个头不等。老师想让他们按照个头从低到高站好,我们从第一个开始,从头到尾找一个个头最小的小盆友,然后把它和第一个小盆友交换。 然后从第二个小盆友开始采取同样的策略,这样一圈下来小盆友就是有序的了。
2、步骤
- ① 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- ② 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
- ③ 重复步骤 ②,直到所有元素均排序完毕。
3、动画演示
4、代码实现
Python 代码:
1 | def selection_sort(arr): |
5、具体示例
1 | def selection_sort(arr): |
三、插入排序(Insertion Sort)
1、原理
插入排序一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。它的基本思想是将一个记录插入到已经排好序的有序表中,从而形成一个新的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素进行遍历,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。拿在左手上的牌总是排序好的,原来这些牌是桌子上牌堆中顶部的牌。
2、步骤
- ① 从第一个元素开始,该元素可以认为已经被排序;
- ② 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- ③ 如果该元素(已排序的)大于新元素,将该元素往右移到下一位置,重复该步骤,直到找到已排序的元素小于或者等于新元素的位置;
- ④ 将新元素插入到步骤 ③ 找到的位置的后面;
- ⑤ 重复步骤 ② ~ ④。
3、动画演示
4、代码实现
1 | def insertion_sort(arr): |
5、具体示例
1 | def insertion_sort(arr): |
四、希尔排序(Shell Sort)
1、原理
希尔排序是插入排序的一种更高效的改进版本,是一种分组插入排序算法,又称缩小增量排序(Diminishing Increment Sort),希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
2、步骤
- ① n 为数组长度,首先取一个整数 d1=n/2,将元素分为 d1 个组,每组相邻量元素之间距离为 d1-1,在各组内进行直接插入排序;
- ② 取第二个整数 d2=d1/2,重复步骤 ① 分组排序过程,直到 di=1,即所有元素在同一组内进行直接插入排序。
PS:希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。
3、动画演示
4、代码实现
1 | def insertion_sort_gap(arr, gap): # 将 gap 看做隔 gap 个距离摸一张牌,而不是依次按照顺序摸牌 |
也可以不使用两个函数,写在一起即可:
1 | def shell_sort(arr): |
5、具体示例
1 | def insertion_sort_gap(arr, gap): # 将 gap 看做隔 gap 个距离摸一张牌,而不是依次按照顺序摸牌 |
五、归并排序(Merge Sort)
1、原理
归并的概念:假设一个列表分为两段,其中每一段都是有序列表,现在将该两段合并为一个有序列表,这种操作称为一次归并。
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
2、步骤
归并的基本步骤:
- ① 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- ② 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- ③ 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- ④ 重复步骤 ③ 直到某一指针达到序列尾;
- ⑤ 将另一序列剩下的所有元素直接到合并序列尾。
归并排序的步骤:
- ① 分解:将列表越分越小,直至分成一个元素,终止条件:一个元素是有序的。
- ② 合并:不断将两个有序列表进行归并,列表越来越大,直到所有序列归并完毕。
3、动画演示
4、代码实现
1 | def merge(arr, low, mid, high): |
5、具体示例
1 | def merge(arr, low, mid, high): |
六、快速排序(Quick Sort)
1、原理
快速排序是对冒泡排序的一种改进。顾名思义快速排序就是快,而且效率高!它是处理大数据最快的排序算法之一了。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
2、步骤
- ① 从数列中挑出一个元素,称为 “基准值”;
- ② 重新排序数列,所有元素比基准值小的放在基准值的左边,比基准值大的放在基准值的右边(相同的数可以到任一边)。在这个分区退出之后,该基准值就处于数列的中间位置。这个称为分区(partition)操作,也可以称为一次归位操作,归位操作的过程见下动图;
- ③ 递归地把小于基准值元素的子数列和大于基准值元素的子数列按照步骤 ① ② 排序。
3、动画演示
4、代码实现
1 | def partition(arr, left, right): |
5、具体示例
1 | def partition(arr, left, right): |
七、堆排序(Heap Sort)
1、原理
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
- 堆:一种特殊的完全二叉树结构
- 大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大
- 小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小
2、步骤
- ① 构建堆:将待排序序列构建成一个堆 H[0……n-1],从最后一个非叶子结点开始,从左至右,从下至上进行调整。根据升序或降序需求选择大顶堆或小顶堆;
- ② 此时的堆顶元素,为最大或者最小元素;
- ③ 把堆顶元素和堆尾元素互换,调整堆,重新使堆有序;
- ④ 此时堆顶元素为第二大元素;
- ⑤ 重复以上步骤,直到堆变空。
3、动画演示
堆构建完成后再进行推排序:
4、代码实现
1 | def sift(arr, low, high): |
5、具体示例
1 | def sift(arr, low, high): |
八、计数排序(Counting Sort)
1、原理
计数排序是一个非基于比较的排序算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为 Ο(n+k),其中 k 是整数的范围,快于任何比较排序算法。计数排序是一种牺牲空间换取时间的做法。计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
2、步骤
- ① 找到待排序列表中的最大值 k,开辟一个长度为 k+1 的计数列表,计数列表中的值都为 0。
- ② 遍历待排序列表,如果遍历到的元素值为 i,则计数列表中索引 i 的值加1。
- ③ 遍历完整个待排序列表,计数列表中索引 i 的值 j 表示 i 的个数为 j,统计出待排序列表中每个值的数量。
- ④ 创建一个新列表(也可以清空原列表,在原列表中添加),遍历计数列表,依次在新列表中添加 j 个 i,新列表就是排好序后的列表,整个过程没有比较待排序列表中的数据大小。
3、动画演示
4、代码实现
1 | def count_sort(arr): |
5、具体示例
1 | def count_sort(arr): |
九、桶排序(Bucket Sort)
1、原理
桶排序又叫箱排序,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。
桶排序也是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量;
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
- 最快情况:当输入的数据可以均匀的分配到每一个桶中;
- 最慢情况:当输入的数据被分配到了同一个桶中。
2、步骤
- ① 创建一个定量的数组当作空桶子;
- ② 遍历序列,把元素一个一个放到对应的桶子去;
- ③ 对每个不是空的桶子进行排序;
- ④ 从不是空的桶子里把元素再放回原来的序列中。
3、动画演示
(动图来源于@五分钟学算法,侵删)
4、代码实现
1 | def bucket_sort(arr): |
5、具体示例
1 | def bucket_sort(arr): |
十、基数排序(Radix Sort)
1、原理
基数排序属于分配式排序,是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序、计数排序、桶排序三种排序算法都利用了桶的概念,但对桶的使用方法上是有明显差异的:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值。
2、步骤
- ① 取数组中的最大数,并取得位数;
- ② 从最低位开始,依次进行一次排序;
- ③ 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
3、动画演示
4、代码实现
1 | def radix_sort(li): |
5、具体示例
1 | def radix_sort(li): |