排序算法分类
- 内部排序:指在排序期间,元素全部存放在内存中的排序,常见的内部排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、基数排序等。
- 外部排序:指在排序期间,元素无法完全全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序。
- 比较类排序::通过比较来决定元素间的相对次序,由于其时间复杂度不能突破$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$,因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。 常见的非比较类排序算法有:基数排序、计数排序、桶排序。
一般情况下,内部排序算法在执行过程中都要进行两种操作:比较和移动。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。但是,并非所有的内部排序算法都要基于比较操作。
每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序和基数排序五大类,内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。
一、冒泡排序 (Bubble Sort)
1.原理
每次检查相邻的两个元素,如果前面的元素与后面的元素满足给定的排序条件,就将相邻两个元素交换。当没有相邻元素需要交换时,排序就完成了。
经过$i$次扫面后,数列的末尾$i$项必然是最大的$i$项,因此冒泡排序最多需要扫描$n-1$遍数组就能完成扫描。
2.步骤
- ① 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- ② 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- ③ 针对所有的元素重复步骤 ① ~ ②,除了最后一个元素,直到排序完成。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O(\mathrm N^2)$
最佳时间复杂度:$\mathrm O(\mathrm N)$
最差时间复杂度:$\mathrm O(\mathrm N^2)$
空间复杂度:$\mathrm O(\mathrm 1)$
排序方式:In-place
稳定性:稳定
5.代码
1 | public void bubbleSort(int arr[]) { |
冒泡排序还有一种优化算法,就是立一个 flag,当某一趟序列遍历中元素没有发生交换,则证明该序列已经有序,就不再进行后续的排序。动画演示里就是改进后的算法,改进后的代码如下:
1 | public void bubbleSort(int arr[]) { |
二、选择排序 (Selection Sort)
1.原理
每次找出第$i$小的元素 (也就是$A_{i..n}$中最小的元素),然后将这个元素与数组第$i$个位置上的元素交换。
2.步骤
- ① 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- ② 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
- ③ 重复步骤 ②,直到所有元素均排序完毕。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O(\mathrm N^2)$
最佳时间复杂度:$\mathrm O(\mathrm N^2)$
最差时间复杂度:$\mathrm O(\mathrm N^2)$
空间复杂度:$\mathrm O(\mathrm 1)$
排序方式:In-place
稳定性:不稳定
5.代码
1 | public void selectionSort(int arr[]) { |
三、插入排序 (Insertion Sort)
1.原理
将排列元素划分为“已排序”和”未排序”两部分,每次从”未排序的”元素中选择一个插入到”已排序”的元素中的正确位置。
一个与插入排序相同的操作是打扑克牌时,从牌桌上抓一张牌。按牌面大小插到手牌后,再抓下一张牌。
2.步骤
- ① 从第一个元素开始,该元素可以认为已经被排序;
- ② 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- ③ 如果该元素(已排序的)大于新元素,将该元素往右移到下一位置,重复该步骤,直到找到已排序的元素小于或者等于新元素的位置;
- ④ 将新元素插入到步骤 ③ 找到的位置的后面;
- ⑤ 重复步骤 ② ~ ④。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O(\mathrm N^2)$
最差时间复杂度:$\mathrm O(\mathrm N^2)$
空间复杂度:$\mathrm O(\mathrm 1)$
排序方式:In-place
稳定性:稳定
5.代码
1 | public void insertSort(int arr[]) { |
四、希尔排序 (Shell Sort)
1.原理
排序对不相邻的记录进行比较和移动:
将待排序序列分为若干子序列(每个子序列的元素在原始数组中间距相同)
对这些子序列进行插入排序;
减小每个子序列中元素之间的间距,重复上述过程直至间距减少为 1。
2.步骤
① n 为数组长度,首先取一个整数$d1=n/2$,将元素分为$d1$ 个组,每组相邻量元素之间距离为$d1-1$,在各组内进行直接插入排序;
② 取第二个整数$d2=d1/2$,重复步骤 ① 分组排序过程,直到$di=1$,即所有元素在同一组内进行直接插入排序。
PS:希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
最佳时间复杂度:
最差时间复杂度:$\mathrm O(\mathrm N^2)$
空间复杂度:$\mathrm O(\mathrm 1)$
稳定性:不稳定
复杂性:较复杂
5.代码
1 | public void shellSort(int arr[]) { |
五、归并排序 (Merge Sort)
1.原理
归并排序分为三个步骤:
- 将数列划分为两部分;
- 递归地分别对两个子序列进行归并排序;
- 合并两个子序列。
不难发现,归并排序的前两步都很好实现,关键是如何合并两个子序列。注意到两个子序列在第二步中已经保证了都是有序的了,第三步中实际上是想要把两个 有序 的序列合并起来。
2.步骤
归并的基本步骤
- ① 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- ② 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- ③ 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- ④ 重复步骤 ③ 直到某一指针达到序列尾;
- ⑤ 将另一序列剩下的所有元素直接复制到合并序列尾。
归并排序的步骤
- ① 分解:将列表越分越小,直至分成一个元素,终止条件:一个元素是有序的。
- ② 合并:不断将两个有序列表进行归并,列表越来越大,直到所有序列归并完毕。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
最佳时间复杂度:$\mathrm O(\mathrm N)$
最差时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
空间复杂度:$\mathrm O(\mathrm N)$
排序方式:In-place
稳定性:稳定
5.代码
迭代法:
1 | public void mergeSort(int arr[]) { |
递归法:
1 | static void mergeSortRecursive(int[] arr, int[] result, int start, int end) { |
六、快速排序 (Quick sort)
1.原理
通过分治的方式来将一个数组排序。
快速分为三个过程:
- 将数列划分为两部分 (要保证相对大小关系);
- 递归到连个子序列中分别进行快速排序;
- 不用合并,因为此时数列已经完全有序。
和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。为了保证平均时间复杂度,一般是随机选择一个数$m$来当作两个子数列的分界。
之后,维护一前一后两个指针$p$和$q$,依次考虑当前的数是否放在了应该放的位置 (前还是后)。如果当前的数没放对,比如说如果后面的指针$q$遇到了一个比$m$小的数,那么可以交换$p$和$q$位置上的数,再把$p$向后移一位。当前的数的位置全放对后,再移动指针继续处理,直到两个指针相遇。
2.步骤
- ① 从数列中挑出一个元素,称为 “基准值”;
- ② 重新排序数列,所有元素比基准值小的放在基准值的左边,比基准值大的放在基准值的右边(相同的数可以到任一边)。在这个分区退出之后,该基准值就处于数列的中间位置。这个称为分区(partition)操作,也可以称为一次归位操作,归位操作的过程见下动图;
- ③ 递归地把小于基准值元素的子数列和大于基准值元素的子数列按照步骤 ① ② 排序。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
最佳时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
最差时间复杂度:$\mathrm O(\mathrm N^2)$
空间复杂度:根据实现方式的不同而不同
5.代码
1 | class quickSort { |
七、堆排序 (Heap Sort)
1.原理
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
- 堆:一种特殊的完全二叉树结构
- 大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大
- 小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小
2.步骤
- ① 构建堆:将待排序序列构建成一个堆 H[0……n-1],从最后一个非叶子结点开始,从左至右,从下至上进行调整。根据升序或降序需求选择大顶堆或小顶堆;
- ② 此时的堆顶元素,为最大或者最小元素;
- ③ 把堆顶元素和堆尾元素互换,调整堆,重新使堆有序;
- ④ 此时堆顶元素为第二大元素;
- ⑤ 重复以上步骤,直到堆变空。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
最佳时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
最差时间复杂度:$\mathrm O({\mathrm{Nlog}}_2\mathrm N)$
稳定性:不稳定
5.代码
1 | import java.util.Arrays; |
八、计数排序 (Counting Sort)
1.原理
使用一个额外的数组 $C$,其中第$i$ 个元素是待排序数组$A$中值等于$i$的元素的个数,然后根据数组$C$来将$A$中的元素排到正确的位置。
他的工作过程分为三个步骤:
- 计算每个数出现了几次;
- 求出每个数的前缀和;
- 利用出现次数的前缀和,从右至左计算每个数的排名。
2.步骤
- ① 找到待排序列表中的最大值 k,开辟一个长度为 k+1 的计数列表,计数列表中的值都为 0。
- ② 遍历待排序列表,如果遍历到的元素值为 i,则计数列表中索引 i 的值加1。
- ③ 遍历完整个待排序列表,计数列表中索引 i 的值 j 表示 i 的个数为 j,统计出待排序列表中每个值的数量。
- ④ 创建一个新列表(也可以清空原列表,在原列表中添加),遍历计数列表,依次在新列表中添加 j 个 i,新列表就是排好序后的列表,整个过程没有比较待排序列表中的数据大小。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O(\mathrm N+\mathrm K)$
最佳时间复杂度:$\mathrm O(\mathrm N+\mathrm K)$
最差时间复杂度:$\mathrm O(\mathrm N+\mathrm K)$
空间复杂度:$\mathrm O(\mathrm N+\mathrm K)$
5.代码
1 | public class CountingSort { |
九、桶排序 (Bucket Sort)
1.原理
桶排序又叫箱排序,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。
桶排序也是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量;
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
- 最快情况:当输入的数据可以均匀的分配到每一个桶中;
- 最慢情况:当输入的数据被分配到了同一个桶中。
2.步骤
- ① 创建一个定量的数组当作空桶子;
- ② 遍历序列,把元素一个一个放到对应的桶子去;
- ③ 对每个不是空的桶子进行排序;
- ④ 从不是空的桶子里把元素再放回原来的序列中。
3.动画演示
4.性能分析
平均时间复杂度:$\mathrm O(\mathrm N+\mathrm K)$
最佳时间复杂度:$\mathrm O(\mathrm N+\mathrm K)$
最差时间复杂度:$\mathrm O(\mathrm N^2)$
空间复杂度:$\mathrm O(\mathrm N\ast\mathrm K)$
稳定性:稳定
5.代码
1 | public class BucketSort { |
十、基数排序 (Radix Sort)
1.原理
基数排序属于分配式排序,是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序、计数排序、桶排序三种排序算法都利用了桶的概念,但对桶的使用方法上是有明显差异的:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值。
2.步骤
- ① 取数组中的最大数,并取得位数;
- ② 从最低位开始,依次进行一次排序;
- ③ 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
3.动画演示
4.性能分析
时间复杂度:$\mathrm O(\mathrm N\ast\mathrm K)$
空间复杂度:$\mathrm O(\mathrm N+\mathrm K)$
稳定性:稳定
5.代码
1 | public class RadixSort{ |