【数据结构与算法-Day 39】插入排序与希尔排序:从 O(n²) 到 O(n^1.3) 的性能飞跃
Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
Python系列文章目录
Go语言系列文章目录
Docker系列文章目录
数据结构与算法系列文章目录
01-【数据结构与算法-Day 1】程序世界的基石:到底什么是数据结构与算法?
02-【数据结构与算法-Day 2】衡量代码的标尺:时间复杂度与大O表示法入门
03-【数据结构与算法-Day 3】揭秘算法效率的真相:全面解析O(n^2), O(2^n)及最好/最坏/平均复杂度
04-【数据结构与算法-Day 4】从O(1)到O(n²),全面掌握空间复杂度分析
05-【数据结构与算法-Day 5】实战演练:轻松看懂代码的时间与空间复杂度
06-【数据结构与算法-Day 6】最朴素的容器 - 数组(Array)深度解析
07-【数据结构与算法-Day 7】告别数组束缚,初识灵活的链表 (Linked List)
08-【数据结构与算法-Day 8】手把手带你拿捏单向链表:增、删、改核心操作详解
09-【数据结构与算法-Day 9】图解单向链表:从基础遍历到面试必考的链表反转
10-【数据结构与算法-Day 10】双向奔赴:深入解析双向链表(含图解与代码)
11-【数据结构与算法-Day 11】从循环链表到约瑟夫环,一文搞定链表的终极形态
12-【数据结构与算法-Day 12】深入浅出栈:从“后进先出”原理到数组与链表双实现
13-【数据结构与算法-Day 13】栈的应用:从括号匹配到逆波兰表达式求值,面试高频考点全解析
14-【数据结构与算法-Day 14】先进先出的公平:深入解析队列(Queue)的核心原理与数组实现
15-【数据结构与算法-Day 15】告别“假溢出”:深入解析循环队列与双端队列
16-【数据结构与算法-Day 16】队列的应用:广度优先搜索(BFS)的基石与迷宫寻路实战
17-【数据结构与算法-Day 17】揭秘哈希表:O(1)查找速度背后的魔法
18-【数据结构与算法-Day 18】面试必考!一文彻底搞懂哈希冲突四大解决方案:开放寻址、拉链法、再哈希
19-【数据结构与算法-Day 19】告别线性世界,一文掌握树(Tree)的核心概念与表示法
20-【数据结构与算法-Day 20】从零到一掌握二叉树:定义、性质、特殊形态与存储结构全解析
21-【数据结构与算法-Day 21】精通二叉树遍历(上):前序、中序、后序的递归与迭代实现
22-【数据结构与算法-Day 22】玩转二叉树遍历(下):广度优先搜索(BFS)与层序遍历的奥秘
23-【数据结构与算法-Day 23】为搜索而生:一文彻底搞懂二叉搜索树 (BST) 的奥秘
24-【数据结构与算法-Day 24】平衡的艺术:图解AVL树,彻底告别“瘸腿”二叉搜索树
25-【数据结构与算法-Day 25】工程中的王者:深入解析红黑树 (Red-Black Tree)
26-【数据结构与算法-Day 26】堆:揭秘优先队列背后的“特殊”完全二叉树
27-【数据结构与算法-Day 27】堆的应用:从堆排序到 Top K 问题,一文彻底搞定!
28-【数据结构与算法-Day 28】字符串查找的终极利器:深入解析字典树 (Trie / 前缀树)
29-【数据结构与算法-Day 29】从社交网络到地图导航,一文带你入门终极数据结构:图
30-【数据结构与算法-Day 30】图的存储:邻接矩阵 vs 邻接表,哪种才是最优选?
31-【数据结构与算法-Day 31】图的遍历:深度优先搜索 (DFS) 详解,一条路走到黑的智慧
32-【数据结构与算法-Day 32】掌握广度优先搜索 (BFS),轻松解决无权图最短路径问题
33-【数据结构与算法-Day 33】最小生成树之 Prim 算法:从零构建通信网络
34-【数据结构与算法-Day 34】最小生成树之 Kruskal 算法:从边的视角构建最小网络
35-【数据结构与算法-Day 35】拓扑排序:从依赖关系到关键路径的完整解析
36-【数据结构与算法-Day 36】查找算法入门:从顺序查找的朴素到二分查找的惊艳
37-【数据结构与算法-Day 37】超越二分查找:探索插值、斐波那契与分块查找的奥秘
38-【数据结构与算法-Day 38】排序算法入门:图解冒泡排序与选择排序,从零掌握 O(n²) 经典思想
39-【数据结构与算法-Day 39】插入排序与希尔排序:从 O(n²) 到 O(n^1.3) 的性能飞跃
文章目录
- Langchain系列文章目录
- Python系列文章目录
- PyTorch系列文章目录
- 机器学习系列文章目录
- 深度学习系列文章目录
- Java系列文章目录
- JavaScript系列文章目录
- Python系列文章目录
- Go语言系列文章目录
- Docker系列文章目录
- 数据结构与算法系列文章目录
- 摘要
- 一、插入排序 (Insertion Sort)
- 1.1 核心思想:像整理扑克牌一样
- 1.2 算法步骤与图解
- 1.3 代码实现 (Java)
- 1.4 复杂度与稳定性分析
- 1.4.1 时间复杂度
- 1.4.2 空间复杂度
- 1.4.3 稳定性
- 1.5 适用场景
- 二、希尔排序 (Shell Sort)
- 2.1 插入排序的困境与希尔的破局之道
- 2.2 核心概念:增量序列 (Gap Sequence)
- 2.3 算法步骤与图解
- 2.4 代码实现 (Java)
- 2.5 复杂度与稳定性分析
- 2.5.1 时间复杂度
- 2.5.2 空间复杂度
- 2.5.3 稳定性
- 三、插入排序 vs. 希尔排序:全面对比
- 四、总结
摘要
在排序算法的家族中,插入排序(Insertion Sort)以其直观易懂的思想占有一席之地,它就像我们日常整理扑克牌一样简单。然而,其 O(n2)O(n^2)O(n2) 的时间复杂度在处理大规模数据时显得力不从心。为了突破这一瓶颈,希尔排序(Shell Sort)应运而生,它通过一种巧妙的“分组插入”策略,将插入排序的性能提升至一个新的量级。本文将深入剖析这两种关联紧密的排序算法,从核心思想、图解步骤、代码实现,到复杂度与稳定性分析,带你领略从朴素思想到精妙优化的算法演进之路。
一、插入排序 (Insertion Sort)
插入排序是一种简单、直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1.1 核心思想:像整理扑克牌一样
想象一下你正在玩扑克牌,需要将手中的牌按点数从小到大整理好。你通常会怎么做?
- 你首先会拿起一张牌,它本身就是有序的。
- 然后,你拿起第二张牌,与第一张牌比较,如果比第一张小,就放在前面;否则,放在后面。现在你手中有两张有序的牌。
- 接着,你拿起第三张牌,从右到左(从大到小)与手中已有的有序牌进行比较,找到它应该插入的位置,然后把它插进去。
- 重复这个过程,直到你手中的所有牌都整理完毕。
插入排序的算法思想与这个过程完全一致。它将待排序的数组分为两个部分:已排序区间和未排序区间。初始时,已排序区间只包含第一个元素。接着,算法每次从未排序区间中取出一个元素,将其在已排序区间中找到一个合适的位置并插入,从而扩大已排序区间,直到未排序区间为空。
1.2 算法步骤与图解
以数组 [5, 2, 4, 6, 1, 3]
为例,我们来图解插入排序的过程。
- 初始状态: 已排序区间
[5]
, 未排序区间[2, 4, 6, 1, 3]
。 - 第一轮:
- 从未排序区间取出第一个元素
2
。 - 将
2
与已排序区间的元素从后向前比较。2 < 5
,5
向后移动一位。 - 将
2
插入到空出的位置。 - 结果:
[2, 5, 4, 6, 1, 3]
。已排序区间[2, 5]
。
- 从未排序区间取出第一个元素
- 第二轮:
- 从未排序区间取出第一个元素
4
。 - 将
4
与已排序区间的元素从后向前比较。4 < 5
,5
向后移动。4 > 2
,找到插入位置。 - 将
4
插入。 - 结果:
[2, 4, 5, 6, 1, 3]
。已排序区间[2, 4, 5]
。
- 从未排序区间取出第一个元素
- 后续轮次: 依次将
6
,1
,3
插入到已排序区间中。
下面用 Mermaid 流程图展示第一轮(将 2
插入 [5]
)的详细逻辑:
graph TDA[开始: arr = [5, 2, 4, ...]] --> B{取出待插入元素 `temp = 2`};B --> C{从已排序区间的末尾 `arr[0]=5` 开始比较};C --> D{`temp(2) < arr[0](5)` ?};D -- 是 --> E[将 `arr[0]` 后移一位: `arr[1] = 5`];E --> F[比较指针前移, 已到头部];F --> G[在空出的位置 `arr[0]` 插入 `temp`];G --> H[完成: arr = [2, 5, 4, ...]];
1.3 代码实现 (Java)
public class InsertionSort {public static void sort(int[] arr) {if (arr == null || arr.length < 2) {return;}// 从第二个元素开始,逐个将其插入到前面已排序的区间for (int i = 1; i < arr.length; i++) {// 记录待插入的元素值int temp = arr[i];// 从已排序区间的末尾开始,向前查找插入位置int j = i - 1;while (j >= 0 && arr[j] > temp) {// 如果当前元素大于待插入元素,则将其后移一位arr[j + 1] = arr[j];j--;}// 循环结束时,j+1 就是 temp 的正确插入位置arr[j + 1] = temp;}}public static void main(String[] args) {int[] arr = {5, 2, 4, 6, 1, 3};sort(arr);System.out.println("Sorted array: " + java.util.Arrays.toString(arr));// 输出: Sorted array: [1, 2, 3, 4, 5, 6]}
}
1.4 复杂度与稳定性分析
1.4.1 时间复杂度
- 最坏情况: 当数组完全逆序时,例如
[6, 5, 4, 3, 2, 1]
。每一轮插入都需要将待插入元素与所有已排序区间的元素进行比较和移动。第i
个元素需要比较i-1
次。总比较次数约为 1+2+...+(n−1)=n(n−1)/21+2+...+(n-1) = n(n-1)/21+2+...+(n−1)=n(n−1)/2。因此,最坏时间复杂度为 O(n2)O(n^2)O(n2)。 - 最好情况: 当数组已经基本有序时,例如
[1, 2, 3, 4, 5, 6]
。每一轮插入都只需要比较一次,因为待插入元素总是比已排序区间的最后一个元素大。总共需要比较n-1
次。此时,时间复杂度为 O(n)O(n)O(n)。 - 平均情况: 在随机数据下,每个元素平均需要移动其前面元素的一半。时间复杂度为 O(n2)O(n^2)O(n2)。
1.4.2 空间复杂度
插入排序是 原地排序 (in-place sort),它只需要一个额外的变量 temp
来暂存待插入的元素,与输入规模 n
无关。因此,空间复杂度为 O(1)O(1)O(1)。
1.4.3 稳定性
插入排序是 稳定 的。稳定性指如果数组中有两个相等的元素,排序后它们的相对位置不发生改变。在插入排序中,当 arr[j] > temp
时才会移动元素,如果 arr[j] == temp
,则循环停止,temp
会被插入到 arr[j]
的后面,从而保证了相等元素的原始顺序。
1.5 适用场景
- 数据规模小: 对于小数组, O(n2)O(n^2)O(n2) 的开销可以接受,且其常数项较小,实现简单。
- 数组基本有序: 这是插入排序最高效的场景,时间复杂度接近 O(n)O(n)O(n)。在很多复杂排序算法的优化中(如 Timsort),当数据分块到足够小时,会切换到插入排序。
二、希尔排序 (Shell Sort)
希尔排序,也称“缩小增量排序”,是插入排序的一种更高效的改进版本。它通过允许元素进行“长距离”交换,来克服插入排序中元素一次只能移动一位的限制。
2.1 插入排序的困境与希尔的破局之道
插入排序的主要瓶颈在于:如果一个很小的元素位于数组的末尾,它需要经过大量的比较和移动才能到达正确的位置。
希尔排序的破局思路是:先将整个待排序的记录序列分割成若干个子序列,分别进行直接插入排序。但这个“分割”不是连续的,而是按一个固定的“增量”(gap
)来选取的。例如,gap=4
,则 arr[0], arr[4], arr[8], ...
构成一组,arr[1], arr[5], arr[9], ...
构成另一组。
通过对这些分组进行插入排序,可以使得一些小元素快速地向数组前端移动,大元素快速地向后端移动。然后,逐步缩小增量 gap
,重复上述过程。当 gap
最终缩小为 1
时,整个数组已经变得“基本有序”,此时再进行一次完整的插入排序(即 gap=1
的希尔排序),效率会非常高。
2.2 核心概念:增量序列 (Gap Sequence)
增量序列的选择是希尔排序性能的关键。一个好的增量序列可以大大减少比较和移动的次数。
- 希尔原始增量: N/2,N/4,...,1N/2, N/4, ..., 1N/2,N/4,...,1。这是最简单的序列,但不是最优的,在某些情况下可能导致效率降低。
- Knuth 增量序列: 1,4,13,40,...1, 4, 13, 40, ...1,4,13,40,... ,通项公式为 h=3×h+1h = 3 \times h + 1h=3×h+1。这个序列被证明性能更好。
- Sedgewick 增量序列: 1,5,19,41,...1, 5, 19, 41, ...1,5,19,41,...,有多种复杂的通项公式。
在实际实现中,通常使用 Knuth 序列或简单的 gap = gap / 2
。
2.3 算法步骤与图解
我们仍然以数组 [5, 2, 4, 6, 1, 3]
为例,使用增量序列 gap=3, 1
来演示。
第一轮: gap = 3
- 数组被分为 3 组:
- 组1:
arr[0], arr[3]
->[5, 6]
- 组2:
arr[1], arr[4]
->[2, 1]
- 组3:
arr[2], arr[5]
->[4, 3]
- 组1:
- 对每组进行插入排序:
[5, 6]
->[5, 6]
(不变)[2, 1]
->[1, 2]
[4, 3]
->[3, 4]
- 排序后,将元素放回原位,数组变为:
[5, 1, 3, 6, 2, 4]
。- 注意:此时较小的元素
1
,2
,3
都已经向前移动了。
- 注意:此时较小的元素
第二轮: gap = 1
- 此时数组为
[5, 1, 3, 6, 2, 4]
,已经“基本有序”。 - 对整个数组进行一次标准的插入排序。
- 将
1
插入[5]
->[1, 5, 3, 6, 2, 4]
- 将
3
插入[1, 5]
->[1, 3, 5, 6, 2, 4]
- … 依次类推
- 将
- 最终结果:
[1, 2, 3, 4, 5, 6]
。
2.4 代码实现 (Java)
public class ShellSort {public static void sort(int[] arr) {if (arr == null || arr.length < 2) {return;}int n = arr.length;// 初始增量 gap,并逐步缩小增量for (int gap = n / 2; gap > 0; gap /= 2) {// 从 gap 个元素开始,逐个对其所在的组进行插入排序for (int i = gap; i < n; i++) {int temp = arr[i];int j = i - gap;// 对 arr[j], arr[j-gap], arr[j-2*gap]... 进行插入排序while (j >= 0 && arr[j] > temp) {arr[j + gap] = arr[j];j -= gap;}arr[j + gap] = temp;}}}public static void main(String[] args) {int[] arr = {5, 2, 4, 6, 1, 3};sort(arr);System.out.println("Sorted array: " + java.util.Arrays.toString(arr));// 输出: Sorted array: [1, 2, 3, 4, 5, 6]}
}
2.5 复杂度与稳定性分析
2.5.1 时间复杂度
希尔排序的时间复杂度分析是数学界的难题,至今没有一个精确的公式。它与所选用的增量序列密切相关。
- 最坏情况: 使用希尔原始增量
N/2
时,最坏时间复杂度为 O(n2)O(n^2)O(n2)。 - 平均情况: 对于一些较优的增量序列(如 Knuth 序列),其时间复杂度可以达到 O(n1.3)O(n^{1.3})O(n1.3) 到 O(n1.5)O(n^{1.5})O(n1.5) 之间。它显著优于 O(n2)O(n^2)O(n2),但劣于 O(nlogn)O(n \log n)O(nlogn) 的高级排序算法(如归并排序、快速排序)。
2.5.2 空间复杂度
希尔排序同样是 原地排序 (in-place sort),只使用了有限的几个辅助变量。空间复杂度为 O(1)O(1)O(1)。
2.5.3 稳定性
希尔排序是 不稳定 的。在不同分组的插入排序过程中,可能会改变相等元素的原始相对位置。
反例: 数组 [5a, 5b, 1]
,其中 5a
和 5b
值相等,但我们用 a
和 b
区分它们。
假设 gap=2
。
- 分组为
[5a, 1]
和[5b]
。 - 对
[5a, 1]
排序,1
和5a
交换位置,得到[1, 5a]
。 - 此时数组变为
[1, 5b, 5a]
。 5a
和5b
的相对顺序被改变了,因此希尔排序是不稳定的。
三、插入排序 vs. 希尔排序:全面对比
特性 | 插入排序 (Insertion Sort) | 希尔排序 (Shell Sort) |
---|---|---|
核心思想 | 将元素逐个插入到已排序的序列中 | 分组进行插入排序,逐步缩小增量 |
时间复杂度 | O(n2)O(n^2)O(n2) (平均/最坏), O(n)O(n)O(n) (最好) | 优于 O(n2)O(n^2)O(n2),约 O(n1.3)O(n^{1.3})O(n1.3) (取决于增量序列) |
空间复杂度 | O(1)O(1)O(1) | O(1)O(1)O(1) |
稳定性 | 稳定 | 不稳定 |
实现复杂度 | 非常简单 | 较简单 |
适用场景 | 数据量小,或数据基本有序 | 中等规模数据,对稳定性无要求 |
四、总结
本文详细探讨了插入排序和希尔排序这两种算法,它们展示了算法优化的一种经典思路:
-
插入排序:是一种基础且直观的排序算法,其核心在于“寻找并插入”。它实现简单,对于小规模或基本有序的数据集表现出色,并且是稳定的。然而,其 O(n2)O(n^2)O(n2) 的时间复杂度限制了它在大规模数据场景下的应用。
-
希尔排序:是插入排序的直接改进版,通过引入“增量序列”或“步长”(gap)的概念,将被动的一步一移变成了高效的“长距离跳跃”,使得元素能更快地到达其最终位置附近。这使得数组在最后进行标准插入排序时,已经处于“基本有序”状态,从而极大地提高了效率。
-
算法的演进: 希尔排序完美诠释了如何通过改变策略(从局部比较到分组宏观调整)来优化一个基础算法。虽然它牺牲了稳定性,但换来了显著的时间性能提升,使其在无需稳定性的中等规模排序任务中成为一个非常有竞争力的选择。
理解这两种算法的关系,不仅有助于我们掌握具体的排序技巧,更能启发我们在面对性能瓶颈时,如何思考和寻找突破口。