文章目录
一、排序算法比较
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
希尔排序 | O(n㏒n) | O(n㏒²n) | O(1) | 不稳定 |
快速排序 | O(n㏒n) | O(n²) | O(㏒n) | 不稳定 |
归并排序 | O(n㏒n) | O(n㏒n) | O(n) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n+k) | 稳定 |
堆排序 | O(n㏒n) | O(n㏒n) | O(1) | 不稳定 |
二、冒泡排序
1. 原理
数组相邻两个元素之间两两比较,如果前一个元素大于后一个元素,则两个元素交换位置,第一轮结束后,最大值会到达最大索引处,重复前面的步骤,即可得到一个排好序的数组。
2. 思路
每一次数组相邻元素之间两两比较,如果前一个元素大于后一个元素,则交换位置
每一轮比较完毕,下一轮就会减少一个元素的比较:
第一轮比较,有 0 个元素不比较
第二轮比较,有 1 个元素不比较(最大值已到达最大索引,不需比较)
… … … … … … … … … … … … …
一共需要比较数组长度减 1 轮
思路图示:
3. 代码实现
代码如下:
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {9, 3, 45, -2, 7, -23};
bubbleSort(arr);
}
/**
* 冒泡排序
*
* @param arr 待排序的数组
*/
public static void bubbleSort(int[] arr) {
//一共比较 arr.length - 1 轮
for (int i = 0; i < arr.length - 1; i++) {
//标识某一轮比较是否发生交换
boolean flag = false;
//每轮比较 arr.length - i - 1 次
for (int j = 0; j < arr.length - i - 1; j++) {
//如果前一个元素大于后一个元素,则两个元素交换位置
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
//打印每一轮排序后的结果
System.out.println("第" + (i + 1) + "轮排序后:");
System.out.println(Arrays.toString(arr));
if (!flag) {
break;
}
}
}
}
测试结果:
第1轮排序后:
[-23, -2, 7, 3, 9, 45]
第2轮排序后:
[-23, -2, 3, 7, 9, 45]
第3轮排序后:
[-23, -2, 3, 7, 9, 45]
三、选择排序
1. 原理
每次从数组中找到第 n 小的元素与数组中第 n 个元素交换位置,最后一次交换完成后,即可得到一个排好序的数组。
2. 思路
每轮比较假定当前索引数为最小数,和后面的数进行比较:
如果发现有比当前数更小的数,就重新确定最小数,并得到索引
当遍历到数组的最后就得到本轮比较最小数的索引
交换数据
一共需要比较数组长度减 1 轮
思路图示:
3. 代码实现
代码如下:
public class SelectSort {
public static void main(String[] args) {
int[] arr = {9, 3, 45, -2, 7, -23};
selectSort(arr);
}
/**
* 选择排序
* @param arr 待排序的数组
*/
public static void selectSort(int[] arr) {
//一共交换 arr.length-1 轮
for (int i = 0; i < arr.length - 1; i++) {
//假设当前索引元素是最小值
int index = i;
//最小元素依次与数组中后续元素比较
for (int j = i + 1; j < arr.length; j++) {
//如果当前索引元素值比最小值小,则更新最小值索引
if (arr[index] > arr[j]) {
index = j;
}
}
//如果最小值索引不是最初索引才进行数据交换
if (index != i) {
//交换数据
int temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}
//打印每一轮交换后结果
System.out.println("第" + (i + 1) + "轮交换后:");
System.out.println(Arrays.toString(arr));
}
}
}
测试结果:
第1轮交换后:
[-23, 3, 45, -2, 7, 9]
第2轮交换后:
[-23, -2, 45, 3, 7, 9]
第3轮交换后:
[-23, -2, 3, 45, 7, 9]
第4轮交换后:
[-23, -2, 3, 7, 45, 9]
第5轮交换后:
[-23, -2, 3, 7, 9, 45]
四、插入排序
1. 原理
将 n 个待排序的元素看成一个有序表和一个无序表,开始时,有序表中只包含一个元素,无序表中包含 n-1 个元素,排序过程中每次从无序表中取出第一个元素,将其插入到有序表中适当的位置,使之成为新的有序表。
2. 思路
保证数组左侧有序
每次取出数组右侧第一个元素
将其依次与已经有序的左侧元素(从右往左)比较:
如果到达数组最左侧或者当前元素大于或等于有序表中某一个元素,则将其插入
否则将被比较的元素后移
从无序表第 2 个数据开始,一共插入数组长度 -1 轮
思路图示:
3. 代码实现
代码如下:
public class InsertSort {
public static void main(String[] args) {
int[] arr = {9, 3, 45, -2, 7, -23};
insertSort(arr);
}
/**
* 插入排序
*
* @param arr 待排序的数组
*/
public static void insertSort(int[] arr) {
//从无序表第 2 个数据开始,一共插入 arr.length-1 轮
for (int i = 1; i < arr.length; i++) {
//将当前要插入的数据取出
int insertVal = arr[i];
//从有序表最右侧元素开始遍历
int insertIndex = i - 1;
//当 insertIndex>=0 (还没到有序表最左侧元素)或者 即将插入数据小于有序表当前元素时
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
//将有序表当前元素后移
arr[insertIndex + 1] = arr[insertIndex];
//有序表元素索引减 1
insertIndex--;
}
//循环结束,即找到元素插入位置
// 循环中执行了 insertIndex--,所以此处 insertIndex 为插入位置的前一个索引
//将元素插入有序表
arr[insertIndex + 1] = insertVal;
//打印每一轮插入后结果
System.out.println("第" + i + "轮插入后:");
System.out.println(Arrays.toString(arr));
}
}
}
测试结果:
第1轮插入后:
[3, 9, 45, -2, 7, -23]
第2轮插入后:
[3, 9, 45, -2, 7, -23]
第3轮插入后:
[-2, 3, 9, 45, 7, -23]
第4轮插入后:
[-2, 3, 7, 9, 45, -23]
第5轮插入后:
[-23, -2, 3, 7, 9, 45]
五、希尔排序
1. 插入排序存在的问题
在插入排序中,假定最小数据在数组最后,如:
[-2, 3, 7, 9, 45, -23]
则当需要插入的数为 -23 时,其插入的过程为:
[-2, 3, 7, 9, 45, 45],
[-2, 3, 7, 9, 9, 45],
[-2, 3, 7, 7, 9, 45],
[-2, 3, 3, 7, 9, 45],
[-2, -2, 3, 7, 9, 45],
[-23, -2, 3, 7, 9, 45]
即当需要插入的数据是较小的数时,后移的次数会很多,对效率有影响
2. 原理
希尔排序是插入排序的优化版本:
将 n 个元素按一定增量分组,对每组使用插入排序,随着增量逐渐减小,每组包含的元素越来越多,当增量为 1 时,即可得到排好序的 n 个元素。
3. 思路
第一次增量取数组长度除以 2
后续每次增量取上一次增量除以 2(一直取到增量为 1 )
每次取增量后,将对应的每组元素排好序
当增量为 1 时,为最后一轮排序
思路图示:
4. 代码实现
可以使用交换法(每次交换元素位置),也可以使用移位法(参考插入排序),交换法虽然利用了希尔排序的思想,但是效率较低,一般采用移位法。
代码1 如下(交换法):
public class ShellSort1 {
public static void main(String[] args) {
int[] arr = {9, 3, 45, -2, 7, -23};
shellSort(arr);
}
/**
* 希尔排序(交换法)
* @param arr 待排序的数组
*/
public static void shellSort(int[] arr) {
//初始化计数器
int count = 0;
//每一轮的增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//遍历各组中的所有元素(共 gap 组),每组 arr.length/gap(向上取整)个元素
for (int i = gap; i < arr.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
//如果当前元素大于加上增量后的那个元素,则交换两个元素位置
if (arr[j] > arr[j + gap]) {
int temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
//打印每一轮排序后结果
System.out.println("第" + (++count) + "轮排序后:");
System.out.println(Arrays.toString(arr));
}
}
}
代码2 如下(移位法):
public class ShellSort2 {
public static void main(String[] args) {
int[] arr = {9, 3, 45, -2, 7, -23};
shellSort(arr);
}
/**
* 希尔排序(移位法)
* @param arr 待排序的数组
*/
public static void shellSort(int[] arr) {
//初始化计数器
int count = 0;
//每一轮的增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//遍历各组中的所有元素(共 gap 组),每组 arr.length/gap(向上取整)个元素
//按照插入排序的思想对每组进行排序
for (int i = gap; i < arr.length; i++) {
//保存当前插入元素
int insertVal = arr[i];
//初始化插入元素位置的索引
int insertIndex = i;
//如果当前元素比前一个元素小,才进入循环(需要往前插入)
if (insertVal < arr[insertIndex - gap]) {
//执行插入排序的逻辑
while (insertIndex - gap >= 0 && insertVal < arr[insertIndex - gap]) {
arr[insertIndex] = arr[insertIndex - gap];
insertIndex -= gap;
}
}
arr[insertIndex] = insertVal;
}
//打印每一轮排序后结果
System.out.println("第" + (++count) + "轮排序后:");
System.out.println(Arrays.toString(arr));
}
}
}
测试结果:
第1轮插入后:
[-2, 3, -23, 9, 7, 45]
第2轮插入后:
[-23, -2, 3, 7, 9, 45]
六、快速排序
1. 原理
快速排序是冒泡排序的优化版本:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另外一部分的所有数据都要小,然后继续按照此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,直到整个序列变成有序。
2. 思路
假定每趟快速排序的基准元素为第一个元素:
在所有元素最左侧与最右侧分别定义一个索引
先从右侧扫描数组元素:
如果扫描到一个小于基准元素的数据,则将其放到左侧索引的位置
右侧索引停止扫描,左侧索引开始扫描
如果扫描到一个大于基准元素的数据,则将其放到右侧索引的位置
左侧索引停止扫描,右侧索引开始扫描
直到左侧索引等于右侧索引,则将基准元素放于该位置
思路图示:
3. 代码实现
代码如下:
public class QuickSort {
public static void main(String[] args) {
int[] arr = {9, 3, 45, -2, 7, -23};
quickSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 基数排序方法的重载,方便传参
*
* @param arr 待排序的数组
*/
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
/**
* 快速排序
*
* @param arr 待排序的数组
* @param left 待排序数组最左侧索引
* @param right 待排序数组最右侧索引
*/
public static void quickSort(int[] arr, int left, int right) {
//当左侧索引大于右侧索引时,结束递归
if (left > right) {
return;
}
//取出左右侧索引,是为了方便后续递归
int l = left;
int r = right;
//取出基准元素(每次取第一个元素作为基准元素)
int pivot = arr[left];
//只要左侧索引小于右侧索引
//两侧索引分别从两侧扫描数组(一侧扫描时,另一侧停止)
while (l < r) {
//右侧索引开始扫描
//只要左侧索引小于右侧索引(右侧索引不断减小,可能会减小到与左侧索引相等)
//且右侧索引元素大于基准元素(将大于基准元素的数据,放基准元素右侧,此时已经在右侧)
//则一直扫描
while (l < r && arr[r] > pivot) {
//右侧索引左移
r--;
}
//退出上一步循环,说明右侧索引找到了小于基准元素的数据
//此时右侧索引停止扫描
//如果左侧索引仍然小于右侧索引
if (l < r) {
//则将右侧索引当前值赋给左侧索引
//且左侧索引右移
arr[l++] = arr[r];
}
//左侧索引开始扫描
//只要左侧索引小于右侧索引(左侧索引不断增大,可能会增大到与右侧索引相等)
//且左侧索引元素小于基准元素(将小于基准元素的数据,放基准元素左侧,此时已经在左侧)
//则一直扫描
while (l < r && arr[l] < pivot) {
//左侧索引右移
l++;
}
//退出上一步循环,说明左侧索引找到了大于基准元素的数据
//此时左侧索引停止扫描
//如果左侧索引仍然小于右侧索引
if (l < r) {
//则将左侧索引当前值赋给右侧索引
//且右侧索引左移
arr[r--] = arr[l];
}
}
//退出整个扫描循环时,l==r
//此时将基准元素放入该位置
//即可保证基准元素左侧全为小于它的数
//基准元素右侧全为大于它的数
arr[l] = pivot;
//继续对基准元素左侧元素与基准元素右侧元素分别进行快速排序
quickSort(arr, left, l - 1);
quickSort(arr, l + 1, right);
}
}
测试结果:
[-23, -2, 3, 7, 9, 45]
七、归并排序
1. 原理
归并排序是利用分治思想的一种排序方法:
先分:
将所有元素递归拆分至无法拆分的子序列
再治:
每轮将子序列组合并排序
最后一次组合即可得到排好序的序列
2. 思路
递归将数组拆分成无法拆分的子序列
在拆分完后开始合并,在合并的过程中:
保证每一轮合并后的子序列为排好序的序列
在最后一轮合并后,即可得到排好序的数组
思路图示:
最后一轮合并过程图示:
3. 代码实现
代码如下:
public class MergeSort {
public static void main(String[] args) {
int[] arr = new int[]{9, 3, 45, -2, 7, -23};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 基数排序方法的重载,方便传参
*
* @param arr 待排序的数组
*/
public static void mergeSort(int[] arr) {
mergeSort(arr, 0, arr.length - 1);
}
/**
* 归并排序
*
* @param arr 待排序的数组
* @param left 待排序的序列左侧索引
* @param right 待排序的序列右侧索引
*/
public static void mergeSort(int[] arr, int left, int right) {
//只要左侧索引小于右侧索引,就一直递归拆分数组
if (left < right) {
//数组中间位置索引
int mid = (left + right) / 2;
//递归拆分左右两侧元素,并合并
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
/**
* 合并拆分后的数组元素
*
* @param arr 待排序的数组
* @param left 待排序的序列左侧索引
* @param mid 数组中值索引
* @param right 待排序的序列右侧索引
*/
public static void merge(int[] arr, int left, int mid, int right) {
//初始化数组 temp ,辅助数组
//数组长度为每次需要合并的序列长度
int[] temp = new int[right - left + 1];
//初始化 i ,数组左侧有序序列初始索引
int i = left;
//初始化 j ,数组右侧有序序列初始索引
int j = mid + 1;
//初始化 k ,辅助数组初始索引
int k = 0;
//当 i>mid 或者 j>right 则退出循环
//说明左侧或者右侧序列已经全部放进辅助数组中
while (i <= mid && j <= right) {
//如果左侧索引当前元素小于右侧索引当前元素
if (arr[i] <= arr[j]) {
//则将左侧索引当前元素放进辅助数组中
//且 辅助数组索引 及 左侧索引 分别加 1
temp[k++] = arr[i++];
} else {
//反之,则将右侧索引当前元素放进辅助数组中
//且 辅助数组索引 及 右侧索引 分别加 1
temp[k++] = arr[j++];
}
}
//如果此时 i 仍然小于或者等于 mid
//说明左侧序列还没有全部放入辅助数组(剩下的所有元素都比右侧序列所有元素大)
//则将左侧序列剩余元素依次放入辅助数组
while (i <= mid) {
temp[k++] = arr[i++];
}
//如果此时 j 仍然小于或者等于 right
//说明右侧序列还没有全部放入辅助数组(剩下的所有元素都比左侧序列所有元素大)
//则将右侧序列剩余元素依次放入辅助数组
while (j <= right) {
temp[k++] = arr[j++];
}
//将辅助数组中的有序序列复制进初始数组中
for (k = 0; k < temp.length; k++) {
arr[k + left] = temp[k];
}
}
}
测试结果:
[-23, -2, 3, 7, 9, 45]
八、基数排序
1. 原理
基数排序是桶排序的扩展:
将所有待排序的数据统一为同样的数位长度,数位较短的数前面补 0 ,然后从最低位开始,依次进行一次排序。当从最低位排序到最高位排序完成以后,即可得到排好序的序列。
2. 思路
每一轮排序的过程:
先定义一个二维数组,作为桶,用来存放每次取出的数据:
二维数组中的第几个一维数组表示桶的值为几
将取出的数据放到对应的桶
再定义一个一维数组,用来存放每个桶中存放的数据个数
将桶中的数据依次取出,放回原数组
在数组中最大数据的位数轮过后,即可得到排好序的数组
思路图示:
3. 代码实现
代码如下:
public class RadixSort {
public static void main(String[] args) {
int[] arr = {9, 357, 45, 285, 787, 23, 429, 1436};
radixSort(arr);
}
/**
* 基数排序
*
* @param arr 待排序的数组
*/
public static void radixSort(int[] arr) {
//先假定数组中的最大值为第一个元素
int max = arr[0];
//遍历数组
for (int value : arr) {
//如果遍历当前元素大于最大值,则更新最大值
if (value > max) {
max = value;
}
}
//获取数组中最大值的位数
int maxLength = (max + "").length();
//定义十个桶,用来存放对应的数据
//为防止数据溢出,桶的大小为 arr.length
// 可能数组中的元素在某一位上都是同一个数字
int[][] bucket = new int[10][arr.length];
//用来保存每个桶中数据个数的一维数组
int[] bucketElementCount = new int[10];
//一共要进行 maxLength 轮排序
//每轮根据数组中数据某一位的值排序
// 第一轮为个位,第二轮为十位,第三轮为百位……
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
//将数组中的数据放到对应的桶中
for (int value : arr) {
//取出数组中对应位的值
int digitOfElement = value / n % 10;
//将数据放到对应的桶
bucket[digitOfElement][bucketElementCount[digitOfElement]] = value;
//将该桶的数据个数加 1
bucketElementCount[digitOfElement]++;
}
//初始数组索引
int index = 0;
//将桶中的数据依次取出
for (int k = 0; k < bucketElementCount.length; k++) {
//如果桶中有数据
if (bucketElementCount[k] != 0) {
//则依次取出桶中所有数据
for (int l = 0; l < bucketElementCount[k]; l++) {
//将取出的数据放回初始数组
arr[index++] = bucket[k][l];
}
}
//将桶中元素个数置零
//后续每轮要重新放数据
bucketElementCount[k] = 0;
}
//打印每一轮结果
System.out.println("第" + (i + 1) + "轮排序的结果为:" + Arrays.toString(arr));
}
}
}
测试结果:
第1轮排序的结果为:[23, 45, 285, 1436, 357, 787, 9, 429]
第2轮排序的结果为:[9, 23, 429, 1436, 45, 357, 285, 787]
第3轮排序的结果为:[9, 23, 45, 285, 357, 429, 1436, 787]
第4轮排序的结果为:[9, 23, 45, 285, 357, 429, 787, 1436]
九、堆排序
1. 堆的概念
堆是具有以下性质的完全二叉树:每个节点的值都大于等于左右孩子的值(大根堆)或小于等于左右孩子的值(小根堆),对节点左孩子和右孩子值的大小关系没有要求。
概念图示:
2. 堆排序思路
1. 将待排序序列构成一个大根堆(升序用大根堆,降序用小根堆)
2. 此时,整个序列的最大值就是堆顶的根节点
3. 将其与末尾元素交换,此时末尾就是最大值
4. 然后将剩余 n-1 个元素重新构建成一个堆,这样就会得到 n 个元素的次小值
5. 反复执行,即可得到一个有序序列
3. 堆排序图解
- 构造初始堆,将给定的无序序列构建成一个大根堆
- 初始数组:[ 9, 3, 45, -2, 7, -23 ],构建堆如下:
- 此时从最后一个非叶子节点开始(叶子节点不用调整,第一个非叶子节点索引 arr.length-1 / 2 - 1 = 6 / 2 - 1= 2,即元素为 45 的叶子节点 ),从左至右,从下至上调整
- 找到第二个非叶子节点 3,其孩子节点 7 > -3,且 3 < 7,所以将两个节点交换
- 继续找到下一个非叶子节点 9,其孩子节点 45 > 7,且 9 < 45,所以交换两个节点,交换过后就实现了将无序序列构建成大根堆
- 将堆顶元素与末尾元素交换,使末尾元素最大,然后将堆调整为大根堆,再将堆顶元素与末尾元素交换,得到第二大元素,反复交换、调整,得到有序序列
- 将堆顶元素 45 与 末尾元素 -23 交换,得到最大元素 45
- 重新调整为大根堆
- 再将堆顶元素 9 与 末尾元素 3 交换,得到第二大元素 9
- 一直重复上面的步骤,直到得到有序序列
- 将堆顶元素 45 与 末尾元素 -23 交换,得到最大元素 45
4. 代码实现
代码如下:
public class HeapSort {
public static void main(String[] args) {
//初始化数组
int[] arr = new int[]{9, 3, 45, -2, 7, -23};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 堆排序
*
* @param arr 待排序的数组
*/
public static void heapSort(int[] arr) {
//将无序序列构建成大根堆
//从最后一个非叶子节点开始依次往前调整
for (int i = arr.length / 2 - 1; i >= 0; i--) {
//调整节点位置
adjustHeap(arr, i, arr.length);
}
//每次交换后,需要排序的元素减 1
for (int i = arr.length - 1; i >= 0; i--) {
//将堆顶元素与末尾元素交换
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//重新调整为大根堆
adjustHeap(arr, 0, i);
}
}
/**
* 将以 arr[index] 作为根节点的二叉树调整为大根堆
* 如:
* 初始数组:arr: [ 9, 3, 45, -2, 7, -23 ]
* 第一次传入的 index = arr.length/2-1 = 6/2-1 = 2
* 调整得到:arr: [ 9, 3, 45, -2, 7, -23 ]
* 再次调整则传入 index = arr.length/2-2 = 6/2-2 = 1
* 调整得到:arr: [ 9, 7, 45, -2, 3, -23 ]
*
* @param arr 待调整为大根堆的数组
* @param index 非叶子节点在数组中的索引
* @param length 对多少个元素进行调整
* 开始排序后,每次交换,length - 1
*/
public static void adjustHeap(int[] arr, int index, int length) {
//保存当前索引元素
int temp = arr[index];
// index*2+1 为当前节点左子节点
for (int i = index * 2 + 1; i < length; i = i * 2 + 1) {
//如果右子节点存在
//且左子节点值小于右子节点值
if (i + 1 < length && arr[i] < arr[i + 1]) {
//则将 i 指向右子节点
i++;
}
//如果 i 指向的子节点值大于父节点的值
if (arr[i] > temp) {
//则将子节点的值赋给父节点
arr[index] = arr[i];
//将 index 指向被交换的元素位置
//继续向下调整(可能上面节点调整后会打乱其子树)
index = i;
} else {
//如果不需交换,则直接结束循环
//说明该节点及其子树已经调整好了
break;
}
}
//再将 temp 的值放到调整后的位置
arr[index] = temp;
}
}
测试结果:
[-23, -2, 3, 7, 9, 45]