【数据结构与算法】希尔排序

👨‍💻 关于作者:会编程的土豆

"不是因为看见希望才坚持,而是坚持了才看见希望。"

你好,我是会编程的土豆,一名热爱后端技术的Java学习者。

📚 正在更新中的专栏:

💕作者简介:后端学习者

概念

希尔排序 = 插入排序 + 分组跳跃

它不是一次只和前面相邻的元素比,而是先隔着很远比,然后慢慢缩小距离,最后变成普通的插入排序

为什么需要希尔排序?

简单插入排序有个明显的软肋:当较小的数都堆在数组尾部时,排序效率会很低。因为插入排序每次只能交换相邻元素,要把尾部的小数挪到前面,需要一步一步"冒泡"过去,非常耗时。

看一下插入排序的代码:

cpp 复制代码
public static void insertionSort(int[] arr) {
    int len = arr.length;
    for (int i = 1; i < len; i++) {
        int j = i;
        int temp = arr[j];
        while (j > 0 && arr[j - 1] > temp) {
            //如果前一个元素比当前元素大,则将前一个元素向后移动一位。
            //将 j 的值减1,继续比较前一个元素。
            arr[j] = arr[j - 1];
            j--;
        }
        arr[j] = temp;
    }
}

而希尔排序通过引入增量(Gap) 的概念,允许元素跳跃式移动,让小数能更快地"蹦"到前面,从而解决了这一问题

希尔排序代码

cpp 复制代码
void shellSort(vector<int>& arr) {
    int n = arr.size();
    
    // 外层循环:控制 gap 从 n/2 开始,每次减半
    for (int gap = n / 2; gap > 0; gap /= 2) {
        
        // 内层循环:对每个分组做插入排序
        // 注意这里从 gap 开始,而不是从 1 开始
        for (int i = gap; i < n; ++i) {
            int key = arr[i];
            int j = i - gap;
            
            // 和前面相隔 gap 的元素比较
            while (j >= 0 && arr[j] > key) {
                arr[j + gap] = arr[j];  // 大的往后挪 gap 个位置
                j -= gap;               // 往前跳 gap 个位置继续比
            }
            arr[j + gap] = key;          // 插入到正确位置
        }
    }
}

插入排序 vs 希尔排序 代码对比

插入排序 希尔排序
for (int i = 1; i < n; ++i) for (int gap = n/2; gap > 0; gap/=2)
for (int i = gap; i < n; ++i)
int j = i - 1; int j = i - gap;
while (j >= 0 && arr[j] > key) while (j >= 0 && arr[j] > key)
arr[j + 1] = arr[j]; arr[j + gap] = arr[j];
j -= 1; j -= gap;
arr[j + 1] = key; arr[j + gap] = key;

唯一的区别 :把插入排序里的 1 全部换成了 gap,再套一层 gap 递减的循环。

复杂度与特点

特性 说明
时间复杂度 取决于 gap 序列,经典希尔增量 O(n²),优化序列可达 O(n^1.3)
空间复杂度 O(1),原地排序
稳定性 不稳定(跳跃式交换可能打乱相同元素的顺序)
适用场景 中等规模数据,嵌入式设备等内存敏感场景

一句话总结

希尔排序 = 带间隔的插入排序,通过"大步跳跃 + 逐步逼近"来优化小数后移的问题。

希尔排序的核心原理

核心概念:增量(Gap)

希尔排序引入了一个间隔 gap ,它不是让元素和相邻的比,而是和相隔 gap 个位置的元素比。

过程分三步:

  1. 分组:按 gap 把数组分成若干组

  2. 组内插入排序:对每组分别做插入排序

  3. 缩小 gap:gap 减半,重复上述过程,直到 gap = 1

当 gap = 1 时,就是普通的插入排序,但此时数组已经基本有序,插入排序会非常快。

图解演示

数组:[80, 93, 60, 12, 42, 30, 68, 85, 10]

第一轮:gap = 4

把相隔 4 个位置的元素分为一组:

cpp 复制代码
下标:  0   1   2   3   4   5   6   7   8
值:   80  93  60  12  42  30  68  85  10

分组情况(相同标记的为一组):
组1 (▲): 80, 42, 10  → 下标 0, 4, 8
组2 (●): 93, 30      → 下标 1, 5
组3 (■): 60, 68      → 下标 2, 6
组4 (◆): 12, 85      → 下标 3, 7

对每组分别做插入排序:

  • 组1 排序后:10, 42, 80

  • 组2 排序后:30, 93

  • 组3 排序后:60, 68

  • 组4 排序后:12, 85

放回原数组:

cpp 复制代码
排序前: [80, 93, 60, 12, 42, 30, 68, 85, 10]
排序后: [10, 30, 60, 12, 42, 93, 68, 85, 80]

观察 :最小的 10 从最后一位(下标 8)跳到了第一位(下标 0)!一步跨了 8 个位置,这就是希尔排序的效率来源。


第二轮:gap = 2

cpp 复制代码
当前: [10, 30, 60, 12, 42, 93, 68, 85, 80]

分组:
组1 (▲): 10, 60, 42, 68, 80  → 下标 0,2,4,6,8
组2 (●): 30, 12, 93, 85      → 下标 1,3,5,7

分别插入排序后放回:

cpp 复制代码
排序前: [10, 30, 60, 12, 42, 93, 68, 85, 80]
排序后: [10, 12, 42, 30, 60, 85, 68, 93, 80]

第三轮:gap = 1

就是普通的插入排序:

cpp 复制代码
排序前: [10, 12, 42, 30, 60, 85, 68, 93, 80]
排序后: [10, 12, 30, 42, 60, 68, 80, 85, 93]

此时数组已经基本有序,插入排序只需要很少的移动就能完成。

注意排序时:

每一组元素只在属于自己组的下标位置 之间进行排序和交换,完全不碰其他组的元素

说人话就是:比如说这一组数,就是在原来数组的这一组的基础之上排序换位置,而其他组的位置丝毫不影响,只在自己的那个gap组里面换;

相关推荐
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十五)——Alpha混合与透明效果:分层窗口实战
c++·windows·学习·图形渲染·win32
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十四)——GDI Region与裁切:不规则窗口与可视化控制
c++·windows·学习·c·图形渲染·win32
9分钟带帽2 小时前
vscode中配置Qt6和CMake的开发环境
c++·vscode·cmake
邦爷的AI架构笔记2 小时前
GLM-5.1 接入踩坑记录:用免费开源模型搭个 AI 代码审计小工具
后端·算法
苏宸啊2 小时前
哈希扩展问题
算法·哈希算法
汀、人工智能2 小时前
[特殊字符] 第73课:打家劫舍
数据结构·算法·数据库架构·图论·bfs·打家劫舍
我能坚持多久2 小时前
C++类与对象(中)
开发语言·c++
别或许2 小时前
2、高数----数列极限(知识总结)
算法
山栀shanzhi2 小时前
深入C++之:一个类有几张虚函数表?
c++·面试