📚 目录
-
[1. 冒泡排序](#1. 冒泡排序)
-
[2. 直接插入排序](#2. 直接插入排序)
-
[3. 希尔排序](#3. 希尔排序)
-
[4. 堆排序](#4. 堆排序)
-
[5. 选择排序](#5. 选择排序)
-
[6. 快速排序](#6. 快速排序)
-
[7. 归并排序](#7. 归并排序)
-
[8. 计数排序](#8. 计数排序)
前言:
这篇博客是我在学习数据结构时,整理的6种经典排序算法笔记,用Java从零手搓实现,也算是给自己留个存档,方便以后复习。
1. 冒泡排序
冒泡排序是一种基础的交换排序算法,核心思想是相邻元素两两比较,大的往后交换 ,每一轮都会把当前未排序部分中最大的元素"冒泡"到末尾。
就像我们鱼吐泡泡一样,越往上面的泡泡,就会越来越大。
首先我们得先确定排序的趟数,以10个元素为例,当我们没有进行任何优化的时候,我们最多需要进行9趟排序,每次将最大值移动到最后一个。
如果此时是有序的情况时,在没有任何优化的前提下:我们还是需要进行排序9躺。
优化:我们需要定义一个flg来进行判断是否进行交换,如果没进行交换则直接退出。
java
public static void bubbleSort(int[] array) {
if(array == null || array.length <= 1) {
return;
}
// 外层循环控制排序轮数
for (int i = 0; i < array.length - 1; i++) {
// 标记本轮是否发生交换,优化冒泡排序
boolean flg = false;
// 内层循环比较相邻元素
for (int j = 0; j < array.length - 1 - i; j++) {
if(array[j]>array[j+1]) {
//交换,也能写成方法进行交换j和j+1下标的值
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;
}
}
//此时没有进行交换则退出循环
if(!flg) {
break;
}
}
}
public static void main(String[] args) {
int[] array = new int[]{8,7,4,3,2,6,1};
Boke.bubbleSort(array);
System.out.println(Arrays.toString(array));
}
排序结果:

1.1 复杂度与稳定性
时间复杂度
- 最坏情况(完全逆序) : O ( n 2 ) O(n^2) O(n2),需要执行 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2次比较和交换
- 最好情况(完全有序) : O ( n ) O(n) O(n),通过
flag优化后仅需1轮循环即可提前退出 - 平均情况 : O ( n 2 ) O(n^2) O(n2)
空间复杂度
- O ( 1 ) O(1) O(1):冒泡排序是原地排序算法,仅使用常数级别的额外空间。
稳定性
- 冒泡排序是稳定排序 :因为比较条件为
array[j] > array[j+1],相等元素不会发生交换,因此相对位置保持不变。
[🔙 返回目录](#🔙 返回目录)
2. 直接插入排序
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。实际中我们玩扑克牌时,就用了插入排序的思想。

直接插入排序的思想和我们打扑克牌理牌的过程一模一样:
把数组分为「已排序区间」和「未排序区间」
每次从「未排序区间」取出第一个元素,把它插入到「已排序区间」的合适位置重复这个过程,直到整个数组都变成有序的。
举个例子:
- 初始数组:5, 2, 4, 6, 1, 3
- 第 1 步:把 5 当作已排序区间,从 2 开始插入
- 第 2 步:把 2 插入到 5 前面 → 2, 5, 4, 6, 1, 3
- 第 3 步:把 4 插入到 5 前面 → 2, 4, 5, 6, 1, 3
- 以此类推,直到所有元素都插入完毕
java
public static void insertSort(int[] array) {
//边界判断
if(array == null || array.length<=1) {
return;
}
//此时我们需要从第二个元素开始进行排序
for (int i = 1; i < array.length; i++) {
//带插入的位置,假设为i下标
int tmp = array[i];
//j为我们需要插入的位置
int j = i-1;
for (;j>=0;j--) {
//只要比tmp大的元素都往后移动
if(array[j]>tmp) {
array[j+1] = array[j];
}else {
break;
}
}
//插入到正确位置
array[j+1] = tmp;
}
}
public static void main(String[] args) {
int[] array = new int[]{5,2,4,6,1,3};
Boke.insertSort(array);
System.out.println(Arrays.toString(array));
}
结果:

时间复杂度
- 最坏情况(完全逆序):O(n²)
- 最好情况(数组已有序):O(n)
- 平均情况:O(n²)
空间复杂度
- O(1)
- 直接插入排序是原地排序,只使用了常数级的额外空间。
稳定性
- 直接插入排序是稳定排序。
比较条件为 arrayj > tmp,相等元素不会交换位置,相对顺序不变。
[🔙 返回目录](#🔙 返回目录)
3. 希尔排序
希尔排序,也叫缩小增量排序,是对直接插入排序的优化版本 。
核心思想:先选定一个整数,将待排序列分为多个组,所有距离在同一个组,然后对每一组进行排序。
举个简单例子:
- 初始数组:9, 5, 2, 7, 1, 3, 8, 4
- 第 1 趟(gap=4):分成 4 组,每组 2 个元素,分别排序 → 1, 3, 2, 4, 9, 5, 8, 7
- 第 2 趟(gap=2):分成 2 组,每组 4 个元素,分别排序 → 1, 2, 8, 4, 9, 3, 5, 7
- 第 3 趟(gap=1):对整个数组执行直接插入排序 → 1, 2, 3, 4, 5, 7, 8, 9
分组原理:当gap为4的时候,将9和1位置的进行排序,刚好增加了4个位置。
java
// 希尔排序主方法
public static void shellSort(int[] array) {
int gap = array.length;
while (gap>1) {
gap = gap/2;
shell(array,gap);
}
}
private static void shell(int[] array, int gap) {
//边界判断
if (array == null || array.length <= 1) {
return;
}
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 {
break;
}
}
// 插入到正确位置
array[j+gap] = tmp;
}
}
public static void main(String[] args) {
int[] array = new int[]{9, 5, 2, 7, 1, 3, 8, 4};
Boke.shellSort(array);
System.out.println(Arrays.toString(array));
}
结果:

时间复杂度
- 最坏情况:O(n²)
- 最好情况:O(n)
- 平均情况:O(n^1.3)
- 希尔排序的时间复杂度依赖增量序列,目前没有公认的精确数学结论。基于实验统计,常用的增量序列下,平均时间复杂度约为 (O(n^{1.3})),介于 (O(n^{1.25})) 到 (O(n^{1.5})) 之间,最坏情况为 (O(n^2))。
空间复杂度
- 空间复杂度:O(1)
稳定性
- 稳定性:不稳定排序
[🔙 返回目录](#🔙 返回目录)
4. 堆排序
核心思想:
-
建堆:把待排序数组调整成一个大根堆(父节点值 ≥ 子节点值),此时堆顶元素就是数组中的最大值。
-
交换堆顶与末尾元素:把堆顶的最大值和数组末尾元素交换,此时最大值已经到达它的最终位置。
-
调整堆:把末尾元素排除在堆外,对剩下的元素重新调整为大根堆。
-
重复过程:不断交换堆顶与末尾元素、调整堆,直到整个数组有序。

java
//堆排序
public static void rootSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
buildMaxHeap(array);//创建大根堆
int len = array.length - 1;//拿到这个堆的最后最后一个节点下标
while (len > 0) {
swap(array, 0, len); // 交换堆顶和末尾
sifDown(array, 0, len); //进行向下调整
len--;
}
}
// 构建大根堆
private static void buildMaxHeap(int[] array) {
for (int parent = (array.length - 1-1) / 2; parent >= 0; parent--) {
sifDown(array, parent, array.length);
}
}
// 向下调整堆
private static void sifDown(int[] array, int parent, int length) {
int child = parent * 2 + 1;
while (child < length) {
if (child + 1 < length && array[child] < array[child + 1]) {
child++;
}
if (array[child] > array[parent]) {
swap(array, child, parent);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
//交换
private static void swap(int[] array, int a, int b) {
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
public static void main(String[] args) {
int[] array = new int[]{9,5,2,7,1,3,8,4};
Boke.rootSort(array);
System.out.println(Arrays.toString(array));
}
结果:

时间复杂度
- 时间复杂度:O(n log n)(最好、最坏、平均都是 O(n log n))
空间复杂度
- 空间复杂度:O(1)(原地排序)
稳定性
- 稳定性:不稳定排序
[🔙 返回目录](#🔙 返回目录)
5. 选择排序
选择排序是一种基础的选择类排序算法
核心思想:
- 每一轮从待排序的序列中,选出最小(或最大)的元素,放到序列的起始位置,再从剩下的未排序元素中继续找最小 / 最大值,放到已排序序列的末尾,直到所有元素都排好序。
- 举个简单例子,对数组 5, 3, 8, 4, 2 排序:
第 1 轮:找到最小值 2,和第一个元素 5 交换 → 2, 3, 8, 4, 5
第 2 轮:在剩下的 3, 8, 4, 5 中找最小值 3,和第二个元素 3 交换(无变化)
第 3 轮:在剩下的 8, 4, 5 中找最小值 4,和第三个元素 8 交换 → 2, 3, 4, 8, 5
第 4 轮:在剩下的 8, 5 中找最小值 5,和第四个元素 8 交换 → 2, 3, 4, 5, 8
java
//选择排序
public static void selectionSort(int[] array) {
if(array == null || array.length<=1) {
return;
}
//内层循环找比mindex下标小的值
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,minIndex,i);
}
}
//交换
private static void swap(int[] array, int a, int b) {
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
//测试
public static void main(String[] args) {
int[] array = new int[]{5, 3, 8, 4, 2};
Boke.selectionSort(array);
System.out.println(Arrays.toString(array));
}
结果:

时间复杂度:O(n²)
- 最好、最坏、平均情况均为 O(n²)
空间复杂度:O(1)
- 属于原地排序算法
稳定性:不稳定排序
[🔙 返回目录](#🔙 返回目录)
6. 快速排序
基本思想:
任取待排序列中的某一个元素作为基准,按照该排序大小将待排序列分为两个子序列,左子序列中所有元素小于基准值,右子序列中所有元素大于基准值。重复该过程,直到待排序的所有元素在相应位置。
- 以数组 9, 5, 2, 7, 1, 3, 8, 4 为例(选第一个元素 9 为基准):
- 分区后:5, 2, 7, 1, 3, 8, 4 + 9,9 完成归位;
- 对左区间 5, 2, 7, 1, 3, 8, 4 继续选基准分区;
- 不断递归划分,最终得到有序数组 1, 2, 3, 4, 5, 7, 8, 9。
递归版本
首先我们得先找到基准,基准的左边比基准都要笑,基准的右边都比基准大。
Hoare 法 :
Hoare 法是快速排序最原始、最经典的实现方式,由快速排序的发明者 Tony Hoare 提出。
- 它通过双指针相向遍历 + 直接交换的方式完成分区:
- 选定一个基准值(通常取区间最左侧元素)。
- 右指针从区间末尾向左移动,找到第一个小于基准的元素。
- 左指针从区间开头向右移动,找到第一个大于基准的元素。
- 交换两个指针指向的元素,重复上述过程直到两指针相遇。
- 将基准值与相遇位置的元素交换,完成分区。
java
private static int partition(int[] array, int low, int high) {
//先记录low下标
int i = low;
//假设基准为low下标的值
int pivot = array[low];
while (low<high) {
//找到右边比基准小的值
while (low<high && array[ high] >= pivot) {
high--;
}
//找到左边基准小的值
while (low<high && array[low] <= pivot) {
low++;
}
//交换low和high下标的值
swap(array,low,high);
}
//此时low 等于high交换low/high与i下标的值
swap(array,low,i);
return low;
}
** 挖坑法:**
先将第一个数据放在一个临时变量中,形成一个坑位
首先从右边找比坑小的值进行填入到坑中,找到的值变成新坑,然后从左边找比坑大的值填入到坑中。重复该过程直到左右相遇。
java
public static int partition1(int[] array,int low,int high) {
//放入第一个值形成一个坑
int pivot = array[low];
while (low<high) {
while (low<high && array[high] >= pivot) {
high--;
}
array[low] = array[high];
while (low<high && array[low] <= pivot) {
low++;
}
array[high] = array[low];
}
array[low] = pivot;
return low;
}
此时我们就能写出没有进行任何优化过的快速排序。
java
public static void quickSort(int[] array) {
if(array == null || array.length<=1) {
return;
}
sort(array,0,array.length-1);
}
private static void sort(int[] array, int low, int high) {
//递归结束条件low>=high的时候
if(low >= high) {
return;
}
//找基准
int pivotIndex = partition(array,low,high);//此时的可以调用挖坑法也能调用Hoare 法
//然后在进行排序左边
sort(array,low,pivotIndex-1);
sort(array,pivotIndex+1,high);
}
public static void main(String[] args) {
int[] array = new int[]{9, 5, 2, 7, 1, 3, 8, 4};
Boke.quickSort(array);
System.out.println(Arrays.toString(array));
}
结果:

此时快速排序没有进行任何优化,如果想要优化,我们可以采取三树取中的方法进行优化,当排序数目比较小时我们可以采用直接插入排序的方法。
三数取中:
核心思想:在进行找基准前,先找到左下标与右下标以及中间下标中的中间值的下标,让中间节点的下标的值与左边节点的值进行交换。
java
private static int treeIndex(int[] array, int low, int high) {
//拿到中间下标
int mid = (low + high) >> 1;
if (array[low] < array[high]) {
if (array[mid] < array[low]) {
return low;
} else if (array[mid] > array[high]) {
return high;
} else {
return mid;
}
} else {
if (array[mid] < array[high]) {
return high;
} else if (array[mid] > array[low]) {
return low;
} else {
return mid;
}
}
}
直接插入排序优化快速排序
此时和我们的之前写的直接插入排序只有一点点的小区别;区别:外层循环的下标从low下标开始。
java
private static void inserSort(int[] array, int low, int high) {
for (int i = low + 1; i <= high; i++) {
int j = i - 1;
int tmp = array[i];
for (; j >= low; j--) {
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = tmp;
}
}
此时我们就能够进行优化我们手搓的快速排序。加快排序的速度。
java
private static void sort(int[] array, int low, int high) {
//递归结束条件low>=high的时候
if(low >= high) {
return;
}
if ((high - +1) <= 15) {
inserSort(array, low, high);
return;
}
//三树取中
int index = treeIndex(array, low, high);
swap(array, low, index);
//找基准
int pivotIndex = partition(array,low,high);
//然后在进行排序左边
sort(array,low,pivotIndex-1);
sort(array,pivotIndex+1,high);
}
非递归版本快速排序
核心思想:
- 用栈存储待排序区间left, right;
- 初始把整个数组区间0, len-1入栈;
- 循环取出栈顶区间,用挖坑法 / Hoare 法分区;
- 得到基准下标pivot后,把右区间、左区间依次入栈;
- 直到栈为空,排序完成。
java
public static void quickSortNonRecursive(int[] array) {
if(array == null || array.length<=1) {
return;
}
int low = 0;
int high = array.length-1;//最后一个下标
//找到基准
int pivot = partition(array,low,high);
//创建一个栈
Deque<Integer> stack = new LinkedList<>();
//基准大于low下标+1表示至少基准左边有一个元素
if(pivot > low+1) {
stack.push(low);
stack.push(pivot-1);
}
//同理,保证右边至少有1一个元素
if(pivot < high-1) {
stack.push(pivot+1);
stack.push(high);
}
while (!stack.isEmpty()) {
//因为栈先进后出所以此时我们弹出的是high的值先
high = stack.pop();
low = stack.pop();
//在进行找基准
pivot = partition(array,low,high);
//重复之前的操作
if(pivot > low+1) {
stack.push(low);
stack.push(pivot-1);
}
if(pivot < high-1) {
stack.push(pivot+1);
stack.push(high);
}
}
}
public static void main(String[] args) {
int[] array = new int[]{9, 5, 2, 7, 1, 3, 8, 4};
Boke.quickSortNonRecursive(array);
System.out.println(Arrays.toString(array));
}
结果:

时间复杂度:
- 最好:O(n log n)
- 平均:O(n log n)
- 最坏:O(n²)(有序/逆序数组)
空间复杂度:
- O(log n) ------ 递归调用栈开销
- 最坏:O(n)
稳定性:不稳定排序
[🔙 返回目录](#🔙 返回目录)
7. 归并排序
基本思想:
- 归并排序是一种基于分治法的排序算法。
- 它先将待排序数组不断拆分成更小的子数组,直到每个子数组只有一个元素(天然有序)。
- 然后逐层合并这些有序的子数组,最终得到一个完整的有序数组。
步骤: - 拆分:把数组从中间分成左右两部分,递归拆分,直到每个区间只有一个元素。
- 合并:将两个有序的子区间,按大小顺序合并成一个新的有序区间。
- 重复:直到整个数组合并完成。
归并排序递归版本
java
public static void mergeSort(int[] array) {
if(array == null || array.length <= 1) {
return;
}
//嵌套一层函数进行真正的归并排序
mergeSortChild(array,0,array.length-1);
}
private static void mergeSortChild(int[] array, int low, int high) {
if(low>=high) {
return;
}
//此时是合法的,首先分治每次都进行平方,进行排序
int mid = (low+high)>>1;
//先完成左边的,拆分
mergeSortChild(array,low,mid);
//再完成右边的,拆分
mergeSortChild(array,mid+1,high);
//最后进行合并
merge(array,low,mid,high);
}
private static void merge(int[] array, int low, int mid, int high) {
//此时我们就需要定义一个数组出来,长度位low和high的差值+1
int[] tmp = new int[high-low+1];
//定义一个k下标进行完成tmp的下标寻找
int k = 0;
//此时为了更好的理解,我们定义数组的范围的下标
//low - mid 的下标
int s1 = low;
int e1 = mid;
//mid+1 - high下标
int s2 = mid+1;
int e2 = high;
while (s1<=e1 && s2<=e2) {
if(array[s1]<array[s2]) {
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
//此时肯定有一个范围没有放入tmp这个数组中
while (s1<=e1) {
tmp[k++] = array[s1++];
}
while (s2<=e2) {
tmp[k++] = array[s2++];
}
//进行拷贝数组到array中
for (int i = 0; i < tmp.length; i++) {
array[i+low] = tmp[i];
}
}
//Test类中
public static void main(String[] args) {
int[] array = new int[]{9, 5, 2, 7, 1, 3, 8, 4};
Boke.mergeSort(array);
System.out.println(Arrays.toString(array));
}
结果:

非递归版本归并排序
基本思想
- 非递归归并排序抛弃递归拆分,直接从最小有序段(长度 = 1)开始,自底向上逐层合并,直到整个数组变成一个有序段。
- 核心逻辑和递归版完全一致,只是从下往上合并,没有递归调用栈,更稳定、更适合工程使用。
基本步骤: - 从步长 gap=1 开始(每个元素自己就是有序段);
- 每次把相邻的两个有序段合并成一个大的有序段;
- gap 每次翻倍:gap *= 2;
- 直到 gap ≥ 数组长度,排序完成。
java
public static void mergeSortNonRecursive(int[] array) {
if(array == null || array.length <= 1) {
return;
}
int gap = 1;
while (gap<array.length) {
//i = i+2*gap是为了进行处理下一对需要排序的范围,每次进行规定范围合并
for (int i = 0; i < array.length; i= i +2*gap) {
int low = i;
//得到中间下标
int mid = gap+low-1;
//防止mid越界
if(mid >= array.length) {
mid = array.length-1;
}
int high = mid+gap;
//防止high越界
if(high >= array.length) {
high = array.length-1;
}
merge(array,low,mid,high);//此时的合并和递归版本的合并是一样的
}
gap = 2*gap;
}
}
时间复杂度:
- 最好:O(n log n)
- 平均:O(n log n)
- 最坏:O(n log n)
(归并排序效率非常稳定)
空间复杂度:
- O(n) ------ 需要额外的临时数组存储数据
稳定性:稳定排序
[🔙 返回目录](#🔙 返回目录)
8. 计数排序
基本思想:
先统计:
- 遍历原数组,统计每个元素出现的次数,存到一个「计数数组」里。
- 比如数组 3,1,2,3,2,统计结果就是:1:1次,2:2次,3:2次。
再回填:
- 从小到大遍历计数数组,按每个数字的出现次数,把它写回原数组。
- 比如 1 出现 1 次,就写 1 个 1;2 出现 2 次,就写 2 个 2......
适用前提: - 元素是非负整数(可以是正整数、0)
- 数据的最大值和最小值差距不大(不然计数数组会特别大,浪费空间)
java
//计数排序
public static void countSort(int[] array) {
if(array == null || array.length <= 1) {
return;
}
//假设最大最小值都位0下标
int max = array[0];
int min = array[0];
//找到最大最小值
for (int i = 1; i < array.length; i++) {
if(array[i]>max) {
max = array[i];
}
if(array[i] < min) {
min = array[i];
}
}
//新的数组长度
int len = max - min +1;
int[] tmp = new int[len];
//在array[i]下标的值为多少久让tmp下标的值++
for (int i = 0; i < array.length; i++) {
int index = array[i];
//为什么要让index的位置减最小值?
//这样可以减少空间浪费,重新放回到array中的时候重新加上最小值就行了
tmp[index - min]++;
}
//定义k来给array进行赋值
int k = 0;
for (int i = 0; i < tmp.length; i++) {
while (tmp[i]!=0) {
array[k] = i+min;
tmp[i]--;
k++;
}
}
}
//不同类中的Test类
public static void main(String[] args) {
int[] array = new int[]{3,1,2,3,2};
Boke.countSort(array);
System.out.println(Arrays.toString(array));
}
结果:
时间复杂度
- O(n + k)
- n:待排序数组的长度
- k:数据的范围大小(max - min + 1)
- 最好、最坏、平均情况 全部都是 O (n + k)
空间复杂度
- O(k)
- 只开辟了一个计数数组,大小等于数据范围 k
- 属于非原地排序
稳定性
- 稳定排序

[🔙 返回目录](#🔙 返回目录)