🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈
欢迎来到排序算法的学习,恭喜你!本期内容主要介绍排序算法,一起来探索吧~
(ps:真的一直都想写有关排序的文章,奈何每天尊嘟好忙,终于开写啦!)
文章目录
1. 排序的基本概念
1.1 排序预备知识
排序:排序的目的是使一串记录,将一组“无序”的记录序列调整为"有序"的记录序列,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
1.2 内部排序与外部排序
内部排序:数据元素全部放在内存中的排序,内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
外部排序:数据元素数量非常大,不能同时放在内存中,整个过程不可能在内存中完成。
1.3 排序的稳定性
稳定性:假定在待排序的记录序列中,存在两个或者两个以上的具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称为稳定,否则为不稳定
1.4 排序的分类
插入类排序:将待排序的值插入到前面已经有序的序列中
选择类排序:每次排序选出序列的最大值/最小值,放到序列的最后面
交换排序:两两比较待排序的序列,并交换不满足序列的那对数
以上为经典排序,本文主要介绍常见的 7 种排序算法,我们需要掌握~ 一起来正式进入下面的学习吧!
2. 插入类排序
2.1 直接插入排序
【基本思想】 将记录的值,插入到已经排序好的有序序列中,直到所有记录的值全部插入,则该过程完成。
【基本思路】 仅有一个数据时,则认为该数据为已经排好序的,则我们可以这样思考:
- 第1个元素已排好序,从第2个元素开始,下标为i,取出该元素放在变量 tmp 中,从已排好序序列中从后往前使用 j 遍历比较
- 如果下标 j 的值大于 tmp,则 j+1 的值替换为 j 下标的值,继续遍历
- 如果下标 j 的值小于 tmp,则 break,跳出遍历
- 最后 j+1 的值替换成临时变量 tmp 存储的值
- 此时前两个元素已经是有序的了,再用 i 继续遍历(详细解释可以看示意图~)
有没有点像打扑克牌时候呢?拿到一张牌就在以往排好序的牌中插入~
【示意图】
【具体代码】
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-01
* Time: 23:58
*/
public class InsertSort {
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
insertSort(array);
System.out.println(Arrays.toString(array));
}
public static void insertSort(int[] array) {
for(int i = 1; i < array.length; i++) {
int tmp = array[i];
int j = i-1;
for(; j >= 0; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
//array[j+1] = tmp;
break;
}
}
array[j+1] = tmp;
}
}
}
【特性总结】
- 时间复杂度:O(n²),当数据本身就有序的时候,时间复杂度为O(n)
- 空间复杂度:O(1)
- 稳定性:稳定
- 适用场景:当数据基本趋于有序时,建议使用直接插入排序
2.2 希尔排序
【基本思想】希尔排序又称为缩小增量法,其基本思想是先选定一个整数 gap,将待排序的数据分为多个组,每组距离 gap,对每一组进行排序,每一组两个元素,接着 gap/2 ,缩小每组的距离,当 gap = 1 时,所有数据就排好序了
简单理解就是按照 gap 分组,组内进行插入排序,当 gap = 1 时,则为直接插入排序
【基本思路】与直接插入排序思路一致,希尔排序是直接插入排序的优化,先用 gap 分组,让数组预有序,当 gap 为 1 时,数组也基本有序了,此时就是直接插入排序~
【示意图】
【具体代码】
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-02
* Time: 10:28
*/
public class ShellSort {
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
int gap = array.length;
while(gap > 1) {
shellSort(array,gap);
gap /= 2;
}
shellSort(array,1);
System.out.println(Arrays.toString(array));
}
public static void shellSort(int[] array,int gap) {
for(int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i-gap;
for(; j >= 0; j-=gap) {
if(array[j] > tmp) {
array[j+gap] = array[j];
}else {
//array[j+1] = tmp;
break;
}
}
array[j+gap] = tmp;
}
}
【特性总结】
- 时间复杂度:O(n^1.3) - O(n^1.5) ,当数据本身就有序的时候,时间复杂度为O(n),且希尔排序的时间复杂度取决于增量值 gap 的选取,即时间复杂度并不是一个定值
- 空间复杂度:O(1)
- 稳定性:不稳定
- 希尔排序是直接插入排序的优化,gap >1 的排序是预排序,使数据趋于有序,gap=1 时则是直接插入排序
3. 选择类排序
3.1 直接选择排序
【基本思想】每次从待排序的序列中选出最小/最大元素,放在序列的起始位置,直到全部待排序的数据排完
【基本思路】定义一个 minIndex 下标用来存储最小值的下标,第1次 minIndex = 0 下标,遍历整个待排序序列,当有更小数据时,更新 minIndex 下标,再交换起始位置 i 与 minIndex 下标值,这样就找到第1小的元素,接着再遍历,minIndex = 1,遍历后面的待排序元素,找到第2小数据,并进行交换,以此类推…
【示意图】
【具体代码】
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-02
* Time: 10:53
*/
public class SelectSort {
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
selectSort(array);
System.out.println(Arrays.toString(array));
}
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[j] < array[minIndex]) {
minIndex = j;
}
}
swap(array,i,minIndex);
}
}
public static void swap(int[] array,int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
【特性总结】
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:不稳定
3.2 堆排序
【基本思想】堆排序是指利用堆这种数据结构所设计的一种算法,它是选择排序的一种,通过堆进行选择数据,排升序是建大堆,排降序建立小堆
大根堆:每个节点的值都 >= 其子节点的值,用于升序排列
小根堆:每个节点的值都 <= 其子节点的值,用于降序排列
【基本思路】堆排序就是先将我们的数组进行一次建堆,循环交换堆顶元素(最大值)与最后一个元素,即交换数组头和尾的元素,然后重新建堆,再进行交换堆顶元素与最后一个元素
【示意图】
【具体代码】
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-22
* Time: 23:52
*/
public class HeapSort {
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length - 1;
while (end > 0) {
swap(array, 0, end);
shiftDown(array, 0, end);
end--;
}
}
public static void createBigHeap(int[] array) {
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(array, parent, array.length);
}
}
//建大堆
private static void shiftDown(int[] array, int parent, int len) {
int child = 2 * parent + 1;
while (child < len) {
if (child + 1 < len && array[child] < array[child + 1]) {
child++;
}
if (array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
public static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
heapSort(array);
System.out.println(Arrays.toString(array));
}
}
【特性总结】
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
4. 交换类排序
4.1 冒泡排序
【基本思想】将待排序的数据两两进行比较,按比较结果来交换序列位置,将值大/值小的数据放到序列的尾部,将值小/值大的数据放到序列的首部位置,依次比较放置,最后得到有序序列
【基本思路】冒泡排序的每一次循环比较就是找出最大值(最小值),将最大值(最小值)放在数组尾部(首部),经过n次比较后,数据变成有序的,这里 flag 变量是优化作用,如果一轮下来没交换,则数组已经有序~
【示意图】
【具体代码】
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-03
* Time: 23:00
*/
public class BubbleSort {
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
bubbleSort(array);
for(int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
private static void bubbleSort(int[] array) {
for(int i = 0; i < array.length-1; i++) {
boolean flag = false;
for(int j = 0; j < array.length-1-i;j++) {
if(array[j+1] < array[j]) {
swap(array,j,j+1);
flag = true;
}
}
if(flag == false) {
break;
}
}
}
public static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
【特性总结】
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
4.2 快速排序
【基本思想】先从数组中取一个数作为基准数,进行分区,将比这个大的数全放到它的右边,小于或等于它的数全部放到它的左边,直到所有元素在它的位置上,此时就有序了~
【主体框架】
这是快速排序的主框架,我们可以看到这和二叉树的前序遍历递归实现的方式很类似,因此,我们在写快速排序的时候,可以联想到二叉树的前序遍历是如何写的,接下来,介绍快速排序的几种方式:
public class quickSort {
public void quickSort(int[] num) {
int left = 0;
int right = num.length-1;
quick(num,left,right);
}
private void quick(int[] num, int left, int right) {
if(left >= right) {
return;
}
// 按照基准值对数组的[left,right]划分
int pivot = partition(num,left,right);
// 划分区间:[left,pivot-1] [pivot,right]
// 递归排序[left,pivot-1]
quick(num,left,pivot-1);
// 递归排序[pivot,right]
quick(num,pivot+1,right);
}
private int partition(int[] num, int left, int right) {
// 根据一定的规则返回基准值
return -1;
}
}
4.2.1 三种实现方式
4.2.1.1 hoare 法
【基本思路】
hoare法就是定义两个标志位,right 从右往左找到比基准值小的值停下,left 从左往右找到比基准值大的值停下,交换 left 和 right 位置的值,循环继续,直到 left 和 right 相遇,最后再交换基准值和相遇处的值,再对左右子序列重复此过程,直到数据变成有序
1)先取最左侧即第一个元素为基准值
2)两个指针 left 和 right,left 从最左侧向右走,找到比基准大的元素停下,right 从最右侧向左走,找到比基准小的元素停下,交换 left 下标的值和 right 下标的值
3)当 left >= right 两个指针不再移动,此时 left = right 的下标即为基准值对应的下标,交换基准值初始下标和left(right)下标
【示意图】
【具体代码】
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-22
* Time: 0:05
*/
public class quickSort {
public static void quickSort(int[] num) {
int left = 0;
int right = num.length-1;
quick(num,left,right);
}
public static void quick(int[] num, int left, int right) {
if(left >= right) {
return;
}
int pivot = partition(num,left,right);
quick(num,left,pivot-1);
quick(num,pivot+1,right);
}
// 1.hoare法
public static int partition(int[] num, int left, int right) {
// 根据一定的规则返回基准值
int i = left+1;
int j = right;
int tmp = num[left];
while(true) {
while(i <= j && num[i] < tmp) {
i++;
}
while(i <= j && num[j] > tmp) {
j--;
}
if(i >= j) {
break;
}
swap(num,i,j);
}
swap(num,left,j);
return left;
}
public static void swap(int[] num,int i,int j) {
int tmp = num[i];
num[i] = num[j];
num[j] = tmp;
}
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
4.2.1.2 挖坑法
【基本思路】
1)先取最左侧即第一个元素为基准值
2)两个指针 left 和 right,left 从最左侧向右走,找到比基准大的元素停下,将 right 下标处的值赋值给 left 下标的值,right 从最右侧向左走,找到比基准小的元素停下,将 left 下标处的值赋值给 right 下标的值,如此循环
3)当 left >= right 两个指针不再移动,此时 left = right 的下标即为基准值对应的下标,交换基准值初始下标和left(right)下标
【示意图】
【具体代码】
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-22
* Time: 0:10
*/
public class quickSort {
public static void quickSort(int[] num) {
int left = 0;
int right = num.length-1;
quick(num,left,right);
}
public static void quick(int[] num, int left, int right) {
if(left >= right) {
return;
}
int pivot = partition(num,left,right);
quick(num,left,pivot-1);
quick(num,pivot+1,right);
}
// 2.挖坑法
public static int partition(int[] arr,int left,int right) {
int tmp = arr[left];
while(left < right) {
// 找到比基准tmp小的
while(left < right && arr[right] >= tmp) {
right--;
}
arr[left] = arr[right];
// 找到比基准tmp大的
while(left < right && arr[left] <= tmp) {
left++;
}
arr[right] = arr[left];
}
arr[left] = tmp;
return left;
}
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
4.2.1.3 前后指针法
【基本思路】
1)先取最左侧即第一个元素为基准值
2)前后指针 prev 和 cur,prev初始为left,cur初始为right,如果 cur 下标值小于基准值并且前指针prev++后的值与cur下标值不相等,前后指针至少一个间隔,并且值不相等,则交换前后指针的值
3)最后cur走到right,循环结束,交换prev和left值,此时prev就是基准值的位置
【示意图】
【具体代码】
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-22
* Time: 0:30
*/
public class quickSort {
public static void quickSort(int[] num) {
int left = 0;
int right = num.length-1;
quick(num,left,right);
}
public static void quick(int[] num, int left, int right) {
if(left >= right) {
return;
}
int pivot = partition(num,left,right);
quick(num,left,pivot-1);
quick(num,pivot+1,right);
}
//3.前后指针法
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;
}
public static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
4.2.1.4 快排优化
快排的基准是随机选取的,一般情况下,快排的时间复杂度为 O(nlog(n)),比如数据是有序的1,2,3,4,5… 会变成 O(n²)
时间复杂度我们可以用二叉树来理解,如下:
一般情况下,基准到达对应的位置后,序列被分为了左右子序列,基准元素为根结点,左边都比根节点的值小,右边都比根节点的值大,此时时间复杂度为 O(nlog(n))
存在特殊情况,数据是有序的1,2,3,4,5…
只有一个分支,此时树的高度就是结点的个数,时间复杂度则为O(n²),而当数据量足够大的时候,上述代码就无法跑通了~
【优化方法】
1)基准优化:三数取中法
即基准值取 left,mid,right 三个值中间的值,而不再是单纯取下标为 left 的值
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-22
* Time: 0:40
*/
public class quickSort {
public static void quickSort(int[] num) {
int left = 0;
int right = num.length-1;
quick(num,left,right);
}
public static void quick(int[] num, int left, int right) {
if(left >= right) {
return;
}
// 基准值优化
int index = midThree(num,left,right);
swap(num,index,left);
int pivot = partition(num,left,right);
quick(num,left,pivot-1);
quick(num,pivot+1,right);
}
private static int midThree(int[] array,int left,int right) {
int mid = (left + right) / 2;
if (array[left] < array[right]) {
if (array[mid] < array[left]) {
return left;
} else if (array[mid] > array[right]) {
return right;
} else {
return mid;
}
} else {
if (array[mid] < array[right]) {
return right;
} else if (array[mid] > array[left]) {
return left;
} else {
return mid;
}
}
}
public static int partition(int[] arr,int left,int right) {
int tmp = arr[left];
while(left < right) {
// 找到比基准tmp小的
while(left < right && arr[right] >= tmp) {
right--;
}
arr[left] = arr[right];
// 找到比基准tmp大的
while(left < right && arr[left] <= tmp) {
left++;
}
arr[right] = arr[left];
}
arr[left] = tmp;
return left;
}
public static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
2) 递归至较小的子区间时,使用插入排序:
递归至较小区间的时间,数据渐渐趋于有序,当数据有序的时候,建议直接使用插入排序,这样效率是比较高的
4.2.2 快排非递归实现
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-22
* Time: 22:41
*/
// 非递归实现快排
public class quickSort {
public static void quickSort(int[] array) {
Deque<Integer> stack = new LinkedList<>();
int left = 0;
int right = array.length - 1;
int pivot = partition(array,left,right);
if (pivot > left + 1) {
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right-1) {
stack.push(pivot+1);
stack.push(right);
}
while (!stack.isEmpty()) {
right = stack.pop();
left = stack.pop();
pivot = partition(array,left,right);
if (pivot > left + 1) {
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right-1) {
stack.push(pivot+1);
stack.push(right);
}
}
}
public static int partition(int[] arr,int left,int right) {
int tmp = arr[left];
while(left < right) {
// 找到比基准tmp小的
while(left < right && arr[right] >= tmp) {
right--;
}
arr[left] = arr[right];
// 找到比基准tmp大的
while(left < right && arr[left] <= tmp) {
left++;
}
arr[right] = arr[left];
}
arr[left] = tmp;
return left;
}
public static void main(String[] args) {
int[] array = {1,4,2,9,6,7};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
5. 归并排序
【基本思想】归并排序是建立在归并操作上的一种有效算法,该算法是分治法的一种典型应用,将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,将两个有序序列合并为一个有序序列,称为二路归并
【基本思路】使用递归的方式,先分解,再进行合并
【示意图】
【具体代码】
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wlx
* Date: 2025-06-21
* Time: 22:53
*/
public class Mergesort {
public static void mergeSort(int[] arr) {
mergeFunc(arr,0,arr.length-1);
}
public static void mergeFunc(int[] arr,int left,int right) {
if(left >= right) {
return;
}
int mid = (left+right) / 2;
mergeFunc(arr,left,mid);
mergeFunc(arr,mid+1,right);
merge(arr,left,right,mid);
}
public static void merge(int[] arr,int left,int right,int mid) {
int start1 = left;
int start2 = mid+1;
int k = 0;
int[] tmp = new int[right-left+1];
while(start1 <= mid && start2 <= right) {
if(arr[start1] < arr[start2]) {
tmp[k++] = arr[start1++];
} else {
tmp[k++] = arr[start2++];
}
}
while(start1 <= mid) {
tmp[k++] = arr[start1++];
}
while(start2 <= right) {
tmp[k++] = arr[start2++];
}
for(int i = 0; i < k; i++) {
arr[left+i] = tmp[i];
}
}
public static void main(String[] args) {
int[] arr = {1,4,2,9,6,7};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
}
【特性总结】
- 时间复杂度:O(O(N*logN))
- 空间复杂度:O(1)
- 稳定性:稳定
- 应用场景:适合数据特别大的时候,当待排序数据特别大的时候,比如内存只要 10G,但是待排序的数据有 100G,此时可以将待处理的数据分为 20份,每一份 512M,利用归并排序分别对这 512M 的数据进行排序,同时进行二路归并,最后使数据变为有序,这就是文章一开头介绍的外部排序~
6. 特性总结
排序名称 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
希尔排序 | O(n^1.3) - O(n^1.5) | O(n log²n) | O(n log²n) | O(1) | 不稳定 |
直接选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
快速排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(logN) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 稳定 |
【注意】这里的时间复杂度为最坏时间复杂度
✨✨✨本期内容到此结束啦~