第1关:希尔排序的实现
任务描述
本关任务:编写代码实现希尔排序。
相关知识
为了完成本关任务,你需要掌握: 1.如何实现希尔排序; 2.希尔排序的算法分析。
希尔排序
对于插入排序,最好的情况是列表已经基本有序,此时比较次数的时间复杂度是O(n)
。列表越接近有序,插入排序的比较次数就越少。因此,希尔排序以插入排序为基础,将待排序的列表划分为一些子列表,再对每一个子列表执行插入排序,从而实现对插入排序性能的改进。
希尔排序又叫缩小增量排序,划分子列表的特定方法是希尔排序的关键。我们并不是将原始列表直接分成若干个含有连续元素的子列表,而是首先确定一个增量 i 来作为子列表的划分间隔,然后把每间隔为 i 的所有元素选出来组成子列表。在希尔排序的过程中,每一趟排序都将增量不断减小,随着子列表数量的越来越少,无序表整体越来越接近有序,从而能够减少整体排序的比较次数。
图 1 展示的是以 3 为增量的希尔排序。
图1 以 3 为增量的希尔排序
若对这个含有 9 个数据项的列表以 3 为间隔来划分,则会分成三个子列表:
-
将列表中下标为 0、3、6 的数据项分成一组,得到子列表 [54,17,44];
-
将列表中下标为 1、4、7 的数据项分成一组,得到子列表 [26,77,55];
-
将列表中下标为 2、5、8 的数据项分成一组,得到子列表 [93,31,20]。
然后分别对每一个子列表执行插入排序,得到如图 2 所示的列表。
图2 对每个子列表排序后的结果
这三次插入排序的过程可描述为:
-
对于子列表 [54,17,44],首先将 17 与 54 进行比较,17 小于 54,于是将 17 插入到 54 之前。然后将 44 与 54、17 比较,于是将 44 插入到 17 与 54 之间,最终得到有序列表 {17,44,54};
-
对于子列表 [26,77,55],首先将 77 与 26 进行比较,77 大于 26,于是将 77 插入到 26 之后(不需要移动位置)。然后将 55 与 77、26 比较,于是将 55 插入到 26 与 77 之间,最终得到有序列表 {26,55,77};
-
对于子列表 [93,31,20],首先将 31 与 93 进行比较,31 小于 93,于是将 31 插入到 93 之前。然后将 20 与 93、31 比较,于是将 20 插入到 31 之前,最终得到有序列表 {20,31,93}。
这就完成了第 1 趟希尔排序,虽然这个列表还没有完全排好序,但经过这一趟对子列表的排序之后,列表中的每个元素更加靠近它最终应该处在的位置。
希尔排序的最后一趟排序一定是将增量减少到 1,图 3 是对图 2 中得到的列表以 1 为增量进行希尔排序,即执行标准的插入排序过程。
图3 以 1 为增量的排序
通过之前对子列表进行的排序,列表比最开始更加接近有序,此时再进行标准插入排序,能够在一定程度上减少比较和移动的次数。此时,仅需要再进行四次移动就可以完成排序。最后一次插入排序过程中的移动操作有:
-
将插入项 20 与 26 进行比较,20 小于 26,于是将 26 向右移动一个位置;再将 20 与 17 进行比较,20 大于 17,最终将 20 插入到 17 与 26 之间;
-
将插入项 31 与 55 进行比较,31 小于 55,于是将 55 向右移动一个位置;再将 31 与 44 进行比较,31 小于 44,于是将 44 向右移动一个位置;再将 31 与 26 进行比较,31 大于 26,最终将 31 插入到 26 与 44 之间;
-
将插入项 54 与 55 进行比较,54 小于 55,于是将 55 向右移动一个位置;再将 54 与 44 进行比较,54 大于 44,最终将 54 插入到 44 与 55 之间。
对于含有 n 个数据项的列表,希尔排序的增量一般从 n/2 开始,之后的每趟减少到 n/4、n/8……直到 1。图 4 展示了对含有 9 个数据项的列表以 4 为增量划分子列表的一个示例。
图4 以 4 为增量的情况
此外,增量序列中的值不应该有除 1 之外的公因子,否则可能会造成前面某一趟分在同一组已经比较过的数据项,在本趟继续分在同一组,此时这些数据项再次相互比较毫无意义,同时还会增加算法的时间,例如 8、4、2、1 这样的序列就不要选取(8、4、2 有公因子 2)。
希尔排序的算法分析
可能你会觉得希尔排序并不会比插入排序好,因为它最后一步执行了一次完整的插入排序。但事实上,最后的一次排序并不需要很多次的比较和移动,因为已经在之前对子列表的排序中实现了部分排序,这使得最后的排序非常高效。
希尔排序的复杂度分析十分复杂,大致是介于O(n)
和O(
之间。使用某些增量值时,它的时间复杂度为)
O(
。通过改变增量的大小,比如将增量保持在)
2k−1
(1、3、7、15、31 等),希尔排序的时间复杂度可以达到O(
。)
编程要求
根据提示,在右侧编辑器中的 Begin-End 区间补充代码,根据希尔排序的算法思想完成shellSort
和gapInsertionSort
方法,从而实现对无序表的排序。
测试说明
平台会对你编写的代码进行测试,比对你输出的数值与实际正确的数值,只有所有数据全部计算正确才能通过测试:
测试输入:
54,26,93,17,77,31,44,55,20
输入说明:输入为需要对其进行排序的无序表。
预期输出:
增量为 4 : [20, 26, 44, 17, 54, 31, 93, 55, 77]
增量为 2 : [20, 17, 44, 26, 54, 31, 77, 55, 93]
增量为 1 : [17, 20, 26, 31, 44, 54, 55, 77, 93]
输出说明:输出的是对无序表进行希尔排序的每一趟排序的结果,以列表的形式展现。其中增量的取值从 n/2 开始,之后的每趟减少到 n/4……直到 1。在本例测试数据中,数据项个数为 9,则增量序列为:4、2、1。
测试输入:
49,38,65,97,76,13,27
预期输出:
增量为 3 : [27, 38, 13, 49, 76, 65, 97]
增量为 1 : [13, 27, 38, 49, 65, 76, 97]
提示:
for i in range(0, 30, 5): # 步长为 5
print(i, end=" ")
print('\n')
for j in range(1, 30, 5):
print(j, end=" ")
输出:
0 5 10 15 20 25
1 6 11 16 21 26
开始你的任务吧,祝你成功!
'''请在Begin-End之间补充代码, 完成shellSort和gapInsertionSort函数'''
# 希尔排序
def shellSort(alist):
sublistcount = len(alist) // 2 # 设定初始增量为n/2
while sublistcount > 0: # 不断缩小增量,进行多趟排序
for startposition in range(sublistcount): # 每进行一次循环就对某一个子列表进行排序
# 调用gapInsertionSort函数对子列表进行排序
# ********** Begin ********** #
gapInsertionSort(alist,startposition,sublistcount)
# ********** End ********** #
print("增量为",sublistcount,":",alist)
sublistcount = sublistcount // 2
# 带间隔的插入排序
def gapInsertionSort(alist,start,gap):
for i in range(start+gap,len(alist),gap): # 循环的次数表示插入排序的趟数
currentvalue = alist[i] # 当前插入项的值
position = i # 当前插入项所在的位置
# 当 position-gap 位置有数据项 且 当前插入项小于 position-gap 位置的数据项,就不断地进行以下操作
# 将 position-gap 位置的数据项在子列表中向右移动一个位置
# position 指向 position-gap 位置
# ********** Begin ********** #
while position>=gap and alist[position-gap]>currentvalue:
alist[position]=alist[position-gap]
position=position-gap
# ********** End ********** #
alist[position]=currentvalue # 找到当前插入项的插入位置