(c1) 希尔排序:Shell序列

官官方信息技术老师·14 页·深入(追求细节与边界)·0 次浏览·38 天前
排序算法插入排序贪心分组序列设计

希尔排序与 Shell 序列

看完你能亲手推演插入排序分组过程并说清Shell序列为何失效

按 空格/→ 演示下一步

问一句
1 / 14 页

全部页面点击任意一页,跳回舞台从这页播放

排序算法插入排序贪心分组序列设计

希尔排序与 Shell 序列

看完你能亲手推演插入排序分组过程并说清Shell序列为何失效

1第 1 页 · 希尔排序与 Shell 序列

插入排序的痛点

上一页我们知道了希尔排序要靠插入排序打下手。可插入排序本身有个老毛病:一个元素想挪到正确位置,每次只能和左边的邻居换一步。前面堆积的乱序越多,它就越吃力。

一步一挪
相邻交换,元素每轮只前进一格
代价看逆序对
逆序对越多,需要交换的次数越多
乱序在前最慢
小元素堆在数组尾部时要长途跋涉
人挤地铁挪位对应 →插入排序移动

每挤过一个人才挪一步,前面越挤挪得越慢

2第 2 页 · 插入排序的痛点

希尔排序的策略

上一页我们看到:插入排序慢,是因为元素每次只能挪一小步。那让它先大步挪呢?先用大间隔把元素粗略理顺,再逐步细化,最后一步才是普通插入排序——此时数组已近乎有序。

先大步调整
间隔 h 很大时做插入排序,元素能远距离归位
h-有序
每轮结束后,相距 h 的元素各自成有序子序列
间隔逐步缩小
h 按序列递减,前一轮的整理让后一轮更省力
插入排序收尾
h=1 时退化为插入排序,但此时移动量已很小
整理散乱的书堆对应 →希尔排序

先按大类粗分书堆,再逐层细排,最后微调

3第 3 页 · 希尔排序的策略

什么是 h-有序

上一页说了:希尔排序让相隔 h 的元素先排好队。那“排好队”到底怎么算数?这就需要一个精确的标准——它就是 h-有序。

h-有序的定义
任意间隔为 h 的元素单独抽出来看,各自构成一条有序序列
整体表现
整个数组未必全局有序,但称它是 h-有序的
边界情形
h=1 时 h-有序就是普通有序,即排序完成
楼层管家查房对应 →h-有序

每个管家只管固定楼层的房间有序,对应每条 h 间隔子序列各自有序

a[i] ≤ a[i+h](∀ i 使得 i+h<n)a[i]\ \le\ a[i+h]\quad(\forall\,i\ \text{使得}\ i+h<n)
4第 4 页 · 什么是 h-有序

循秩访问:抽取子序列

看 8 个数的数组如何按间隔 h=4 被拆成隐形的子序列:元素不移动,访问时按秩跳跃。

图解渲染中…
a0:下标:元素值,数组物理上仍是原顺序a4:同一颜色链=一条 h=4 的子序列,仅逻辑上相连a8x:虚形节点表示越过末尾,子序列到此结束
5第 5 页 · 循秩访问:抽取子序列

子序列上的插入排序

c

不真正抽取子序列,只把插入排序的步长 1 换成 h——循秩访问即可遍历每个 h-子序列。

代码高亮加载中…

与普通插入排序逐行对照:唯一改动是步长从 1 变为 h,j 按秩跳 h 前进,物理上未抽取任何子序列。

6第 6 页 · 子序列上的插入排序

实例:逐轮执行过程

用 8 个逆序元素演示 h=4、2、1 三轮排序,看每轮后的数组状态。

1
初始逆序
数组 [8,7,6,5,4,3,2,1],全部反着排,是最坏情形
2
h=4 排序
4 个子序列各自插入排序,得 [4,3,2,1,8,7,6,5]
3
h=2 排序
在近有序基础上 2 个子序列排序,得 [2,1,4,3,6,5,8,7]
4
h=1 收尾
最后一轮就是普通插入排序,因近有序几乎无移动
7第 7 页 · 实例:逐轮执行过程

观察:大步换来什么

按轮次从左到右看:每轮标注单次移动的典型距离,前期大步远移,后期小步微调。

图解渲染中…
b1:h=13 时元素一次可跳过 13 个位置f1:h=1 即普通插入排序,此时序列已近似有序
8第 8 页 · 观察:大步换来什么

插入排序 vs 希尔排序

两者最后一轮完全一样,为何性能天差地别?关键在移动次数的分布,值得逐维对照。

纯插入排序
  • 面对全序列,步长固定为 1
  • 元素可能被反复远距离搬运
  • 移动次数正比于全体逆序数
希尔排序
  • 先大步分组,多轮铺垫到 1
  • 大步时一次搬运顶多步小移动
  • 末轮几乎有序,移动极少
正确理解:末轮算法相同,差别在于此前 h-有序铺垫已消灭大部分逆序,1-步长轮只剩收尾。
9第 9 页 · 插入排序 vs 希尔排序

Shell 序列的定义

前页看到逐轮步长带来的加速,那步长到底按什么规律取?Shell 给出的答案很朴素:从数组长度的一半开始,每轮除以 2,一路减到 1 为止。

起点 ⌊n/2⌋
第一轮步长取长度一半(向下取整),一步迈得最大
逐次减半
每轮步长除以 2:n/2, n/4, n/8 … 严格递减
终点必须是 1
最后一步为 1,退化成一次完整插入排序,保证正确性
变焦镜头对应 →Shell 序列

先大倍率粗略定位,再逐级缩小焦距精细对准

h1=⌊n/2⌋,  hk+1=⌊hk/2⌋,  …,  ht=1h_1=\lfloor n/2\rfloor,\; h_{k+1}=\lfloor h_k/2\rfloor,\;\dots,\; h_t=1
10第 10 页 · Shell 序列的定义

Shell 序列的短板

上一页我们看到 Shell 序列按 1、2、4、8… 逐轮折半。它简单好记,但藏着一个隐患:相邻增量全是彼此的倍数。这会让某些位置的元素长期“互相看不见”,最坏情况效率大幅退化。

增量互为倍数
后一轮增量是前一轮的 2 倍,整除关系贯穿始终
奇偶位不交互
h=2 时,奇数位置只和奇数位置比较,偶数位同理
大增量白做了
前几轮的排序成果对后续轮次帮助有限,最坏退回 O(n²)
解法方向
改用互质的增量序列(如 Hibbard),让各轮互补覆盖
只用整箱搬运对应 →Shell 序列

箱子容量逐次减半时,零头物品始终挪不动,直到最后一箱一箱慢慢搬

11第 11 页 · Shell 序列的短板

自测:增量设计

点击作答

使用 Shell 序列 1,2,4,8,… 的希尔排序,最核心的问题是什么?

12第 12 页 · 自测:增量设计

要点收束

  • ✓插入排序的性能瓶颈在于元素只能逐位挪动
  • ✓希尔排序用大步先远距离搬运,小步再精修
  • ✓h-有序是层层铺垫:前一轮为后一轮省工
  • ✓Shell 序列实现简单,但增量间公因数削弱效果
  • ✓增量设计是自由的——序列好坏决定整体复杂度
延伸主题:Knuth 序列:3h+1 的折中设计Hibbard / Sedgewick 序列希尔排序复杂度的已知上界
13第 13 页 · 要点收束

课后思考

三个问题没有唯一答案,先自己想,再看参考答案。

1如果各轮步长互不整除(如 8,4,2,1 之外的序列),大步的成果还能保留到下一轮吗?为什么?

参考答案思考路径:回看 h-有序的定义——h-有序只约束间隔 h 的元素。步长交替互质时,新一轮排序会打破旧约束,但整体逆序数仍大幅下降;Shell 序列的公因子问题正出在这里。

2从逆序对的视角看,一次 h-排序最多能消掉哪些逆序对?哪些它无能为力?

参考答案思路:h-排序等价于对 h 个子序列各做插入排序,只能消灭两端距离恰为 h 倍数的逆序对;间隔不是 h 倍数的逆序对要等更小步长处理。用逆序数下降量解释大步的收益。

3Shell 序列相邻步长公因子太大是短板,如果让你重新设计步长序列,你会遵循什么原则?

参考答案方向:相邻步长尽量互素、避免 2 的幂;末尾必须收敛到 1 保证正确性;经验上步长按约 2.2 倍左右递减效果较好。可对照 Hibbard、Knuth 序列验证你的原则。

14第 14 页 · 课后思考