目录
[1. 提前终止(已实现)](#1. 提前终止(已实现))
[2. 鸡尾酒排序(双向冒泡)](#2. 鸡尾酒排序(双向冒泡))
[3. 记录最后交换位置](#3. 记录最后交换位置)
[1. 高效的平均性能](#1. 高效的平均性能)
[2. 内存效率高](#2. 内存效率高)
[3. 实现灵活](#3. 实现灵活)
[2. 不稳定排序](#2. 不稳定排序)
[3. 递归实现的风险](#3. 递归实现的风险)
[1. 三数取中法选择基准值](#1. 三数取中法选择基准值)
[2. 尾递归优化](#2. 尾递归优化)
[3. 插入排序混合优化](#3. 插入排序混合优化)
[1. 小规模数据使用插入排序](#1. 小规模数据使用插入排序)
[2. 减少不必要的数组复制](#2. 减少不必要的数组复制)
[3. 提前终止合并过程](#3. 提前终止合并过程)
在数据结构中,排序是组织和处理数据的核心操作之一。它不仅是数据组织的核心手段,更是提升计算效率、优化资源利用的基础。其应用贯穿于数据库、操作系统、机器学习、实时系统等领域,是算法设计与系统优化的基石。
选择适合的排序算法需综合考虑数据规模、内存限制、稳定性需求及硬件特性。
这篇文章详细解析了七大排序的思想、步骤及其特点。
一.直接插入排序
(1)基本思想
直接插入排序是一种简单的排序算法,其基本思想是:将待排序的元素逐个插入到已排序序列的适当位置,直到所有元素都插入完毕。
(2)算法步骤
-
将第一个元素视为已排序序列
-
取出下一个元素,在已排序序列中从后向前扫描
-
如果已排序元素大于新元素,将该元素移到下一位置
-
重复步骤3,直到找到已排序元素小于或等于新元素的位置
-
将新元素插入到该位置
-
重复步骤2~5,直到所有元素都排序完成
(3)代码实现
java
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int j = i - 1;
for (; j >= 0; j--) {
if (array[j] > temp) {
/*array[j+1]就是temp,但是1个i对应很多个j,所以i不是等于j+1;要用j+1来表示temp*/
array[j + 1] = array[j];
} else {
//不用改变元素位置,把取出来的temp再放回去
// array[j+1]=temp;
break;
}
}
/*出了j的循环,证明j这时已经小于0了,也就是-1把取出来的temp再放回去j+1的位置,也就是下标0*/
array[j + 1] = temp;
}
(4)算法特性
特性 | 说明 |
---|---|
时间复杂度 | 最好O(n),最坏和平均O(n²) |
空间复杂度 | O(1)(原地排序) |
稳定性 | 稳定排序 |
适用场景 | 小规模数据或基本有序数据 |
优点 | **(1)**实现简单直观; **(2)**稳定排序,不会改变相同元素的相对顺序; **(3)**原地排序,不需要额外的存储空间(空间复杂度O(1)); **(4)**对小规模或部分有序数据高效; |
缺点 | **(1)**时间复杂度高:平均O(n²); **(2)**对逆序数据表现最差; **(3)**数据移动频繁; |
(5)算法优化
-
二分查找优化:在已排序部分使用二分查找确定插入位置(减少比较次数)
-
希尔排序:是插入排序的改进版,通过分组插入提高效率
(6)示例演示
以数组 [5, 2, 4, 6, 1, 3]
为例:
java
初始: [5, 2, 4, 6, 1, 3]
第1轮: [2, 5, 4, 6, 1, 3] (插入2)
第2轮: [2, 4, 5, 6, 1, 3] (插入4)
第3轮: [2, 4, 5, 6, 1, 3] (插入6)
第4轮: [1, 2, 4, 5, 6, 3] (插入1)
第5轮: [1, 2, 3, 4, 5, 6] (插入3)
直接插入排序虽然简单,但在处理小规模或部分有序数据时效率较高,且实现简单,常被用作其他高级排序算法的子过程。
二.希尔排序
(1)基本思想
希尔排序(Shell Sort)是插入排序的一种高效改进版本,由Donald Shell于1959年提出。其核心思想是通过分组插入排序 逐步减少间隔(增量),最终实现整个数组的有序化。
希尔排序的思想 是通过分组来减少增量,提前进行多次小范围排序,使得数据有序化,最后当gap=1(最后一次排序)时,实现高效率的直接插入排序。
(2)算法步骤
-
选择增量序列:确定一个递减的增量序列(例如初始增量为数组长度的一半,后续逐步减半)。
-
分组插入排序 :对每个增量间隔形成的子序列进行直接插入排序。
-
缩小增量 :重复步骤2,直到增量为1,此时整个数组作为一整个序列进行最后一次插入排序(希尔排序的最后一次排序就是直接插入排序)。
(3)代码实现
java
public static void shellSort(int[] array) {
int gap = array.length / 2;
/*随着gap的递减,分的组数也越来越少,直接组数为1,gap=1,这时进行直接插入排序*/
while (gap > 0) {
shell(array, gap);
gap = gap / 2;
}
}
//希尔排序内排序
public static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int j = i - gap;
for (; j >= 0; j = j - gap) {
if (array[j] > temp) {
array[j + gap] = array[j];
} else {
break;
}
}
//此时j为负数,但不是-1
array[j + gap] = temp;
}
}
希尔排序是优化版的直接插入排序,旨在提升直接插入排序的效率。
并且希尔排序的最后一次排序就是直接插入排序 ,希尔排序之所以效率高是因为希尔排序通过前面几次小范围排序使得数组逐渐有序化(直接插入排序在数据有序化的时候效率高)。
(4)算法特性
| 特性 | 说明 |
| 时间复杂度 | 最坏O(n²),平均约 O(n log n)(
当希尔增量**(n/2递减** )时)
|
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定排序(相同元素可能在排序时改变相对顺序) |
| 适用场景 | 中小规模数据排序(特别是内存受限环境) |
| 优点 | 原地排序,不需要额外的存储空间; 比直接插入排序效率更高,更适合中等规模数据; 灵活性强,可以通过选择不同增量序列来优化性能。 |
缺点 | 不稳定排序,相同元素可能在排序时改变相对顺序; 增量依赖,性能受增量序列的选择影响较大; 理论复杂,最佳增量序列至今尚无统一结论。 |
---|
希尔排序的时间复杂度分析是算法理论中的一个经典难题,其复杂度高度依赖增量序列的选择 ,目前无法精确计算所有情况下的时间复杂度。
(5)算法优化
Sedgewick增量优化:
使用Robert Sedgewick提出的增量序列 实现的希尔排序改进版本。这种增量序列通过数学优化 显著提升了希尔排序的性能,是目前已知综合表现最优的增量序列之一。
java
public static void shellSortOptimized(int[] arr) {
int n = arr.length;
// Sedgewick增量序列(部分)
int[] gaps = {1073, 281, 77, 23, 8, 1};
for (int gap : gaps) {
if (gap >= n) continue;
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
(6)实例演示
java
算法演示(以希尔增量为例)
初始数组:[12, 34, 54, 2, 3, 8, 15, 29]
增量 = 4:
分组:[12,3], [34,8], [54,15], [2,29]
排序后:[3,8,15,2,12,34,54,29]
增量 = 2:
分组:[3,15,12,54], [8,2,34,29]
排序后:[3,2,12,8,15,29,54,34]
增量 = 1:
对整个数组插入排序,最终有序。
希尔排序的时间复杂度并非固定值 ,而是由增量序列决定 。早期资料中的
O(n log n)
是对特定场景的简化描述,现代研究更倾向于具体分析增量序列的影响。实际开发中,选择优化增量序列可显著提升性能,使其在小规模数据排序中具备竞争力。
三.选择排序
(1)基本思想
选择排序是一种简单直观的排序算法,其核心思想是每次从未排序序列中选出最小(或最大)元素,将其放到已排序序列的末尾,直到所有元素排序完成。
(2)算法步骤
-
初始化:整个数组视为未排序序列
-
寻找最小值:遍历未排序序列,找到最小元素的位置
-
交换位置:将最小元素与未排序序列的第一个元素交换
-
缩小范围:将已找到的最小元素归入已排序序列
-
重复操作:重复步骤2~4,直到所有元素有序
(3)代码实现
1)每次交换一个元素(最小元素):
java
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;
}
}
//这时j已经遍历完数组,交换存储的最小值下标元素和array[i]
swap(array, minIndex, i);
}
}
//定义方法:交换数组中两个下标对应的元素
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
2)每次交换两个元素(一个最大元素,一个最小元素):
java
public static void selectSort2(int[] array) {
int left = 0;
int right = array.length - 1;
while (left < right) {
int minIndex = left;
int maxIndex = left;
for (int i = left + 1; i <= right; i++) {
if (array[i] > array[maxIndex]) {
maxIndex = i;
} else if (array[i] < array[minIndex]) {
minIndex = i;
}
}
//走完i循环,交换元素
swap(array, minIndex, left);
if (maxIndex == left) {
swap(array, left, right);
} else {
swap(array, maxIndex, right);
}
left++;
right--;
}
}
(4)算法特性
| 特性 | 说明 |
| 时间复杂度 | 固定 O(n²)(无论数据是否有序) |
| 空间复杂度 | O(1)(原地排序) |
| 稳定性 | 通常不稳定(可特殊实现为稳定) |
| 交换次数 | 最多 n-1 次交换 |
| 比较次数 | 固定 n(n−1)22n(n−1) 次 |
| 优点 | 1. 简单直观:逻辑清晰,易于理解和实现 2. 交换次数少:每轮仅需一次交换(相比冒泡排序更高效) 3. 内存友好:原地排序,无需额外空间 4. 适用小数据:数据量较小时实际性能尚可 |
| 缺点 | 1. 效率低下:时间复杂度始终为 O(n²),不适合大规模数据 2. 无适应性:无论数据初始状态如何,比较次数固定 3. 不稳定排序:默认实现会改变相同元素的相对顺序 |
(5)算法优化
-
双向选择排序(鸡尾酒选择排序)
-
同时寻找最小值和最大值,减少循环次数
-
优化后比较次数减少约50%
-
-
堆排序优化
-
使用堆结构优化选择过程,时间复杂度降为 O(n log n)
-
实际上堆排序就是选择排序的高级变种
-
(6)实例演示
java
初始状态: [5, 2, 4, 6, 1, 3]
第1轮:找到最小值1 → 交换位置 → [1, 2, 4, 6, 5, 3]
第2轮:找到次小值2 → 无需交换 → [1, 2, 4, 6, 5, 3]
第3轮:找到最小值3 → 交换位置 → [1, 2, 3, 6, 5, 4]
第4轮:找到最小值4 → 交换位置 → [1, 2, 3, 4, 5, 6]
第5轮:数组已有序,排序完成
四.堆排序
(1)基本思想
堆排序是一种基于完全二叉树结构的高效排序算法,利用堆的性质进行排序。其核心思想是:
-
构建最大堆(或最小堆),将无序数组转换为堆结构
-
逐步取出堆顶元素(最大值或最小值),与堆末尾元素交换并调整堆
-
重复调整直到堆大小为1,完成排序
(2)算法步骤
-
构建最大堆
从最后一个非叶子节点开始,自底向上调整堆结构
-
堆排序阶段
-
交换堆顶与末尾元素(最大值归位)
-
缩小堆范围并重新调整堆
-
重复直到堆大小为1
-
(3)代码实现
java
public static void heapSort(int[] array) {
createHeap(array);
for (int end = array.length - 1; end >= 0; end--) {
//交换堆顶元素和最后一个元素
swap(array, 0, end);
//调整剩余元素为大根堆
siftDown(array, 0, end);
}
}
//定义方法:创建1个大根堆
public static void createHeap(int[] array) {
//确定最后1棵子树的位置
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
//siftDown是将1棵子树调整为大根堆,所以要使parent--,调整所有的子树至大根堆
siftDown(array, parent, array.length);
}
}
/*定义方法:向下调整;将1个堆调整为大根堆在数组array中向下调整到length位置*/
public static void siftDown(int[] array, int parent, int length) {
int child = 2 * parent + 1;
while (child < length) {
//拿到左右孩子最大值
if (child < array.length - 1 && array[child] >= array[child + 1]) {
child++;
}
//如果孩子最大值大于父亲节点,交换两个节点的值
if (array[child] > array[parent]) {
swap(array, child, parent);
//继续向下调整
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
(4)算法特性
| 特性 | 说明 |
| 时间复杂度 | O(n log n) |
| 空间复杂度 | O(1)(原地排序) |
| 稳定性 | 不稳定 |
| 优点 | 1. 时间复杂度稳定:始终保证O(n log n)的性能 2. 内存高效:原地排序,无需额外存储空间 3. 适合大数据:处理海量数据时不会出现快速排序的最坏情况 4. 优先级队列基础:堆结构的重要应用场景 |
缺点 | 1. 不稳定排序:相同值元素的相对位置可能改变 2. 缓存不友好:跳跃式访问内存,可能影响实际性能 3. 常数项较大:实际运行速度通常略慢于快速排序 |
---|
(5)算法优化
-
非递归实现
将调整方法改为迭代实现,避免递归调用开销
-
多叉堆优化
使用三叉堆或四叉堆(适用于现代CPU缓存特性)
-
并行建堆
对大规模数据可采用多线程并行调整子树
(6)实例演示
(以数组[12, 11, 13, 5, 6, 7]为例)
1)建堆过程:
java
初始数组: [12, 11, 13, 5, 6, 7]
转换为完全二叉树:
12
/ \
11 13
/ \ /
5 6 7
调整非叶子节点(索引2→1→0):
最终最大堆: [13, 11, 12, 5, 6, 7]
对应二叉树:
13
/ \
11 12
/ \ /
5 6 7
2)排序阶段
java
第1次交换:13↔7 → [7, 11, 12, 5, 6, 13]
调整堆: [12, 11, 7, 5, 6, 13]
第2次交换:12↔6 → [6, 11, 7, 5, 12, 13]
调整堆: [11, 6, 7, 5, 12, 13]
...(重复过程)...
最终结果: [5, 6, 7, 11, 12, 13]
五.冒泡排序
(1)基本思想
冒泡排序是一种简单的交换排序算法,其核心思想是通过相邻元素的比较和交换,将较大的元素逐步"冒泡"到数组末尾。每一轮遍历都会确定一个当前未排序部分的最大值。
(2)算法步骤
-
外层循环 :控制排序轮数(共需
n-1
轮,n
为数组长度) -
内层循环:遍历未排序部分,比较相邻元素
-
元素交换:若当前元素 > 后一个元素,则交换位置
-
优化判断:若某轮无交换发生,提前终止排序
(3)代码实现
java
public class BubbleSort {
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length <= 1) return;
int n = arr.length;
boolean swapped; // 优化标志
for (int i = 0; i < n - 1; i++) {
swapped = false;
// 每轮确定一个最大值,遍历范围逐渐缩小
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
// 若本轮无交换,说明已有序,提前结束
if (!swapped) break;
}
}
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // 输出 [11, 12, 22, 25, 34, 64]
}
}
(4)算法特性
特性 | 说明 |
---|---|
时间复杂度 | 最好O(n),最坏和平均O(n²) |
空间复杂度 | O(1)(原地排序) |
稳定性 | 稳定排序 |
交换次数 | 最多 n(n−1)22n(n−1) 次 |
优点 | 1. 实现简单:代码逻辑直观,适合教学和小规模数据 2. 稳定性:相等元素不会交换,保持原有顺序 3. 空间效率:无需额外内存空间 4. 适应性:对部分有序数据效率较高(通过优化标志) |
缺点 | 1. 效率低下:大规模数据排序速度显著下降 2. 冗余比较:即便数据已有序仍需多轮遍历(未优化版本) |
(5)算法优化
1. 提前终止(已实现)
-
通过
swapped
标志检测是否发生交换 -
最佳情况时间复杂度优化至O(n)(完全有序数组)
2. 鸡尾酒排序(双向冒泡)
-
交替进行正向和反向遍历
-
对包含大量无序小元素的数组更高效
鸡尾酒排序代码实例:
java
public static void cocktailSort(int[] arr) {
int left = 0;
int right = arr.length - 1;
boolean swapped;
while (left < right) {
// 正向遍历
swapped = false;
for (int i = left; i < right; i++) {
if (arr[i] > arr[i+1]) {
swap(arr, i, i+1);
swapped = true;
}
}
if (!swapped) break;
right--;
// 反向遍历
swapped = false;
for (int i = right; i > left; i--) {
if (arr[i] < arr[i-1]) {
swap(arr, i, i-1);
swapped = true;
}
}
if (!swapped) break;
left++;
}
}
3. 记录最后交换位置
- 记录每轮最后一次交换的位置,减少无效比较
java
int lastSwapIndex = 0;
int sortBorder = arr.length - 1;
for (int i = 0; i < arr.length - 1; i++) {
boolean swapped = false;
for (int j = 0; j < sortBorder; j++) {
if (arr[j] > arr[j+1]) {
swap(arr, j, j+1);
swapped = true;
lastSwapIndex = j;
}
}
sortBorder = lastSwapIndex;
if (!swapped) break;
}
(6)实例演示
(以数组[5, 2, 4, 6, 1, 3]为例)
java
初始状态: [5, 2, 4, 6, 1, 3]
第1轮遍历:
2 5 4 6 1 3 → 2 4 5 6 1 3 → 2 4 5 1 6 3 → 2 4 5 1 3 6
确定最大值6归位
第2轮遍历:
2 4 5 1 3 → 2 4 1 5 3 → 2 4 1 3 5
确定次大值5归位
第3轮遍历:
2 4 1 3 → 2 1 4 3 → 2 1 3 4
确定4归位
第4轮遍历:
2 1 3 → 1 2 3
确定3归位(优化:此时已有序,提前结束)
六.快速排序
(1)基本思想
快速排序是一种高效的分治算法 ,核心思想是通过基准值(pivot)划分数组,将小于基准值的元素放在左侧,大于基准值的元素放在右侧,然后递归处理左右子数组,直到整个数组有序。
(2)算法步骤
-
选择基准值(Pivot):从数组中选取一个元素作为基准
-
分区操作(Partition):重新排列数组,使小于基准值的元素在左,大于基准值的在右
-
递归排序:对左右两个子数组递归执行上述步骤
(3)代码实现
java
public class QuickSort {
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) return;
sort(arr, 0, arr.length - 1);
}
private static void sort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
sort(arr, low, pivotIndex - 1); // 递归处理左子数组
sort(arr, pivotIndex + 1, high); // 递归处理右子数组
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素为基准值
int i = low - 1; // 指向比基准值小的最后一个元素
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high); // 将基准值放到正确位置
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {10, 7, 8, 9, 1, 5};
quickSort(arr);
System.out.println(Arrays.toString(arr)); // 输出 [1, 5, 7, 8, 9, 10]
}
}
(4)算法特性
特性 | 说明 |
---|---|
平均时间复杂度 | O(n log n) |
最坏时间复杂度 | O(n²)(可通过优化避免) |
空间复杂度 | O(log n)(递归调用栈) |
稳定性 | 不稳定 |
最佳适用场景 | 大规模随机数据 |
优点 | #### 1. 高效的平均性能 * 时间复杂度:平均情况O(n log n),实际应用中效率通常高于其他O(n log n)算法(如归并排序) * 比较次数少:相比归并排序,快速排序的比较次数通常更少 #### 2. 内存效率高 * 原地排序:只需O(log n)的栈空间(递归调用),不需要额外存储空间 * 缓存友好:顺序访问内存,能有效利用CPU缓存 #### 3. 实现灵活 * 可通过多种方式选择基准值(pivot) * 可优化为三路快排处理重复元素 * 可与非递归实现结合 |
缺点 | #### 最坏情况性能差 * 最坏时间复杂度:O(n²)(当输入已排序且基准选择不当时) * 解决方案:随机化基准选择或三数取中法 #### 2. 不稳定排序 * 相同元素的相对位置可能改变 * 示例 :排序[3①, 2, 3②] 可能得到[2, 3②, 3①] #### 3. 递归实现的风险 * 深度递归可能导致栈溢出 * 解决方案:尾递归优化或改为迭代实现 |
(5)算法优化
1. 三数取中法选择基准值
避免最坏情况(已排序数组导致O(n²))
java
private static int selectPivot(int[] arr, int low, int high) {
int mid = low + (high - low)/2;
// 比较low/mid/high三个位置的元素
if (arr[low] > arr[mid]) swap(arr, low, mid);
if (arr[low] > arr[high]) swap(arr, low, high);
if (arr[mid] > arr[high]) swap(arr, mid, high);
return mid; // 返回中间值索引
}
2. 尾递归优化
减少递归调用栈深度
java
private static void sortOptimized(int[] arr, int low, int high) {
while (low < high) {
int pivotIndex = partition(arr, low, high);
if (pivotIndex - low < high - pivotIndex) {
sortOptimized(arr, low, pivotIndex - 1);
low = pivotIndex + 1;
} else {
sortOptimized(arr, pivotIndex + 1, high);
high = pivotIndex - 1;
}
}
}
3. 插入排序混合优化
对小规模子数组(如长度≤15)改用插入排序
java
private static final int INSERTION_THRESHOLD = 15;
private static void sort(int[] arr, int low, int high) {
if (high - low <= INSERTION_THRESHOLD) {
insertionSort(arr, low, high);
return;
}
// ...原快速排序逻辑
}
(6)实例演示
示例1:基础快速排序流程
输入数组 :[10, 80, 30, 90, 40, 50, 70]
步骤演示:
-
选择基准值:70(末尾元素)
-
分区过程:
-
10 < 70 → 保留
-
80 > 70 → 跳过
-
30 < 70 → 与80交换 →
[10, 30, 80, 90, 40, 50, 70]
-
90 > 70 → 跳过
-
40 < 70 → 与80交换 →
[10, 30, 40, 90, 80, 50, 70]
-
50 < 70 → 与90交换 →
[10, 30, 40, 50, 80, 90, 70]
-
-
最终交换基准值 →
[10, 30, 40, 50, 70, 90, 80]
-
递归处理左右子数组
示例2:三数取中法优化
输入数组 :[1, 2, 3, 4, 5, 6, 7]
(已排序数组)
优化步骤:
-
选择low=1, mid=4, high=7
-
三数排序后取中值:4
-
分区后得到平衡划分,避免O(n²)最坏情况
示例3:三路快排处理重复元素
输入数组 :[3, 1, 3, 2, 3, 3, 4]
分区过程:
java
初始:lt=0, gt=6, i=1, pivot=3
[3, 1, 3, 2, 3, 3, 4]
步骤1:arr[1]=1 < 3 → 交换lt(0)和i(1)
[1, 3, 3, 2, 3, 3, 4] (lt=1, i=2)
步骤2:arr[2]=3 == 3 → i++
(lt=1, i=3)
步骤3:arr[3]=2 < 3 → 交换lt(1)和i(3)
[1, 2, 3, 3, 3, 3, 4] (lt=2, i=4)
...最终得到:
<3的部分:[1, 2]
=3的部分:[3, 3, 3, 3]
>3的部分:[4]
七.归并排序
(1)基本思想
归并排序(Merge Sort)是一种经典的分治算法,由约翰・冯・诺伊曼在 1945 年提出。它的基本思想是将一个大问题分解为多个小问题,分别解决这些小问题,最后将小问题的解合并起来得到大问题的解。
归并排序采用分治策略,具体分为两个阶段:
- 分解(Divide):将待排序的数组从中间分成两个子数组,然后递归地对这两个子数组继续进行分解,直到每个子数组只有一个元素(因为单个元素的数组本身就是有序的)。
- 合并(Merge):将两个有序的子数组合并成一个有序的数组。合并过程中,比较两个子数组的元素,将较小的元素依次放入新的数组中,直到两个子数组的所有元素都被放入新数组。
(2)算法步骤
- 分解阶段 :
- 找到数组的中间位置,将数组分成左右两部分。
- 递归地对左右两部分进行分解,直到每个子数组只有一个元素。
- 合并阶段 :
- 创建一个临时数组,用于存放合并后的结果。
- 比较左右两个子数组的元素,将较小的元素依次放入临时数组中。
- 将临时数组中的元素复制回原数组。
(3)代码实现
java
private static void mergechild(int[] array, int left, int right) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
mergechild(array, left, mid);
mergechild(array, mid + 1, right);
merge(array,left,mid,right);
}
//定义方法:合并两个子数组
private static void merge(int[] array, int left, int mid, int right) {
int[] tempArray = new int[right - left + 1];
int k = 0;//临时数组tempArray的下标
int start1 = left;
int start2 = mid + 1;
int end1 = mid;
int end2 = right;
//两个子数组中都有数据
while (start1 <= end1 && start2 <= end2) {
if (array[start1] > array[start2]) {
tempArray[k] = array[start2];
k++;
start2++;
} else if (array[start1] <= array[start2]) {
tempArray[k] = array[start1];
k++;
start1++;
}
}
//1个子数组中没有数据了,跳出while循环
while (start1 <= end1) {
tempArray[k] = array[start1];
k++;
start1++;
}
while (start2 <= end2) {
tempArray[k] = array[start2];
k++;
start2++;
}
//将tempArray中的元素复制回原数组
for (int i = 0; i < tempArray.length; i++) {
array[i+left]=tempArray[i];
}
}
(4)算法特性
| 特性 | 说明 |
| 平均时间复杂度 | O(nlogn) |
| 空间复杂度 | O(n) |
| 稳定性 | 稳定 |
| 最佳适用场景 | 处理大规模数据 |
| 优点 | * 稳定性:归并排序是一种稳定的排序算法,即相等元素的相对顺序在排序前后不会改变。 * 时间复杂度稳定:无论输入数据的分布如何,归并排序的时间复杂度都是 O(nlogn)。 |
缺点 | * 空间复杂度较高:需要额外的 O(n) 空间来存储临时数组。 * 常数因子较大:由于需要频繁地进行数组的复制和合并操作,归并排序的常数因子相对较大,在处理小规模数据时效率不如一些简单的排序算法(如插入排序)。 |
---|
(5)算法优化
1. 小规模数据使用插入排序
对于小规模的数据,插入排序的常数时间开销相对较小,性能可能比归并排序更好。因此当子数组规模较小时,可以采用插入排序来处理,减少递归调用带来的开销。
2. 减少不必要的数组复制
在归并过程中,频繁地创建和复制临时数组会带来一定的性能开销。可以通过交替使用原数组和临时数组来减少这种开销。
3. 提前终止合并过程
在合并两个有序子数组时,如果发现其中一个子数组的所有元素都已经小于另一个子数组的所有元素,就可以提前终止合并过程。
(6)实例演示
假设我们有一个待排序的数组 [38, 27, 43, 3, 9, 82, 10]
分解阶段
- 首先将原数组从中间分成两部分:
[38, 27, 43, 3]
和[9, 82, 10]
。 - 对这两个子数组继续分解,
[38, 27, 43, 3]
分成[38, 27]
和[43, 3]
;[9, 82, 10]
分成[9, 82]
和[10]
。 - 继续分解,
[38, 27]
分成[38]
和[27]
;[43, 3]
分成[43]
和[3]
;[9, 82]
分成[9]
和[82]
。此时所有子数组都只有一个元素,分解结束。
合并阶段
- 合并
[38]
和[27]
得到[27, 38]
;合并[43]
和[3]
得到[3, 43]
;合并[9]
和[82]
得到[9, 82]
。 - 合并
[27, 38]
和[3, 43]
得到[3, 27, 38, 43]
;[9, 82]
和[10]
合并得到[9, 10, 82]
。 - 最后合并
[3, 27, 38, 43]
和[9, 10, 82]
得到最终的有序数组[3, 9, 10, 27, 38, 43, 82]
。
八.常见排序表
排序算法 | 时间复杂度(最好) | 时间复杂度(平均) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 | 备注 |
---|---|---|---|---|---|---|
冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 简单但效率低 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 交换次数最少 |
插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 对小规模数据高效 |
快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 | 实际应用中最快(优化后) |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 稳定且适合外部排序 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 适合大规模数据 |
希尔排序 | O(n log n) | O(n log² n) | O(n²) | O(1) | 不稳定 | 插入排序的改进版 |