作为一名对技术充满热情的学习者,我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代,我远非专家,而是一位不断追求进步的旅行者。通过这篇博客,我想分享我在某个领域的学习经验,与大家共同探讨、共同成长。请大家以开放的心态阅读,相信你们也会在这段知识之旅中找到启示。
前言
大家好!新年快乐哦,年后我们学习还要继续,博主依然要定时发文,今天我们一起来讲讲另一个排序算法----希尔排序
一、什么是希尔排序
希尔排序是一种基于插入排序的排序算法,由Donald Shell于1959年提出。其关键的概念是将原来要排序的列表划分成若干个较小的子列表,分别进行插入排序,然后逐渐减少子列表的数量,直到全列表作为一个列表来对待进行插入排序为止
。这种方法中,每个子列表实际上是间隔一定"增量"的一组元素构成的。
希尔排序算法的步骤概括为:
- 选择一个增量序列
t1, t2, ..., tk
,其中ti > tj
,tk = 1
(通常使用序列中每个元素作为n/2
的结果,n
是要排序的元素的数量)。 - 按增量序列个数
k
,对序列进行k
轮排序。 - 每轮排序,根据对应的增量
ti
,将待排序列分割成若干长度为m
的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序算法的效率与增量序列的选择有很大关系。在最好的情况下,使用合适的增量序列,希尔排序的时间复杂度可以达到O(nlogn)
,但最坏情况下可能为O(n^2)
。
以下是希尔排序的一个基本实现示例:
public class ShellSort { public static void shellSort(int[] array) { int n = array.length; // Gaps序列以n/2开始,并逐步递减至1 for (int gap = n / 2; gap > 0; gap /= 2) { // 从gap开始对所有元素进行插入排序 for (int i = gap; i < n; i++) { // 添加array[i]到已排序的序列中 int temp = array[i]; int j; for (j = i; j >= gap && array[j - gap] > temp; j -= gap) { array[j] = array[j - gap]; } array[j] = temp; } } } }
关于希尔排序的时间复杂度
希尔排序的时间复杂度是依赖于其所使用的增量序列的
。目前还没有确切的公式能够描述所有情况下希尔排序的时间复杂度,因为不同的增量序列会导致算法行为大不相同。
对于最原始的希尔排序,即增量序列取值为n/2, n/4, ..., 1
,平均和最坏的时间复杂度大约在O(n^1.5)
左右。但这并不是最优的增量序列,随着研究的不断深入,人们发现了更好的增量序列,使得希尔排序的性能得到大幅提升。
例如,使用Hibbard的增量序列(1, 3, 7, ..., 2^k - 1
)时,希尔排序的最坏情况时间复杂度是O(n^1.5)
。而使用Sedgewick提出的一种增量序列的希尔排序,时间复杂度可以达到O(n^1.3)
。有更复杂的增量序列,如Pratt序列,可以让希尔排序达到几乎接近O(nlogn)
的性能。
除了时间复杂度,希尔排序的另一个重要性能标准是其比较次数和移动(或交换)次数。由于希尔排序在前面的步骤可能会进行大范围的移动,后面的排序步骤需要的移动和比较就会较少。
综上所述,希尔排序的时间复杂度无一定的定论,它受所选择的增量序列的影响很大,且具体情况下的性能也受到数据初始状态的影响。尽管如此,希尔排序通常比传统的插入排序快,特别是在数据量较大的情况下。
二、希尔排序相对于传统的插入排序有什么优势
希尔排序相对于传统的插入排序有几个明显的优势:
- 处理大量无序数据的能力: 在插入排序中,如果一个元素需要被移动到很远的位置,它必须逐步通过多次移动到达最终位置。而希尔排序允许交换相距较远的元素,使得可以一步到位地将一个元素移动到很远的位置,这对于大量无序的数组来说尤其有效。
- 减少逆序的对数: 希尔排序通过先进行宽间隔的排序来减少整个数组中逆序对(即不正确顺序的元素对)的数量,从而使后续的插入排序步骤变得更加高效。
- 更好的子数组性能: 由于希尔排序是在多个子数组上分别进行插入排序,当数组的大小缩小后,插入排序在小数组上表现是非常高效的,因为它利用了局部性原理。
- 适用性广: 希尔排序不需要大量的辅助空间,只需要一个额外的元素空间进行交换,因此它是一种原地排序算法。这意味着它在空间限制的环境中是非常实用的。
- 适度的递增序列效率非常高: 通过适当地选择递增序列,可以使希尔排序在实际应用中表现出比其他时间复杂度为O(n^2)的算法(如简单插入排序、冒泡排序等)更优秀的性能。
尽管希尔排序在理论上的最坏情况运行时间比某些高级排序算法(如归并排序、快速排序)要慢,但由于其相对简单和低开销的特点,在处理中等大小的数组时它通常会表现得非常高效。因此,在需要考虑算法实现的复杂性和排序效率时,它是一个不错的选择。
三、练习
简单习题
数组 [8, 6, 7, 9, 5]
。
假设我们选择的增量序列是简单的除以2策略,第一个增量为 n/2(向下取整),接着是 n/4,依此类推,直到增量减到1。
初始数组:[8, 6, 7, 9, 5]
第一轮排序(增量为 5/2 = 2,向下取整):
分两组进行排序:
- 第一组:[8, 7]
- 第二组:[6, 9]
在这种情况下,因为我们的数组较小,每组数据最多只有两个元素,因此在这个步骤中,这两个子数组都被认为是有序的。
第二轮排序(增量为 2/2 = 1,即最终的插入排序):
从第二个元素开始(因为第一个元素本身已经是“有序的”,作为一元数组),将每个元素与前面的元素比较进行插入排序:
- 比较6和8:[6, 8, 7, 9, 5] (6和8交换位置)
- 比较7和8:[6, 7, 8, 9, 5] (7插入到8前面,因为已经比8小了)
- 比较9和8:不需要操作,因为9大于前面的8
- 比较5与前面的所有元素:
- 比较5和9:[6, 7, 8, 5, 9] (5和9交换位置)
- 比较5和8:[6, 7, 5, 8, 9] (5和8交换位置)
- 比较5和7:[6, 5, 7, 8, 9] (5和7交换位置)
- 比较5和6:[5, 6, 7, 8, 9] (5插入到第一个位置)
最终排序后的数组是 [5, 6, 7, 8, 9]。
这个过程演示了希尔排序如何通过首先使用较大的增量减少逆序对,然后再进行最终的插入排序,从而高效地对数组进行排序。这个例子中的数组较小,所以看起来希尔排序和插入排序差异不大,但在处理更大的数组时,希尔排序的效率提升将会更加明显。
较难习题
初始数组:[33, 10, 14, 27, 35, 19, 42, 44, 31, 3]
我们仍然可以使用递减的增量序列,这里我们选择增量序列为 n/2, n/4, …, 1(对每个增量向下取整)。
第一轮排序(增量为 10/2 = 5):
分五组进行排序:
- 第一组:[33, 19]
- 第二组:[10, 42]
- 第三组:[14, 44]
- 第四组:[27, 31]
- 第五组:[35, 3]
对每组使用插入排序(由于只有两个元素,所以如果第二个元素比第一个小就交换它们):
- 组1:19 比 33 小,交换它们。新数组:[19, 10, 14, 27, 3, 33, 42, 44, 31, 35]
- 组2、组3、组4 由于都是由数目只有两个的组,而且都是有序的,所以不需要交换。
- 组5:3 比 35 小,交换它们。新数组 (更新组5后):[19, 10, 14, 27, 3, 33, 42, 44, 31, 35]
第二轮排序(增量为 5/2 = 2):
分为两组进行排序:
- 奇数索引组:[19, 14, 3, 42, 31]
- 偶数索引组:[10, 27, 33, 44, 35]
对每组使用插入排序:
- 奇数索引组变成了 [3, 14, 19, 31, 42]
- 偶数索引组变成了 [10, 27, 33, 35, 44]
合在一起的新数组:[3, 10, 14, 27, 19, 31, 33, 35, 42, 44]
第三轮排序(逐元素插入排序,增量为 1):
使用插入排序对整个数组排序:
- 10, 14, 27, 19, 31, 33, 35, 42, 44 都已经有序。
- 3是最小的元素,已经在正确的位置。
最终结果没有变化,因为在上一轮中,经过奇数和偶数分组的插入排序之后,数组已经是完全有序的了。这恰好是一个较为理想的场景,在实际应用中可能很少见。实际情况下,最后一轮通常需要一些移动和比较才能将所有元素排序到最终位置。
排序后的数组:[3, 10, 14, 27, 19, 31, 33, 35, 42, 44]
注意,上面的步骤可能需要更多的交换和比较才能达到完全有序,上述过程省略了部分细节。希尔排序的效果很大程度上依赖于选择的增量序列。在不同环境和应用领域,可能会有不同的增量序列以期望获得更优的性能。
四、Java面试题
- Java面试题:
请在Java中实现希尔排序,并解释其时间复杂度与工作原理。此外,请分析希尔排序比起直接插入排序的优势。
解答例子:
public class ShellSort { public static void shellSort(int[] array) { int n = array.length; // Start with a big gap, then reduce the gap for (int gap = n / 2; gap > 0; gap /= 2) { // Do a gapped insertion sort for this gap size. // The first gap elements array[0..gap-1] are already in gapped order // keep adding one more element until the entire array is gap sorted for (int i = gap; i < n; i += 1) { // add array[i] to the elements that have been gap sorted // save array[i] in temp and make a hole at position i int temp = array[i]; // shift earlier gap-sorted elements up until the correct location for array[i] is found int j; for (j = i; j >= gap && array[j - gap] > temp; j -= gap) { array[j] = array[j - gap]; } // put temp (the original array[i]) in its correct location array[j] = temp; } } } public static void main(String[] args) { int[] exampleArray = {33, 10, 14, 27, 35, 19, 42, 44, 31, 3}; shellSort(exampleArray); for (int i : exampleArray) { System.out.print(i + " "); } } }
时间复杂度:
希尔排序的最坏情况和平均情况运行时间都依赖于所使用的增量序列。对于上面代码中使用的增量序列(n/2, n/4, …, 1),最坏情况时间复杂度为O(n2),但是实际的性能可能比O(n2)好一些,因为它启动时每个子列表都比较短。对于其他增量序列,特别是最好的已知序列,时间复杂度可以降至O(n log^2 n)。不过,希尔排序的确切时间复杂度仍然是一个开放的问题。
工作原理:
希尔排序是基于插入排序的以下两个性质而提出改进方法:
- 插入排序在对几乎已经排好序的数据操作时,效率高(即数据越接近最终排列的顺序越好),即可达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动到相邻的位置,每次只能移动一步。
希尔排序的基本思想是将数组列在一个表中并对列分别进行插入排序,从而将不相邻的元素进行比较,以移动那些距离正确位置很远的元素。然后逐渐减小间隔(增量gap),继续排序,最终增量为1并执行普通的插入排序,此时文件已经基本有序,所以最后这步会很快。
优势分析:
相较于直接插入排序,希尔排序的优势在于它允许交换的项之间有较大的间隔,使得较小的项可以一次性通过多次交换达到较远的位置。因此,希尔排序对于大规模的乱序数组来说,比直接插入排序有更高的执行效率。
该面试题评估了求职者对排序算法的理解和代码实现能力,并且通过要求解释希尔排序的工作原理及优势,检验了求职者的分析和表达能力。此外,代码质量和可读性也是评估的一个重要方面。
总结
以上就是对希尔排序的初步总结,同学们还需要加强训练,对于算法的掌握就是理解原理后在不同场景下频繁的使用,才能够真正的熟练掌握数据结构这门课程。
感谢大家抽出宝贵的时间来阅读博主的博客,新人博主,感谢大家关注点赞,祝大家未来的学习工作生活一帆风顺,加油!!!