news 2026/9/2 15:57:25

希尔排序是什么?一文详解(附例题)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
希尔排序是什么?一文详解(附例题)

摘要

希尔排序是插入排序的改进版,通过增量分组实现元素跨距离移动,大幅减少逐位挪动开销。每趟排序按当前增量将序列分成若干子序列,分别进行插入排序;随后逐步缩小增量,直到增量为1时做最后一次整体插入排序。若增量为1,希尔排序退化为普通插入排序。希尔排序是原地、不稳定的算法,时间复杂度依赖增量序列,通常为O(n¹·³)到O(n²)。使用给定增量序列(如5,3,1)时,直接按序列执行即可,每趟必须涵盖全部元素,且上一趟结果是下一趟的输入。

目录

一. 直接手写一个例子(可大白话理解)

二. 官方解释

三. 例题(仔细看,很有收获)

1. 题目

2. 分析过程

四. 代码实现(C语言)

解读

软考真题—2020—下半年—分治法—希尔排序

1. 题目

2. 问题

3. 分析

4. 官方答案

一. 直接手写一个例子(可大白话理解)

注意:一开始小白可能不懂“希尔排序”是什么,下面这段文字看看就行了,实在不会,继续往下看,结合结合例题,找个感觉就OK了。

如下图,题目给的条件是增量d一开始为2,即d1=2,那么d2=d1/2=1(这是默认规则,除非题目跟你说了增量序列是多少,那我们就不需要依次除以2自己去求了)。注意以下几点:

  • 第一趟:要从第1个元素开始,往后数d1(即2)个元素,一直数,直到没有可数的,这些元素组成第一组;然后再从第2个元素开始,往后数d1(即2)个元素,一直数,直到没有可数的,这些元素组成第二组;......以此类推,直到把所有元素都涵盖掉。
  • 第二趟:要从第1个元素开始,往后数d2(即1)个元素,一直数,直到没有可数的,这些元素组成第一组;可见此时只有一组,因为步长是1,直接整个序列属于同一组。

可见两点:

  • 我们的每一趟,只是步长不一样,但是都必须涵盖掉所有元素
  • 每一组之间,只是往后错开一个元素
  • 直到d=1时,就不能继续往下再来一趟了,因为d=1时,一组就可以涵盖掉所有元素
  • 可见,如果一上来就令d=1,那么此时“希尔排序”就会退化成“插入排序”

二. 官方解释

希尔排序(Shell Sort)是一种基于插入排序的改进算法,核心思想是让元素能一次跨越多个位置移动,从而减少传统插入排序中大量逐位挪动的开销。它通过选择一个增量序列(如数组长度的一半,再逐次减半),将整个待排序序列按增量分成若干个子序列,分别对每个子序列进行直接插入排序;完成一轮后缩小增量,重复分组排序,直到增量变为1,此时整个序列已基本有序,只需再做一次普通的插入排序即可。这种“预排序”策略使得较小的元素能快速跳到前面,较大的元素快速移到后面,大幅减少了逆序对数量,因此希尔排序的时间复杂度依赖于增量序列的选择,通常在O(n^1.3)到O(n^2)之间,且不需要额外大量内存,属于不稳定的原地排序算法,适合中等规模数据的快速排序场景。

三. 例题(仔细看,很有收获)

1. 题目

2. 分析过程

可总结出以下几点:

  • 题目给了增量序列 = 5,3,1,此时我们直接用这个就行(即:第一趟步长为5,第二趟步长为3,第三趟步长为1),而不是再除以2手动去求每个步长了。
  • 每一趟排序,不管步长是多少,都要涵盖掉所有元素
  • 上一趟排序的结果序列,是下一趟排序的开始序列

四. 代码实现(C语言)

#include <stdio.h> #include <stdlib.h> // 希尔排序函数 // data[]: 待排序数组 // n: 数组长度 void ShellSort(int data[], int n) { int *delta, k, i, t, dk, j; // --- 阶段一:生成步长序列 (n/2, n/4 ... 1) --- k = n; // 分配空间存储步长序列,最坏情况需要 n/2 个空间 delta = (int*)malloc(sizeof(int) * (n / 2)); i = 0; do { k = k / 2; // 【关键】计算下一个步长(减半) delta[i++] = k; // 将步长存入数组 } while (k > 1); // 【关键】直到步长为1时停止生成序列 // --- 阶段二:按步长进行分组插入排序 --- i = 0; // 遍历每一个步长(从大到小) while (i < n / 2 && delta[i] > 0) { dk = delta[i]; // 【修复】取出当前步长赋值给 dk // 标准的“带间隔”插入排序逻辑 // 从第 dk 个元素开始,对每个元素进行插入操作 for (k = dk; k < n; ++k) { // 如果当前元素小于同组的前一个元素,则需要移动 if (data[k] < data[k - dk]) { t = data[k]; // 暂存当前待插入元素 // 在同组内寻找插入位置,元素后移 for (j = k - dk; j >= 0 && data[j] > t; j -= dk) { data[j + dk] = data[j]; } data[j + dk] = t; // 【关键】将暂存元素插入到正确位置 } } i++; // 切换到下一个更小的步长 } free(delta); // 释放动态分配的内存 } // 测试主函数 int main() { int arr[] = {15, 9, 7, 8, 20, -1, 4}; int n = sizeof(arr) / sizeof(arr[0]); printf("排序前: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); ShellSort(arr, n); printf("排序后: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); return 0; }

解读

核心难点破解:代码里的kdk到底是什么?

在手写时,我们习惯把数组拆成几个独立的子序列,比如:

  • 子序列1:data[0], data[3], data[6]...
  • 子序列2:data[1], data[4], data[7]...

但在代码里,计算机不会真的把数组拆开。它用了一个很巧妙的技巧:交错扫描

1. 变量对应关系

  • dk(Delta K):就是你手写时的步长(比如第一趟是 3)。
  • k:这是当前正在处理的元素的下标
  • k - dk:这是当前元素在同一个子序列里的前一个元素

2. 代码逻辑翻译

让我们看代码里最核心的那个for循环(第 13 行):

for (k = dk; k < n; ++k)

这句话的意思是:从下标dk开始,一直遍历到数组末尾。

你可能会问:“为什么从dk开始?”

  • 因为下标0dk-1的元素,分别是各个子序列的第一个元素。第一个元素没法跟前面比(前面没人),所以不需要处理。
  • dk开始,刚好就是各个子序列的第二个元素!

图解代码执行过程(以题目数据为例)

假设数组是[15, 9, 7, 8, 20, -1, 4],长度n=7
第一趟步长dk = 3

代码的for (k = 3; k < 7; k++)会依次让k等于 3, 4, 5, 6。我们来看看这四次循环对应的手写过程:

循环轮次代码中的k对应的子序列操作手写时的动作代码里的判断data[k] < data[k-dk]
第1轮k=3处理子序列2的第2个元素 (8)比较8和它前面的15(下标0)data[3] < data[0]8 < 15(成立) →插入/交换
第2轮k=4处理子序列3的第2个元素 (20)比较20和它前面的9(下标1)data[4] < data[1]20 < 9(不成立) →不动
第3轮k=5处理子序列1的第2个元素 (-1)比较-1和它前面的7(下标2)data[5] < data[2]-1 < 7(成立) →插入/交换
第4轮k=6处理子序列2的第3个元素 (4)比较4和它前面的8(下标3)data[6] < data[3]4 < 8(成立) →插入/交换

发现规律了吗?
代码并没有显式地写三个for循环去分别处理三个子序列,而是用一个for循环,通过k的递增,轮流照顾了所有的子序列!

  • k=3时,它在照顾子序列2。
  • k=4时,它在照顾子序列3。
  • k=5时,它在照顾子序列1。
  • k=6时,它又回到了子序列2。

这就是希尔排序代码最难理解的地方:它把多组并行的插入排序,压缩成了一个串行的循环。

再看那个难懂的if和内部循环

if (data[k] < data[k - dk]) { t = data[k]; // 暂存 for (j = k - dk; j >= 0 && data[j] > t; j -= dk) { data[j + dk] = data[j]; // 后移 } data[j + dk] = t; // 插入 }

这一段就是标准的直接插入排序代码,只是把原来的步长1换成了dk

  • j -= dk:意思是沿着子序列往回找
  • data[j + dk] = data[j]:意思是把大的元素往后挪一个坑位(挪dk的距离)。

软考真题—2020—下半年—分治法—希尔排序

1. 题目

注意:这道题有漏印刷代码dk = delta[i];

意思是:dk= 当前这一轮排序使用的步长

2. 问题

3. 分析

对照【四】的解读,应该能秒杀这道题的填空。

【问题3】的解题过程(手写)如下:

注意:步长序列的第一个步长d1 = 数组长度 / 2= 7 / 2 = 3

所以我们第一趟排序就按照步长3进行操作即可

4. 官方答案

以上就是本篇文章的全部内容,喜欢的话可以留个免费的关注呦~~~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 15:52:37

7 MCP协议

一、MCP 必知必会 什么是 MCP&#xff1f; MCP&#xff08;Model Co⁠ntext Protocol&#xff0c;模型上下文协议&#xff09;是‌一种开放标准&#xff0c;目的是增强 AI 与外部系统的交互​能力。MCP 为 AI 提供了与外部工具、资源和‎服务交互的标准化方式&#xff0c;让 A…

作者头像 李华
网站建设 2026/9/2 15:50:26

单片机计算机毕设之基于 STM32 的本地身份核验与 APP 远程管理门禁系统实现 基于 STM32 的指纹射频卡密码智能门锁监控系统设计(012506)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/2 15:49:45

人工智能70年:从学科诞生到大模型本地部署与API调用实践

今年是2026年&#xff0c;人工智能这个学科正式走到第70个年头。1956年夏天&#xff0c;达特茅斯学院的一场暑期研讨会&#xff0c;第一次把“Artificial Intelligence”作为学科名称固定下来&#xff0c;也因此被视为AI的诞生原点。 但70年这个数字本身不重要。重要的是&…

作者头像 李华
网站建设 2026/9/2 15:45:27

渭河流域地理数据包:矢量边界、DEM、水系与行政区划一站式解决方案

简介&#xff1a;本资源是一套面向地理信息科学、水文水资源、区域规划及生态环境研究领域的渭河流域综合空间数据集&#xff0c;解决科研与工程实践中流域边界界定不清、多源数据整合困难、制图效率低等痛点。资源共57个文件&#xff0c;包含ArcGIS可编辑MXD工程文件&#xff…

作者头像 李华
网站建设 2026/9/2 15:44:56

NVIDIA Magpie TTS:本地部署低延迟高质量语音合成实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华