数据结构(20)
目录
插入排序
3、表插入排序
(1)核心思想
(2)具体步骤(示例演示)
(3)算法实现(单链表代码示例)
(4)算法特性
(5)与直接插入排序(数组)的对比
4、希尔(shell)排序
(1)核心思想
(2)增量序列的选择
(3)具体步骤(示例演示)
(4)算法实现(代码示例,Shell 增量)
(5)算法特性
(6)与直接插入排序的对比
插入排序
3、表插入排序
表插入排序是直接插入排序在链表(线性表的链式存储结构) 上的实现,核心思想与直接插入排序一致 —— 将待排序元素逐个插入到已排序部分的合适位置,但利用链表的指针特性避免了直接插入排序中大量的元素后移操作,更适合链式存储的场景。
(1)核心思想
- 初始状态:将链表的第一个节点视为 “已排序部分”(仅含一个节点,天然有序),剩余节点组成 “待排序部分”。
- 逐个插入:从待排序部分的第一个节点开始,依次取出每个节点(称为 “当前节点”),与已排序部分的节点从前往后(或从后往前)比较,找到其在已排序部分中的正确位置。
- 调整指针:通过修改链表的指针,将当前节点插入到正确位置,使已排序部分始终保持有序。由于链表的节点物理地址不连续,插入时无需移动元素,只需调整前后节点的指针即可。
(2)具体步骤(示例演示)
以单链表 5 → 3 → 8 → 4 → 2(箭头表示 next 指针)为例,排序目标是升序,步骤如下:
1. 初始状态
- 已排序部分:
5(头节点为5)。 - 待排序部分:
3 → 8 → 4 → 2(当前待插入节点为3)。
2. 插入节点 3
- 比较
3与已排序部分的5:3 < 5,需插入到5之前。 - 调整指针:将
3的next指向5,已排序部分变为3 → 5。 - 待排序部分剩余:
8 → 4 → 2。
3. 插入节点 8
- 比较
8与已排序部分的3:8 > 3,继续比较下一个节点5:8 > 5,需插入到5之后。 - 调整指针:
5的next指向8,8的next为None,已排序部分变为3 → 5 → 8。 - 待排序部分剩余:
4 → 2。
4. 插入节点 4
- 比较
4与3:4 > 3,继续比较5:4 < 5,需插入到3与5之间。 - 调整指针:
3的next指向4,4的next指向5,已排序部分变为3 → 4 → 5 → 8。 - 待排序部分剩余:
2。
5. 插入节点 2
- 比较
2与3:2 < 3,需插入到3之前。 - 调整指针:
2的next指向3,已排序部分变为2 → 3 → 4 → 5 → 8。 - 待排序部分为空,排序结束。
例子:

(3)算法实现(单链表代码示例)
class ListNode:def __init__(self, val):self.val = valself.next = Nonedef list_insertion_sort(head):if not head or not head.next:return head # 空链表或单个节点无需排序# 已排序部分的头节点(初始为第一个节点)sorted_head = head# 待排序部分的第一个节点(初始为第二个节点)current = head.nextsorted_head.next = None # 断开已排序部分与待排序部分的连接while current:# 保存下一个待排序节点next_node = current.next# 寻找插入位置:prev是插入位置的前一个节点prev = Nonecurr_sorted = sorted_head# 遍历已排序部分,找到第一个大于current.val的节点while curr_sorted and curr_sorted.val <= current.val:prev = curr_sortedcurr_sorted = curr_sorted.next# 插入current到已排序部分if prev is None:# 插入到已排序部分的头部current.next = sorted_headsorted_head = currentelse:# 插入到prev与curr_sorted之间prev.next = currentcurrent.next = curr_sorted# 处理下一个待排序节点current = next_nodereturn sorted_head# 测试:构建链表 5→3→8→4→2
head = ListNode(5)
head.next = ListNode(3)
head.next.next = ListNode(8)
head.next.next.next = ListNode(4)
head.next.next.next.next = ListNode(2)# 排序
sorted_head = list_insertion_sort(head)# 输出排序结果
result = []
while sorted_head:result.append(sorted_head.val)sorted_head = sorted_head.next
print(result) # 输出:[2, 3, 4, 5, 8]
(4)算法特性
-
时间复杂度:
- 比较次数:与直接插入排序相同,最坏和平均情况均为 O(n2)(需逐个比较已排序部分的节点)。
- 移动次数:无需移动元素,仅需调整指针(O(1) 操作),因此实际效率优于数组上的直接插入排序(尤其数据量大时,避免了大量元素后移的开销)。
- 整体复杂度:O(n2)。
-
空间复杂度:仅需常数级额外空间(存储指针变量),空间复杂度为 O(1)(原地排序,通过修改指针实现)。
-
稳定性:稳定排序。当待插入节点与已排序节点的值相等时,会插入到相等节点的后面(因比较条件为
<=),保持原有相对顺序。 -
适用场景:
- 链式存储结构(单链表、双链表),不适合顺序表(数组)。
- 数据量较小或接近有序的链表(大规模数据时 O(n2) 复杂度仍效率较低)。
- 需频繁插入元素的场景(链表插入操作代价低)。
(5)与直接插入排序(数组)的对比
| 对比维度 | 直接插入排序(数组) | 表插入排序(链表) |
|---|---|---|
| 存储结构 | 顺序表(数组) | 链表 |
| 插入操作 | 需移动大量元素(O(n)) | 仅调整指针(O(1)) |
| 比较次数 | O(n2) | O(n2) |
| 空间开销 | 低(随机访问) | 稍高(需存储指针) |
| 适用场景 | 数组、小规模或接近有序数据 | 链表、需频繁插入的场景 |
表插入排序是直接插入排序在链表上的适配实现,利用链表的指针特性消除了元素后移的开销,在链式存储场景中效率优于数组上的直接插入排序。其核心仍是 “逐个插入构建有序序列”,但通过指针操作优化了插入过程,适合处理链式结构的小规模或接近有序数据。
4、希尔(shell)排序
希尔排序(Shell Sort)是对直接插入排序的改进,由 Donald Shell 于 1959 年提出。其核心思想是通过 “分组插入排序” 逐步缩小间隔(增量),使序列整体逐渐接近有序,最后再进行一次直接插入排序(间隔为 1)。这种 “先宏观调整,再微观优化” 的策略,大幅提升了插入排序的效率,尤其适合中等规模数据的排序。
(1)核心思想
- 分组排序:将整个序列按某个间隔(增量)gap 分成若干子序列,每个子序列由 “间隔为gap的元素” 组成(如gap=3时,子序列为(a0,a3,a6,...)、(a1,a4,a7,...)等)。
- 逐组插入:对每个子序列分别执行直接插入排序,使每组内的元素初步有序。
- 缩小间隔:减小增量gap(如取gap=gap/2),重复步骤 1-2,直到gap=1。
- 最终排序:当gap=1时,整个序列被视为一个子序列,执行一次直接插入排序,此时序列已接近有序,插入排序效率极高。
(2)增量序列的选择
增量gap的选择直接影响希尔排序的效率,常见的增量序列有:
- Shell 初始增量:gap=n/2,gap=gap/2,直至gap=1(简单但效率一般)。
- Hibbard 增量:gap=2k−1(如 1,3,7,15,...),效率优于 Shell 增量。
- Sedgewick 增量:gap=4k+3×2k−1+1(如 1,5,19,41,...),在实践中效率较高。
注:增量序列需满足 “最后一个增量必须为 1”,且通常 “增量互质”(避免重复排序)。
(3)具体步骤(示例演示)
以序列 [10, 1, 9, 2, 8, 3, 7, 4, 6, 5](n=10)为例,采用 Shell 初始增量(gap=5→2→1):
1. 第一趟:gap=5(分组排序)
- 分组规则:间隔 5 取元素,共分成 5 组:组 1:
[10, 3]、组 2:[1, 7]、组 3:[9, 4]、组 4:[2, 6]、组 5:[8, 5]。 - 每组执行直接插入排序:组 1:
[3, 10]、组 2:[1, 7]、组 3:[4, 9]、组 4:[2, 6]、组 5:[5, 8]。 - 排序后序列:
[3, 1, 4, 2, 5, 10, 7, 9, 6, 8](整体更接近有序)。
2. 第二趟:gap=2(分组排序)
- 分组规则:间隔 2 取元素,共分成 2 组:组 1:
[3, 4, 5, 7, 6](索引 0,2,4,6,8)、组 2:[1, 2, 10, 9, 8](索引 1,3,5,7,9)。 - 每组执行直接插入排序:组 1:
[3, 4, 5, 6, 7]、组 2:[1, 2, 8, 9, 10]。 - 排序后序列:
[3, 1, 4, 2, 5, 8, 6, 9, 7, 10](有序度进一步提升)。
3. 第三趟:gap=1(直接插入排序)
- 此时序列已接近有序,执行一次直接插入排序:最终结果:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]。
例子:

(4)算法实现(代码示例,Shell 增量)
def shell_sort(arr):n = len(arr)gap = n // 2 # 初始增量while gap > 0:# 对每个子序列执行直接插入排序for i in range(gap, n):key = arr[i] # 待插入元素j = i - gap # 子序列中前一个元素的索引# 子序列内比较并后移while j >= 0 and key < arr[j]:arr[j + gap] = arr[j]j -= gap# 插入待排序元素arr[j + gap] = keygap = gap // 2 # 缩小增量return arr# 测试
arr = [10, 1, 9, 2, 8, 3, 7, 4, 6, 5]
print(shell_sort(arr)) # 输出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
(5)算法特性
-
时间复杂度:
- 时间复杂度与增量序列密切相关,目前尚未找到最优增量序列。
- 最坏情况:Shell 增量下为 O(n2),Hibbard 增量下为 O(n3/2),Sedgewick 增量下接近 O(nlog2n)。
- 平均情况:通常认为在 O(n1.3) 左右,优于直接插入排序的 O(n2)。
-
空间复杂度:仅需常数级额外空间(存储
key、gap等),空间复杂度为 O(1)(原地排序)。 -
稳定性:不稳定排序。因分组排序时,相同元素可能被分到不同组,插入过程中可能改变相对顺序(如
[2, 1, 2]以 gap=2 排序时,第一个2会被分到组 1,第二个2分到组 2,最终顺序可能颠倒)。 -
适用场景:
- 中等规模数据(n 为几千到几万),效率优于简单排序(插入、冒泡)。
- 顺序表(数组)结构(依赖随机访问,不适合链表,因分组时需按间隔访问元素)。
(6)与直接插入排序的对比
| 对比维度 | 直接插入排序 | 希尔排序 |
|---|---|---|
| 核心策略 | 逐元素插入,整体一次排序 | 分组插入,多轮缩小间隔 |
| 时间复杂度 | O(n2) | O(n1.3)∼O(n2) |
| 稳定性 | 稳定 | 不稳定 |
| 适用数据规模 | 小规模(n 较小) | 中等规模(n 中等) |
| 有序度影响 | 对接近有序数据效率高 | 对无序数据效率提升明显 |
希尔排序通过 “分组缩小增量” 的策略,突破了直接插入排序在无序数据上的效率瓶颈,是一种 “半突破 O(n2)” 的排序算法。其核心是利用 “远距离元素先排序” 减少后续插入排序的移动次数,在中等规模数据排序中表现优异。尽管稳定性不足且增量选择复杂,但其高效性使其成为实践中常用的排序算法之一。
