排序算法进阶:直接插入排序(简单排序)与希尔排序

从直接插入排序到希尔排序:深入理解两类经典排序算法

在数据排序的众多算法中,有一类算法被称为"简单排序"。它们通常易于理解、代码实现简单,非常适合作为学习排序算法的入门选择。然而,这些简单排序算法(如冒泡排序、选择排序、直接插入排序)的时间复杂度普遍为 O(n²),在处理大规模数据时效率较低。其中,直接插入排序 因其在数据基本有序时表现优异而备受关注。希尔排序 则是在直接插入排序基础上进行改进的一种更高效的排序算法,它通过"分组插入"的方式,显著提升了排序的整体性能。本文将详细且深入地介绍这两种排序算法的原理、步骤、代码实现以及复杂度分析,并通过丰富的示例帮助你彻底掌握它们。


一、直接插入排序(Insertion Sort)

算法思想

直接插入排序是一种简单直观的排序算法,它的思想与我们平时整理手牌或整理书架的过程高度相似:每次将一个新元素"插入"到已经排好序的序列中的正确位置,从而不断扩展已排序部分,直到整个数组有序。

具体来说,我们把数组看作两个区域:左侧的已排序区域右侧的未排序区域 。初始时,已排序区域只包含第一个元素(因为单个元素自然有序)。然后,我们依次从未排序区域取出第一个元素(称为"待插入元素"或 key),将它插入到已排序区域中的合适位置,同时将大于它的元素依次向后移动一位,为它腾出空间。重复这个过程,直到未排序区域为空。

算法步骤(以升序为例)
  1. 从数组的第二个元素开始(下标 i = 1),依次遍历每个元素,将其视为待插入元素 key
  2. key 与它前面的元素(下标 j = i-1, i-2, ..., 0)从右向左逐一比较。
  3. 如果前面的元素 arr[j] 大于 key,则将该元素向后移动一位(arr[j+1] = arr[j]),然后继续向左比较(j--)。
  4. 当遇到一个小于等于 key 的元素或者已经比较到数组开头(j < 0)时,停止移动。
  5. key 放入空出来的位置,即 arr[j+1] = key
  6. 继续处理下一个 i,直到所有元素都被插入。
详细示例演示

我们以数组 [5, 2, 4, 6, 1, 3] 为例,逐步展示每一轮插入的过程。为了方便理解,我们用 | 分隔已排序部分和未排序部分。

  • 初始状态[5] | [2, 4, 6, 1, 3]

    已排序部分只有第一个元素 5

  • 第1轮(i=1,key=2)

    比较 key=2 与它前面的元素 5:由于 5 > 2,将 5 后移一位。此时数组变为 [5, 5, 4, 6, 1, 3](注意两个 5 是临时状态)。j 减为 -1,退出循环。将 key 放入 j+1 = 0 位置,得到 [2, 5] | [4, 6, 1, 3]

  • 第2轮(i=2,key=4)

    比较 key=4 与前面元素:先与 5 比较,5 > 4,将 5 后移一位,数组变为 [2, 5, 5, 6, 1, 3]。然后 j 减为 0,比较 arr[0]=242 <= 4,停止。将 key 放入 j+1 = 1 位置,得到 [2, 4, 5] | [6, 1, 3]

  • 第3轮(i=3,key=6)

    比较 key=6 与前面元素:先与 5 比较,5 < 6,无需移动。直接停止,将 6 放在原位置(实际上没有变化)。此时数组为 [2, 4, 5, 6] | [1, 3]

  • 第4轮(i=4,key=1)

    比较 key=1 与前面元素:从右向左依次与 6, 5, 4, 2 比较。

    • 6 > 1 → 将 6 后移一位,数组 [2, 4, 5, 6, 6, 3]
    • 5 > 1 → 将 5 后移一位,数组 [2, 4, 5, 5, 6, 3]
    • 4 > 1 → 将 4 后移一位,数组 [2, 4, 4, 5, 6, 3]
    • 2 > 1 → 将 2 后移一位,数组 [2, 2, 4, 5, 6, 3]
    • 到达开头,j = -1,退出。将 key 放入 j+1 = 0 位置,得到 [1, 2, 4, 5, 6] | [3]
  • 第5轮(i=5,key=3)

    比较 key=3 与前面元素:从右向左依次与 6, 5, 4, 2 比较。

    • 6 > 3 → 将 6 后移一位,数组 [1, 2, 4, 5, 6, 6]
    • 5 > 3 → 将 5 后移一位,数组 [1, 2, 4, 5, 5, 6]
    • 4 > 3 → 将 4 后移一位,数组 [1, 2, 4, 4, 5, 6]
    • 2 <= 3 → 停止。将 key 放入 j+1 = 2 位置,得到 [1, 2, 3, 4, 5, 6]

排序完成。

代码实现(Python)
python 复制代码
def insertion_sort(arr):
    """
    直接插入排序(升序)
    参数 arr: 待排序列表
    返回: 排序后的列表(原地修改)
    """
    n = len(arr)
    # 从第二个元素开始遍历(下标1)
    for i in range(1, n):
        key = arr[i]          # 当前待插入元素
        j = i - 1             # 已排序部分的最后一个元素下标
        
        # 将比 key 大的元素向右移动一位
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        
        # 插入 key 到正确位置
        arr[j + 1] = key
    
    return arr

# 测试
test = [5, 2, 4, 6, 1, 3]
print("排序前:", test)
sorted_arr = insertion_sort(test.copy())  # 使用副本,避免修改原列表
print("直接插入排序后:", sorted_arr)

代码执行输出

复制代码
排序前: [5, 2, 4, 6, 1, 3]
直接插入排序后: [1, 2, 3, 4, 5, 6]
复杂度与稳定性分析
时间复杂度
  • 最坏情况 O(n²) :当输入数组完全逆序时(例如 [6,5,4,3,2,1]),每一轮插入都需要将 key 移动到最前面,比较和移动的次数分别为 1+2+...+(n-1) = n(n-1)/2,因此时间复杂度为 O(n²)。
  • 最好情况 O(n):当输入数组已经有序时,每一轮只需要比较一次(发现前一个元素 ≤ key),不需要移动,所以总共只需要 n-1 次比较,时间复杂度为 O(n)。
  • 平均情况 O(n²):对于随机排列的数组,平均比较和移动次数约为 n²/4,仍为 O(n²)。
空间复杂度
  • O(1) :直接插入排序是原地排序 算法,只需要常数级别的额外空间(一个 key 变量和循环变量)。
稳定性
  • 稳定 :因为在比较时,只有当 arr[j] > key 时才移动元素(严格大于)。如果遇到相等的元素,不会移动它,因此相等元素的相对顺序保持不变。例如:对 [2, ①, ②](两个 2 用编号区分),排序后 [2, ①, ②] 仍保持原顺序。
优缺点
  • 优点:实现简单,对于小规模数据或基本有序的数据效率很高(接近线性),且稳定。
  • 缺点:对于大规模乱序数据,复杂度 O(n²) 不可接受。

二、希尔排序(Shell Sort)

算法背景与思想

直接插入排序有一个重要特性:当数组基本有序时,插入排序的效率非常高(接近 O(n)) 。然而,对于完全逆序的数组,它需要移动大量元素。那么,有没有一种方法可以先让数组变得"基本有序",然后再用插入排序完成最终整理呢?

1959年,计算机科学家唐纳德·希尔(Donald Shell)提出了希尔排序。其核心思想是:将整个数组按照某个增量(gap)分成若干个子序列,对每个子序列分别进行直接插入排序。随着增量逐渐缩小,每个子序列包含的元素越来越多,但整个数组也越来越接近全局有序。当增量缩小到 1 时,整个数组实际上只进行一次全局插入排序,而此时数组已经基本有序,因此排序速度很快。

希尔排序也称为"缩小增量排序"。

增量序列的选择

增量序列的设计对希尔排序的性能至关重要。最经典的是希尔提出的原始序列:n/2, n/4, ..., 1(每次减半)。但后来人们发现其他增量序列可以获得更优的时间复杂度,例如:

  • Hibbard 增量序列1, 3, 7, 15, ..., 2^k - 1,最坏复杂度 O(n^(3/2))。
  • Sedgewick 增量序列1, 5, 19, 41, 109, ...,最坏复杂度 O(n^(4/3)),实践中表现很好。
  • Knuth 增量序列1, 4, 13, 40, 121, ...(即 (3^k - 1)/2)。

本文以最简单的原始增量(每次折半)为例进行讲解。

算法步骤(以升序为例)
  1. 初始化增量 gap = len(arr) // 2
  2. gap > 0 时,执行以下操作:
    • 从下标 i = gap 开始,依次遍历每个元素 arr[i]
    • 对于当前元素,将其视为待插入元素,对它所在的子序列 (该子序列由索引 i, i-gap, i-2*gap, ... 组成)执行一次插入排序。
    • 遍历完所有 i 后,将增量缩小:gap = gap // 2
  3. 重复步骤2,直到 gap == 0,排序完成。

注意 :希尔排序并不是先完整地排好每个子序列,再处理下一个子序列,而是采用交替比较 的方式:从 gap 开始,每个元素都与其前面相距 gap 的元素进行比较和插入。这种方式使得代码更简洁,且不需要显式地拆分子序列。

希尔排序的详细解析

初始数组与第一轮排序(gap=4)

原始数组为 [9, 8, 3, 7, 5, 6, 4, 1],初始增量 gap=4。将数组分为4个子序列:

  • 子序列1(索引0,4):[9,5] → 排序后 [5,9]
  • 子序列2(索引1,5):[8,6] → 排序后 [6,8]
  • 子序列3(索引2,6):[3,4] → 保持 [3,4]
  • 子序列4(索引3,7):[7,1] → 排序后 [1,7]

合并后数组更新为 [5,6,3,1,9,8,4,7],此时较大元素(如9、8)向后移动,较小元素(如1、3)向前移动。

第二轮排序(gap=2)

增量缩小为 gap=2,分为2个子序列:

  • 子序列1(索引0,2,4,6):[5,3,9,4] → 插入排序后为 [3,4,5,9]
  • 子序列2(索引1,3,5,7):[6,1,8,7] → 插入排序后为 [1,6,7,8]

合并后数组更新为 [3,1,4,6,5,7,9,8],此时数组已接近有序。

第三轮排序(gap=1)

gap=1 时,退化为标准插入排序:

  • [3,1,4,6,5,7,9,8] 逐元素插入排序,最终得到完全有序的 [1,3,4,5,6,7,8,9]

Python 代码实现

python 复制代码
def shell_sort(arr):
    n = len(arr)
    gap = n // 2  # 初始增量
    
    while gap > 0:
        for i in range(gap, n):
            temp = arr[i]
            j = i
            # 在子序列中执行插入排序
            while j >= gap and arr[j - gap] > temp:
                arr[j] = arr[j - gap]
                j -= gap
            arr[j] = temp
        gap //= 2  # 缩小增量
    return arr

# 测试
test = [9, 8, 3, 7, 5, 6, 4, 1]
print("排序前:", test)
print("希尔排序后:", shell_sort(test.copy()))

输出结果

复制代码
排序前: [9, 8, 3, 7, 5, 6, 4, 1]
希尔排序后: [1, 3, 4, 5, 6, 7, 8, 9]

复杂度与稳定性分析

时间复杂度

  • 最坏情况(原始增量序列):O(n²)。
  • 优化增量序列(如Hibbard或Sedgewick):可降至 O(n^(3/2)) 或 O(n^(4/3))。
  • 平均情况(经验值):O(n^(1.3~1.5))。

空间复杂度

O(1),原地排序算法。

稳定性

不稳定。分组插入可能导致相等元素的相对顺序改变(例如 [5,3,3,4] 排序后可能变为 [3,3,4,5],两个3的顺序可能交换)。


直接插入排序 vs 希尔排序

特性 直接插入排序 希尔排序
核心思想 逐个插入已排序序列 分组插入,逐步缩小增量
最坏时间复杂度 O(n²) O(n²)(原始增量)或更低
平均时间复杂度 O(n²) O(n^(1.3~1.5))(经验值)
稳定性 稳定 不稳定
适用场景 小规模或基本有序数据 中等规模数据,无需稳定性

适用建议

  • 直接插入排序:适合小规模数据(如n ≤ 1000)或已基本有序的序列,实现简单且稳定。
  • 希尔排序:适合中等规模数据(几千到几万),性能显著优于直接插入排序,但需注意不稳定性。

实际开发中,Python内置的 sorted() 使用Timsort(混合归并和插入排序),通常无需手动实现希尔排序,但理解其分治思想对算法学习很有帮助。

相关推荐
nlpming1 小时前
opencode System Prompt 构建机制 & AGENTS.md注入机制
算法
nlpming1 小时前
opencode - 安装和配置
算法
洛水水1 小时前
【Redis入门】一篇详解Redis五大数据结构
数据结构·数据库·redis
nlpming1 小时前
opencode 内置工具
算法
nlpming1 小时前
opencode - 常用命令&自定义命令
算法
CoderCodingNo1 小时前
【CSP】CSP-J 2021真题 | 插入排序 luogu-P7910 (适合GESP四-六级及以上考生练习)
数据结构·算法·排序算法
艺术电影节2 小时前
祝贺电影《撤离》《悼念词》《水草长生》 荣获亚洲艺术电影节提名
算法·推荐算法·电视
MATLAB代码顾问2 小时前
改进鲸鱼优化算法(IWOA)求解柔性作业车间调度问题(FJSP)——附MATLAB代码
开发语言·算法·matlab
量子-Alex2 小时前
【大模型】EvoLM论文LLM训练各个阶段效果
人工智能·算法·机器学习