打牌是很多人喜欢的与亲朋好友一起娱乐的一种方式。在牌局开始,打牌的各位都要依此摸牌。与此同时,为了方便后续出牌,很多人对摸到的牌从大到小排序。这种排序手法,在算法中,被称作“插入排序”。
为了理解“插入排序”的主要流程,我们先来简单地回顾一下对牌排序的流程。
每摸一张牌,我们都会将这张牌和手上的牌依次进行对比,找到合适的位置插入,保证手上的牌是有序的。下次摸牌时,也是类似的流程。最终,手上会是一个有序的牌组。
将上述流程抽象概括一下,就是:每次将一个待排序的数据,在前面已排好序的子序列中从后向前扫描, 按其值大小找到适当的位置并插入其中,直到序列中的全部数据都插入完毕为止。
这个算法的流程在排序牌的例子中已经有了非常直观的解释,这里我们直接进入代码实现环节。
1 | def insert_sort(lst): |
假设待排序的数据量为 \(N\) ,依据上述插入排序算法,外层循环将执行 \(N\) 次,内层循环依据外层循环次数 \(i\) 将执行 \(i\) 次(将两个内层循环次数相加即可)。所以该算法至多执行 \[ \begin{aligned} &\ \ \ \ \ 1+2+3+ ··· +N-1\\ &= (N-1)(1 + N – 1)/2\\ &= N(N-1)/2 \end{aligned} \] 次。所以插入排序的时间复杂度为 \(O(N^{2})\) 。
插入排序算法在时间性能上来说,表现得并不理想。能不能对他进行优化呢?希尔(Donald Shell)提出了他对该算法的改进。这个算法描述如下:
取一个小于 \(n\) 的整数 \(d_{1}\) 作为第一个增量, 把序列中的全部数据分成 \(d_{1}\) 个组。 所有距离为 \(d_{1}\) 的倍数的数据放在同一个组中;
\(n\) 在各组内进行直接插入排序;
\(n\) 取第二个增量 \(d_{2} < d_{1}\) , 重复上述的分组和排序, 直至所取的增量 \(d_{t}=1(d_{t}< …<d_{2}<d_{1})\) , 即所有数据都放在同一组中进行直接插入排序为止。
所以这个算法本质是一种分组插入排序。除了插入排序的思想,更关键的是算法中增量序列d的确定。
有一种增量叫做Hibbard增量,其表达式如下:
\[ h_{i} = 2^{i} - 1 \]
它可以使希尔排序的时间复杂度降低到 \(O(N^{\frac{3}{2}})\) 。
我们以这种增量序列生成方式来实现一下希尔排序。
1 | # 参考:https://baike.baidu.com/item/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F/3229428?fr=aladdin#7_8 |
大家可以对照上述程序的执行过程来体会希尔排序的排序流程。在实现希尔排序的过程中,默认采用了函数嵌套的写法。同时,我也写了不用函数嵌套的写法。(将5054注释,将5672行取消注释,即是不用函数嵌套的写法)。
在文章的最后,照例进行总结:
- 插入排序的主要流程:每次将一个待排序的数据,在前面已排好序的子序列中从后向前扫描,按其值大小找到适当的位置并插入其中,直到序列中的全部数据都插入完毕为止。
- 插入排序的时间复杂度为 \(O(N^{2})\) 。
- 一种插入排序的改进算法,希尔排序,本质是一种分组排序方法。
- 利用Hibbard增量生成希尔排序中需要的增量序列,可以使得其时间复杂度变为 \(O(N^{\frac{3}{2}})\) 。