排序算法概述
排序算法主要分为内部排序和外部排序两大类别
- 内部排序 :将数据加载进内存进行排序
- 外部排序 :使用内存和外存相结合的方式对数据进行排序
相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律
- 空间复杂度:指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数
- k代表数值中的”数位”个数
- n代表数据规模
- m代表数据的最大值减最小值
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | |
| 插入排序 | O(n2) | O(n2) | O(n) | O(1) | |
| 选择排序 | O(n2) | O(n2) | O(n2) | O(1) | |
| 快速排序 | O(nlogn) | O(n2) | O(nlogn) | O(nlogn) | |
| 希尔排序 | O(nlogn) | O(n2) | O(n) | O(1) | |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | |
| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | |
| 计数排序 | O(n + m) | O(n + m) | O(n + m) | O(n + m) | |
| 桶排序 | O(n + m) | O(n2) | O(n) | O(n + m) | |
| 基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) |
冒泡排序
基本思想
通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
冒泡排序的优化
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。
代码实现
1 | public void bubbleSort(int[] arr) { |
优化
1 | public void bubbleSort(int[] arr) { |
快速排序
基本思想
- 从数组中取出一个数,称之为基数(pivot)
- 遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域
- 将左右两个区域视为两个数组,重复前两个步骤,递归排序即可
基准元素的选择
快速排序是首先定义基准元素 –> 基准元素的选取一般分三种
- 数组起始元素–> 下标为0
- 数组的末尾元素 –> 下标为
arr.length - 1 - 随机选取
选择的基数不同,算法的实现也不同。实际上随机选取的方式的平均时间复杂度是最优的。
分区方法的设计
分区方法的目的包括
- 将当前数组元素按照基准元素分区,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。
- 返回当前基准元素的下标 ,为接下来的递归分区确定边界。 –> 基准值的下标在分区的过程是不断变化的
递归退出的条件
我们定义两个指针L, R, 初始分别指向数组的起始和末尾位置 –> L = 0; R =arr.length - 1;
在递归的过程中根据基准元素下标的变化动态的调整 L 及 R ,当 L >= R 时即退出递归
- L = R :分区中只有一个元素
- L > R:分区中没有元素
代码实现
移动”填坑法”实现分区方法
1 | public void quickSort(int[] arr, int L, int R) { |
交换法实现分区方法
1 | public void quickSort(int[] arr, int L, int R) { |
插入排序
基本思想
把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,并依次与有序表中的元素值进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
代码实现
交换法
1 | public void insertSort(int[] arr) { |
移动法
1 | public static void insertSort(int[] arr) { |
希尔排序
基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;
随着增量逐渐减少,每组包含的元素越来越多,当增量减至 1 时,所有元素恰被分成一组。
算法图解
代码实现
交换法 -> 分组 + 冒泡交换
1 | public static void shellSort(int[] arr) { |
移动法 -> 分组 + 插入移动 ==> 真正的希尔排序
1 | public static void shellSort(int[] arr) { |
简单选择排序
基本思想
从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
同冒泡排序一样都是双层循环,只不过选择排序是每次选定外层循环为基准值 -> 代表最小值;内层循环每次找到当前循环的最小值,并与外层的基准元素进行交换。每次都将最小值交换到序列头部,当外层循环结束的时,序列自然有序~
代码实现
1 | public void selectSort(int[] arr) { |
堆排序
基本思想
将无序序列构建成一个堆,根据升序或者降序要求选择大顶堆或者小顶堆
将堆顶元素与末尾元素交换 ==> 将最大元素”沉”到数组的末端
每次交换之后重新调整结构 使其满足堆结构,然后继续交换堆顶元素与当前末尾元素 反复交换 + 调整 直到整个序列有序
注意
堆排序并不是要我们真的建立一颗二叉树 而是借助 顺序存储二叉树的思想 将数据存储从数组形式 ==> 二叉树的存储形式
本质仍然是从一个数组的一般排序状态 ===> 当前数组的特定排序状态
与简单选择排序的区别
选择排序每次通过全盘扫描的方式找到当前循环的最大值 而堆排序是通过创建堆结构的形式通过取出堆顶元素找到当前循环的最大值
代码实现
1 | public static void heapSort(int[] arr) { |
归并排序
基本思想
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法
先将整个待排序的数组向下分割成最小块,然后将最小单元按照排序规则向下逐渐进行组合
注意:
在拷贝到原数组时并不是只拷贝最后一次,而是从栈顶开始合并时就开始拷贝,一共合并arr.length - 1 次,所以也一共拷贝了arr.length - 1次
算法图解
代码实现
1 | public static void mergeSort(int[] arr) { |
计数排序
直观的想法
- 定义一个能够覆盖数组中最小值到最大值之间各数的数组 –> 作为计数数组
- 从头到尾遍历数组中的各个元素,在计数数组中存储各个数字出现的次数
- 最后遍历计数数组,将每个元素输出,输出的次数就是对应位置记录的次数
[1,9]数字范围的代码实现如下:
1 | public static void main(String[] args) { |
运行结果
1 | 排序前[1, 2, 3, 4, 5, 6, 9, 8, 2, 1, 1, 5, 7, 6, 4, 5, 6, 1, 8, 4] |
问题
我们发现,在排序完成后,arr 中记录的元素已经不再是最开始的那个元素了,他们只是值相等,但却不是同一个对象。也就是说这样的算法实现是不稳定的—> 因为被排序的对象往往都会携带其他的属性,但这份算法将被排序对象的其他属性都丢失了。
计数排序的基本思想
通过上面的分析我们发现只是通过简单的统计然后按照下标来赋值这样的方式是不稳定的。那么我们可以通过计数的结果,先统计出每个元素在排序完成后的位置,然后将元素赋值到对应位置即可。
算法图解
[1,9]数字范围的代码实现如下:
1 | public static void countingSort(int[] arr) { |
优化代码使其适用于不同数字范围的的排序,代码实现如下:
代码实现-> 真正的计数排序
1 | public static void countingSort(int[] arr) { |
桶排序
基本思想
将数据集划分多个范围相同的区间,每个自区间自排序,最后合并。
与计数排序的联系
桶排序是对计数排序的改进,计数排序申请的额外空间跨度从最小元素值到最大元素值,若待排序集合中元素不是依次递增的,则必然有空间浪费情况。桶排序则是弱化了这种浪费情况,将最小值到最大值之间的每一个位置申请空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况。
实现步骤
- 根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
- 遍历待排序集合,将每一个元素移动到对应的桶中;
- 对每一个桶中元素进行排序,并移动到已排序集合中;
算法图解
将元素分配到桶中
对桶中的元素进行排序
代码实现
1 | public static void bucketSort(int[] arr) { |
基数排序
基本思想
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零
然后,从最低位开始,依次进行一次排序。 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
主要步骤
- 找出数组中最大的数字的位数
maxLength - 获取数组中每个数字的基数
- 遍历
maxLength轮数组,每轮按照基数对其进行排序
负数的处理
在对基数进行计数排序时,申请长度为 19 的二维数组数组–> 申请19个桶,用来存储 [-9, 9] 这个区间内的所有整数。
在把每一位基数计算出来后,加上 9,就能对应上bucketElementCounts 数组的下标了,bucketElementCounts 数组的下标 [0, 18] 对应基数 [-9, 9];
代码实现
1 | public static void radixSort(int[] arr) { |