2025-07-31-排序
本文章默认是从小到大排序
排序的概念
稳定性: 排完序后,位置会不会乱。比如
这个数组(a,b 仅仅用来标记) 稳定的排完序后是
不稳定的排完序后可能是
内部排序:在内存上的排序,适用于数据比较少的情况
外部排序:在外存(硬盘)上的排序,适用于数据比较多的情况
排序的类型
排序的实现
直接插入排序
思想:
- 从第 2 个元素开始,获取到这个元素的值,并用临时变量存储
- 如果这个元素比前一个元素大,那么就不需要覆盖,直接跳出这个循环
- 如果比前一个元素小,那么就要前一个元素的内容就要覆盖后一个元素的内容,然后继续判断,直到遍历完或者条件不满足
- 在 2/3 这两步循环完后,就可以把之前临时存储的元素放入最后一个覆盖元素的原位置上
- 1/2/3/4 是一次循环,按照这样的步骤,遍历完整个数组后即可完成循环
性质:
- 时间复杂度:
- 空间复杂度:
- 稳定性:稳定
当前动作:等待输入...
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int j = i - 1;
int tmp = array[i];
for( ; j >= 0; j--) {
// 不要写为 >= 不然就是不稳定的
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
break;
}
}
// j + 1 是最后一个元素的下标
array[j + 1] = tmp;
}
}希尔排序
思想:
- 它是分组排序,分为
array.length / gap组,通过让每组元素的个数变小从而让排序次数变小- 内部排序(shell() 执行的过程)的思路与直接插入排序基本相同,只是从相隔
1之间排序变为相隔gap- 外部排序只需要保证
gap最后为 1 的时候在执行一次内部排序即可性质:
- 时间复杂度:
由于
gap不确定什么时候最优解,所以目前来说,这个时间复杂度还没有确定的值- 空间复杂度:
- 稳定性:不稳定
当前动作:等待输入...
public static void shellSort(int[] array) {
int gap = array.length / 2; // gap 变化逻辑可以自己定义,但是最终必须为 1
while (gap >= 1) {
shell(array, gap);
gap /= 2;
}
}
private static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int j = i - gap; // 同一个组的前一个元素的下标
int tmp = array[i];
for( ; j >= 0; j -= gap) {
// 希尔排序是不稳定的
if (array[j] > tmp) {
array[j + gap] = array[j];
} else {
break;
}
}
// j + 1 是最后一个元素的下标
array[j + gap] = tmp;
}
}选择排序
由于时间复杂度/稳定性都没有优势,所以通常不使用这个排序
思想:
- 先遍历一次数组获取最小值的下标
- 接着把第一个元素与最小值交换,把第一个元素变为最小值
- 然后遍历第二个,第三个元素,直到遍历完数组为止
性质:
- 时间复杂度:
- 空间复杂度:
- 稳定性:不稳定
当前动作:等待输入...
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i + 1; j < array.length; j++) {
if (array[minIndex] > array[j]) {
minIndex = j;
}
}
// minIndex 存储最小值的下标
swap(array, i, minIndex);
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}堆排序
详情见:通过 优先级队列 这篇文章,里面有堆的 创建+排序
性质:
- 时间复杂度:
- 空间复杂度:
- 稳定性:不稳定
当前动作:等待输入...
public static void heapSort(int[] array) {
// 建大根堆 O(N)
createHeap(array);
// 排序 O(N * logN)
int end = array.length - 1;
while (end > 0) {
swap(array, 0, end);
siftDown(array, 0, end);
end--;
}
}
private static void createHeap(int[] array) {
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
siftDown(array, parent, array.length);
}
}
private static void siftDown(int[] array, int parent, int useSize) {
int child = parent * 2 + 1;
while (child < useSize) {
if (child + 1 < useSize && array[child] < array[child + 1]) {
child++;
}
// 此时 child 指向的下标是孩子节点的最大值
if (array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = parent * 2 + 1;
} else {
// 剩下的都是大根堆
break;
}
}
}冒泡排序
思路:
- 如果第一个数据比第二个数据大,那么就交换
- 一直执行上诉步骤,直到遍历完一次,此时最后一个元素一定是最大值
- 接着排除最后一个,反复循环1/2两个步骤,即可排完序
性质:
- 时间复杂度:
- 空间复杂度:
- 稳定性:稳定
当前动作:等待输入...
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean isSwap = false;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] >= array[j + 1]) {
swap(array, j, j + 1);
isSwap = true;
}
}
if (!isSwap) {
break;
}
}
}快速排序
思路:
- 找到一个中间值,通过
partition这个方法后,保证这个中间值的左边比这个值小,右边比这个值大partition返回这个中间值的下标pivot,通过递归,排序完左边与右边的那一部分parition由于类型不同,思路也有所不同,所以在具体的内部在解析
partition是在具体的方法内部实现 完整代码是 框架内容+具体类型的partition实现 性质:
- 时间复杂度:最好:
,最坏: - 空间复杂度:最好:
,最坏: - 稳定性:不稳定
思考
为什么是先从右边开始,而不是从左边开始?
排序的结果从小到大,从左边开始,最后一次就会获取到较大值,与
tmp交换,导致较大值换到左边获取较大值/较小值的下标时候,能不能省略等号? 为什么?
比如
left < right && array[right] >= tmp能不能变为left < right && array[right] > tmp不取等号会有死循环这个可能,比如 [5,5,5] 这个数组就有这个问题
// 框架的内容
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
private static void quick(int[] array, int start, int end) {
// start >= end 就结束了
if (start >= end) {
return;
}
// 排序,然后返回中间值的下标
int pivot = partition(array, start, end);
// 遍历左边与右边
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}快速排序类型实现
Hoare法
思路:
- 获取左边第一个元素
tmp作为基准,并记录下此时的下标i- 先从右开始,获取到比
tmp小的下标right- 然后从左边开始,获取到比
tmp大的下标left- 使用
swap()这个方法交换right与left所对应的值- 直到循环结束,最后 使用
swap()交换i与left所对应的值 这个下标并返回left
当前动作:等待输入...
private static int partition(int[] array, int left, int right) {
int tmp = array[left];
int i = left; // 用来存储 left
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
while (left < right && array[left] <= tmp) {
left++;
}
swap(array, left, right);
}
// 与开始的交换
swap(array, i, left);
return left;
}挖坑法
思路:
- 获取左边第一个元素
tmp作为基准,用tmp存储- 先从右开始,获取到比
tmp小的下标right,循环结束后,用right这个值覆盖掉left对应的值- 然后从左边开始,获取到比
tmp大的下标left,循环结束后,用left这个值覆盖掉right对应的值- 直到循环结束,最后 将
tmp赋值给left这个下标并返回left
当前动作:等待输入...
// 挖坑法
private static int partition(int[] array, int left, int right) {
int tmp = array[left];
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
// 右边比较小的覆盖掉左边比较大的
array[left] = array[right];
while (left < right && array[left] <= tmp) {
left++;
}
// 左边比较大的覆盖掉右边比较小的
array[right] = array[left];
}
array[left] = tmp;
return left;
}前后指针法
这个仅仅只需要了解即可
当前动作:等待输入...
private static int partition(int[] array, int left, int right) {
int prev = left ;
int cur = left + 1;
while (cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}快速排序优化
背景:
由于快速排序主要的问题在于 在
个数据是有序 的情况下, 树的高度会趋于
(遍历次数),而每次遍历的个数是 , 导致时间复杂度会区域 优化目标: 把 树的高度尽可能降低 来实现排序的优化
- 获取三数的中位数
- 在递归到小区间,使用插入排序经行优化
// 优化后的快速排序
private static void quick(int[] array, int start, int end) {
// start >= end 就结束了
if (start >= end) {
return;
}
// 低于一定的数值后 剩下的数据基本有序,适用于插入排序
if (end - start <= 15) {
insertSort(array, start, end);
}
// 获取三数中中位数的下标并且交换
int midIndex = getMidIndex(array, start, end);
swap(array, start, midIndex);
// 排序,然后返回中间值的下标
int pivot = partition(array, start, end);
// 遍历左边与右边
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}非递归快排
用
Stack<Integer>存储,用两个元素分别表示头和尾,这样就可以知道快排的范围了
public static void quickSortNor(int[] array) {
int start = 0;
int end = array.length - 1;
int pivot = partition2(array, start, end);
Stack<Integer> stack = new Stack<>();
if (pivot - start > 1) {
// 左边有两个及其以上
stack.push(pivot - 1);
stack.push(start);
}
if (end - pivot > 1) {
stack.push(end);
stack.push(pivot + 1);
}
while (!stack.isEmpty()) {
start = stack.pop();
end = stack.pop();
pivot = partition2(array, start, end);
if (pivot - start > 1) {
// 左边有两个及其以上
stack.push(pivot - 1);
stack.push(start);
}
if (end - pivot > 1) {
stack.push(end);
stack.push(pivot + 1);
}
}
}归并排序
思想:分而治之,分为“归”和“并”这两个过程
- 一直对半分,直到这个数组只剩下2个元素为止(归)
- 2个元素可以看为两个有序数组,将两个有序数组合并为一个有序数组,并覆盖掉原数组(并)
- 反复执行 1/2 这两个步骤,直到全部都遍历完
性质:
- 时间复杂度:
- 空间复杂度:
- 稳定性:稳定
当前动作:等待输入...
递归方式实现归并排序
public static void mergeSort(int[] array) {
mergeSort(array, 0, array.length - 1);
}
private static void merge(int[] array, int left, int mid, int right) {
int[] tmpArray = new int[right - left + 1];
int index = 0;
int s1 = left, e1 = mid, s2 = mid + 1, e2 = right;
while (s1 <= e1 && s2 <= e2) {
// 这里使用 <= 是稳定的,没有等号就是不稳定的
if (array[s1] <= array[s2]) {
tmpArray[index++] = array[s1++];
} else {
tmpArray[index++] = array[s2++];
}
}
while (s1 <= e1) {
// 第一个数组还有元素
tmpArray[index++] = array[s1++];
}
while (s2 <= e2) {
// 第二个数组还有元素
tmpArray[index++] = array[s2++];
}
// 用临时数组的数据覆盖掉原数组的数据
for (int i = 0; i < tmpArray.length; i++) {
array[i + left] = tmpArray[i];
}
}
private static void merge(int[] array, int left, int right) {
int mid = (right + left) / 2;
merge(array, left, mid, right);
}非递归方式实现归并排序
public static void mergeSortNor(int[] array) {
int gap = 1; // 用来表示当前有序数组的长度,从小到大归并
// 不需要等号
while (gap < array.length) {
for (int i = 0; i < array.length; i += gap * 2) {
int left = i;
// 防止最后几个,跳过头导致 数组越界
int mid = left + gap - 1;
if (mid >= array.length) {
// mid = array.length - 1;
break; // 如果左半部分已经触底,说明右半部分元素为 0,而左半部分是不需要排序的,因为已经是有序的了
}
int right = left + gap * 2 - 1;
if (right >= array.length) {
right = array.length - 1;
}
// 不能少 mid 这个参数
merge(array, left, mid, right);
}
gap *= 2;
}
}疑问
为什么
gap < array.length不需要等号gap表示有序数组的长度,等于length时候,有序数组的长度与原数组一样,那么就根本不需要排序了为什么是使用
merge(array, left, mid, right)而不是使用merge(array, left, right)非递归的情况下,
right由于会数组越界,所以在越界的时候会修正一下,此时如果还按照mid = (right + left) / 2来计算,那么mid就不是两个有序数组的中间了,而是整个数组的中间了
计数排序
适用于数据比较集中的数字集合
思路:
- 计算最大值 max 与最小值 min,依据 max 与 min 创建计数数组
- 遍历数组,通过元素值获取到相关值在计数数组的下标,然后在计数数组内加一
- 最后依据计数数组的下标与内部的计数,把相关信息赋值给原数组即可
性质:
- 时间复杂度:
- 空间复杂度:
- 稳定性:稳定
当前动作:等待输入...
public static void countSort(int[] array) {
// 先获取到最大最小值
int max = array[0], min = array[0];
for (int i = 1; i < array.length; i++) {
if (max < array[i]) {
max = array[i];
} else if (min > array[i]) {
min = array[i];
}
}
int[] countArray = new int[max - min + 1];
// 计数
for (int i = 0; i < array.length; i++) {
int index = array[i] - min;
countArray[index]++;
}
// 赋值
int index = 0; // array 下标
for (int i = 0; i < countArray.length; i++) {
while (countArray[i] != 0) {
countArray[i]--;
array[index++] = i + min;
}
}
}基数排序
适用于纯数字的情况
思路:
- 创建10个队列(从 0 下标开始)
- 从个位数开始,通过当前位数的数字获取到队列,然后添加进去即可
- 一轮添加完毕后,从 0 下标的队列开始,依次出来,返回原来数组中
- 反复循环 1/2 这两个步骤,直到添加所有的位数都遍历过
- 最后一次出来后,数组就有序了
当前动作:等待输入...
桶排序
适用于纯数字的情况
思路:
- 分为若干桶,每桶存储一定范围内的数字
- 然后每桶内部经行排序
- 最后利用有序数组合并这个思想,把各桶之间的数据进行合并
当前动作:等待输入...