前言
排序是编程开发中最基础也最核心的操作之一,无论是日常业务开发中的数据整理,还是算法竞赛里的核心逻辑实现,掌握排序算法都至关重要。作为一名Java学习者,今天就带大家梳理Java中常用的排序算法,以及JDK内置排序工具的使用技巧。
今天的内容主要是两个部分,把常见的8中排序手动实现一下,然后介绍一下java内部的arrays.sort().
经典排序算法实现(Java版)
我们先从几种基础排序算法入手,理解其核心思想和代码实现,这对数据结构学习和蓝桥杯等竞赛备考也很有帮助。
1. 冒泡排序(Bubble Sort)
核心思想:重复遍历待排序数组,每次比较相邻两个元素,若顺序错误则交换,直到没有元素需要交换为止。就像气泡从水底逐渐上浮到水面一样,大的元素会逐步"冒泡"到数组末尾。
Java实现:
java
public static void bubbleSort(int[] arr){
//i表示的躺数
for (int i = 0; i < arr.length - 1; i++) {
boolean flg = false;
for (int j = 0; j < arr.length -1 - i; j++) {
if (arr[j] > arr[j+1]){
swap(arr,j,j+1);
flg = true; //说明我这一趟是有过交换的
}
}
if (!flg){
return;
}
}
}
特点:
- 稳定排序;
- 平均时间复杂度 O(n2)O(n^2)O(n2) 最好O(N)O(N)O(N)
- 空间复杂度O(1)O(1)O(1)
- 适合小规模数据排序。
2. 快速排序(Quick Sort)
算法原理:分治思想的典型应用。选择一个元素作为"基准",将数组分为两部分,一部分元素比基准小,另一部分比基准大;然后递归地对两部分数组进行排序。
Java实现:
java
//非递归形式
public static void quickSortNor(int[] arr){
if (arr == null || arr.length <= 1) {
return;
}
Stack<Integer> stack = new Stack<>();
// 初始化栈,存入整个数组的边界:先存high,再存low(因为栈是后进先出)
stack.push(arr.length - 1);
stack.push(0);
while (!stack.isEmpty()) {
int low = stack.pop();
int high = stack.pop();
// 分区操作
int par = parttionHoare(arr, low, high);
// 左子数组入栈:[low, par-1]
if (low < par - 1) {
stack.push(par - 1);
stack.push(low);
}
// 右子数组入栈:[par+1, high]
if (par + 1 < high) {
stack.push(high);
stack.push(par + 1);
}
}
}
//快排序
public static void quickSort(int[] arr){
quick(arr,0,arr.length -1);
}
private static void quick(int[] arr, int start, int end) {
if (start > end){
return;
}
int par = parttion(arr,start,end);
quick(arr,start,par-1);
quick(arr,par +1 ,end);
}
private static int parttion(int[] arr, int low, int high) {
//定义一个基准
//int pivot = arr[low];
// 优化:三数取中法选基准(可选,避免有序数组的最坏情况)
int mid = low + (high - low) / 2;
//比较前两个
if(arr[mid] < arr[low]) swap(arr,mid,low);
//比较第一个与最后一个 -- h是最小的
if(arr[high] < arr[low]) swap(arr,high,low);
//中间与最后
if (arr[high] < arr[mid]) swap(arr,mid,high);
swap(arr, low, mid); // 把中间值放到 low 位置作为基准
int tmp = arr[low];
//标记一下low的位置
int i = low;
while (low < high){
while (low < high && arr[high] >= tmp){
high --;
}
arr[low] = arr[high];
while (low < high && arr[low] <= tmp){
low ++;
}
arr[high] = arr[low];
//左边找到大的,右边找到小的了,此时交换
}
//交换基准值
arr[high] = tmp;
return high;
}
private static int parttionHoare(int[] arr, int low, int high) {
//定义一个基准
//int pivot = arr[low];
// 优化:三数取中法选基准(可选,避免有序数组的最坏情况)
int mid = low + (high - low) / 2;
//比较前两个
if(arr[mid] < arr[low]) swap(arr,mid,low);
//比较第一个与最后一个 -- h是最小的
if(arr[high] < arr[low]) swap(arr,high,low);
//中间与最后
if (arr[high] < arr[mid]) swap(arr,mid,high);
swap(arr, low, mid); // 把中间值放到 low 位置作为基准
int pivot = arr[low];
//标记一下low的位置
int i = low;
while (low < high){
while (low < high && arr[high] >= pivot){
high --;
}
while (low < high && arr[low] <= pivot){
low ++;
}
//左边找到大的,右边找到小的了,此时交换
swap(arr,low,high);
}
//交换基准值
swap(arr,high,i);
return high;
}
特点:
- 稳定排序:不稳定
- 平均时间复杂度 O(nlogn)O(n\log n)O(nlogn),最好 O(nlogn)O(n\log n)O(nlogn),最坏 O(n2)O(n^2)O(n2)
- 空间复杂度 O(logn)O(\log n)O(logn)(递归版,函数栈帧消耗)/ O(logn)O(\log n)O(logn)(非递归版,手动栈消耗)
- 适合大规模数据排序
3.直接插入排序(insertionSort)
算法原理
初始化:默认数组的第一个元素属于「已排序区间」,从第二个元素开始处理未排序区间。
取出待插入元素:保存当前未排序区间的第一个元素(记为 current),避免后续移动元素时被覆盖。
向前比较并移动:将 current 和已排序区间的元素从后往前逐一比较:
如果已排序元素 > current,则将该元素向后移动一位。
如果遇到已排序元素 ≤ current,则停止比较,找到了插入位置。
插入元素:将 current 插入到找到的位置。
重复:直到所有未排序元素都完成插入。
java代码实现
java
public static void insertionSort(int[] arr) {
// 空数组或只有一个元素,无需排序
if (arr == null || arr.length == 1) {
return;
}
// 从第2个元素开始(下标1),遍历未排序区间
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j = i-1;
for (; j >= 0 ; j++) {
//j>=0 处理越界情况 后面处理
if (arr[j] >arr[j+1]){
arr[j+1] = arr[j];
}else {
arr[j+1] = tmp;
break;
}
} //不走else
arr[j+1] = tmp;
}
}
特点:
- 平均时间复杂度 O(n2)O(n^2)O(n2)、最好时间复杂度 O(n)O(n)O(n)、最坏时间复杂度 O(n2)O(n^2)O(n2);
- 空间复杂度 O(1)O(1)O(1);
- 适合小规模数据或基本有序数据的排序。
4.希尔排序(shell)
核心原理
1.增量分组 :先选定一个增量 gap,将数组按 gap 划分为若干子序列(下标差为 gap 的元素为一组)。
2.组内插入排序 :对每个子序列分别进行直接插入排序,让数组整体变得 "基本有序"。
3.缩小增量 :逐步缩小增量 gap(通常取 gap = gap / 2),重复分组和排序操作。
4.最终排序:当增量 gap = 1 时,整个数组被分为一组,此时执行一次直接插入排序,即可完成最终排序。
Java实现:
java
public static void shellSort(int[] arr) {
int gap = arr.length;
while (gap >= 1) {
gap = gap / 2;
shell(arr, gap);
}
}
private static void shell(int[] arr, int gap) {
if (arr == null || arr.length == 1) {
return;
}
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j = i-gap;
for (; j >= 0 ; j-=gap) {
//j>=0 处理越界情况 后面处理
if (arr[j] > tmp){
arr[j+gap] = arr[j];
}else {
arr[j+gap] = tmp;
break;
}
} //不走else
arr[j+gap] = tmp;
}
}
特点
- 不稳定排序;
- 时间复杂度与增量序列相关,平均时间复杂度约 O(n1.3)O(n^{1.3})O(n1.3) 、最好时间复杂度 O(n)O(n)O(n)、最坏时间复杂度 O(n2)O(n^2)O(n2);
- 空间复杂度 O(1)O(1)O(1);
- 适合中等规模数据排序,效率优于直接插入、冒泡排序。
5. 选择排序(Selection)
核心原理
- 划分区间 :将数组划分为已排序区间 (初始为空)和未排序区间(初始为整个数组)。
- 查找最值:遍历未排序区间,找到其中的最小(或最大)元素。
- 交换位置:将找到的最值元素与未排序区间的第一个元素交换,扩大已排序区间。
- 重复操作:重复上述步骤,直到未排序区间为空。
Java 实现
java
public static void selectionSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
// 记录未排序区间的最小元素下标
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换最小元素和未排序区间首元素
int tmp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = tmp;
}
}
特点
- 不稳定排序;
- 平均时间复杂度 O(n2)O(n^2)O(n2)、最好时间复杂度 O(n2)O(n^2)O(n2)、最坏时间复杂度 O(n2)O(n^2)O(n2);
- 空间复杂度 O(1)O(1)O(1);
- 适合小规模数据排序,数据移动次数少(仅需 n−1n-1n−1 次交换)。
6. 堆排序(Heap)
核心原理
- 构建大顶堆:将待排序数组构建成完全二叉树形式的大顶堆(每个父节点值 ≥ 子节点值)。
- 交换堆顶与堆尾:将堆顶的最大值与堆的最后一个元素交换,此时最大值进入已排序区间。
- 堆化调整:将剩余的未排序元素重新调整为大顶堆,恢复堆的性质。
- 重复操作:重复交换和堆化步骤,直到堆的大小缩减为 1。
Java 实现
java
public static void heapSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int n = arr.length;
// 1. 构建大顶堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 2. 交换堆顶和堆尾,再堆化
for (int i = n - 1; i > 0; i--) {
int tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
heapify(arr, i, 0);
}
}
// 堆化函数:调整以 i 为根的子树为大顶堆
private static void heapify(int[] arr, int heapSize, int i) {
int largest = i; // 根节点为最大值
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 比较左子节点
if (left < heapSize && arr[left] > arr[largest]) {
largest = left;
}
// 比较右子节点
if (right < heapSize && arr[right] > arr[largest]) {
largest = right;
}
// 最大值不是根节点则交换并递归堆化
if (largest != i) {
int tmp = arr[i];
arr[i] = arr[largest];
arr[largest] = tmp;
heapify(arr, heapSize, largest);
}
}
特点
- 不稳定排序;
- 平均时间复杂度 O(nlogn)O(nlogn)O(nlogn)、最好时间复杂度 O(nlogn)O(nlogn)O(nlogn)、最坏时间复杂度 O(nlogn)O(nlogn)O(nlogn);
- 空间复杂度 O(1)O(1)O(1);
- 适合大规模数据排序,不受数据初始状态影响。
7. 归并排序(Merge)
核心原理
- 分治拆分:采用分治法,将待排序数组递归拆分为两个长度相等的子数组,直到子数组长度为 1。
- 有序合并:将两个有序的子数组合并为一个有序数组,合并过程中需要临时空间存储合并结果。
- 递归回溯:逐层回溯合并,最终得到一个完全有序的数组。
Java 实现
java
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int[] tmpArr = new int[arr.length];
mergeSort(arr, tmpArr, 0, arr.length - 1);
}
private static void mergeSort(int[] arr, int[] tmpArr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
// 递归拆分左半部分
mergeSort(arr, tmpArr, left, mid);
// 递归拆分右半部分
mergeSort(arr, tmpArr, mid + 1, right);
// 合并两个有序子数组
merge(arr, tmpArr, left, mid, right);
}
}
private static void merge(int[] arr, int[] tmpArr, int left, int mid, int right) {
int i = left; // 左子数组起始下标
int j = mid + 1; // 右子数组起始下标
int k = left; // 临时数组起始下标
// 合并两个有序子数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tmpArr[k++] = arr[i++];
} else {
tmpArr[k++] = arr[j++];
}
}
// 拷贝左子数组剩余元素
while (i <= mid) {
tmpArr[k++] = arr[i++];
}
// 拷贝右子数组剩余元素
while (j <= right) {
tmpArr[k++] = arr[j++];
}
// 临时数组元素拷贝回原数组
while (left <= right) {
arr[left] = tmpArr[left];
left++;
}
}
特点
- 稳定排序;
- 平均时间复杂度 O(nlogn)O(nlogn)O(nlogn)、最好时间复杂度 O(nlogn)O(nlogn)O(nlogn)、最坏时间复杂度 O(nlogn)O(nlogn)O(nlogn);
- 空间复杂度 O(n)O(n)O(n)(非原地实现,需临时数组);
- 适合大规模数据排序,尤其适合链表等非连续存储数据。
8. 基数排序(了解)(Radix)
核心原理
- 确定位数 :找到数组中的最大值,确定其最高位数 ddd(如 123 的最高位是百位,d=3d=3d=3)。
- 按位排序:从最低位到最高位依次对数组进行排序,每一轮排序基于当前位的数值,采用桶排序或计数排序实现。
- 合并结果 :每一轮排序后,数组会按当前位有序,经过 ddd 轮排序后,数组整体有序。
Java 实现
java
public static void radixSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
// 1. 找到最大值,确定最大位数
int max = arr[0];
for (int num : arr) {
if (num > max) {
max = num;
}
}
// 2. 按位排序:从个位到最高位
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}
// 按当前位(exp 对应位)进行计数排序
private static void countingSortByDigit(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n]; // 存储排序结果
int[] count = new int[10]; // 0-9 的数字计数
// 统计当前位的数字出现次数
for (int num : arr) {
int digit = (num / exp) % 10;
count[digit]++;
}
// 计算前缀和,确定元素位置
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 倒序遍历,保证稳定性
for (int i = n - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
// 拷贝结果回原数组
System.arraycopy(output, 0, arr, 0, n);
}
特点
- 稳定排序;
- 平均时间复杂度 O(d×(n+r))O(d\times(n+r))O(d×(n+r))、最好时间复杂度 O(d×(n+r))O(d\times(n+r))O(d×(n+r))、最坏时间复杂度 O(d×(n+r))O(d\times(n+r))O(d×(n+r))(ddd 为位数,rrr 为基数);
- 空间复杂度 O(n+r)O(n+r)O(n+r);
- 适合整数、字符串等基于"位"或"关键字"排序的场景,数据范围较大时优势明显。
*在这里插入代码片*# 常见排序算法特性对比表
| 排序算法 | 稳定性 | 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|---|---|
| 直接插入排序 | 稳定 | O(n2)O(n^2)O(n2) | O(n)O(n)O(n) | O(n2)O(n^2)O(n2) | O(1)O(1)O(1) | 小规模数据、基本有序数据 |
| 希尔排序 | 不稳定 | O(n1.3)O(n^{1.3})O(n1.3) | O(n)O(n)O(n) | O(n2)O(n^2)O(n2) | O(1)O(1)O(1) | 中等规模数据,优于插入/冒泡排序 |
| 选择排序 | 不稳定 | O(n2)O(n^2)O(n2) | O(n2)O(n^2)O(n2) | O(n2)O(n^2)O(n2) | O(1)O(1)O(1) | 小规模数据,数据移动次数少的场景 |
| 堆排序 | 不稳定 | O(nlogn)O(nlogn)O(nlogn) | O(nlogn)O(nlogn)O(nlogn) | O(nlogn)O(nlogn)O(nlogn) | O(1)O(1)O(1) | 大规模数据,不受数据初始状态影响 |
| 归并排序 | 稳定 | O(nlogn)O(nlogn)O(nlogn) | O(nlogn)O(nlogn)O(nlogn) | O(nlogn)O(nlogn)O(nlogn) | O(n)O(n)O(n) | 大规模数据、链表等非连续存储数据 |
| 基数排序 | 稳定 | O(d×(n+r))O(d\times(n+r))O(d×(n+r)) | O(d×(n+r))O(d\times(n+r))O(d×(n+r)) | O(d×(n+r))O(d\times(n+r))O(d×(n+r)) | O(n+r)O(n+r)O(n+r) | 整数/字符串排序、数据范围较大的场景 |
备注:
- 基数排序中,ddd 为数据的最大位数,rrr 为基数(如十进制 r=10r=10r=10);
- 稳定性定义:排序后相等元素的相对位置不发生改变。
- 到这里我的分享就先结束了~,希望对你有帮助
- 我是dylan 下次见~
- 无限进步