希尔排序是一种基于插入排序的高效的排序算法,
它的基本思想
- 将待排序的序列分成若干个子序列,对每个子序列进行直接插入排序,然后逐渐减小子序列的间隔,直到间隔为1时,对整个序列进行最后一次直接插入排序。
1. 希尔排序的原理
希尔排序是由D.L.Shell于1959年提出的一种改进的插入排序算法,它的核心思想是利用插入排序在对基本有序的数据操作时效率高的特点,通过分组和缩小增量来加速排序过程。我们先来回顾一下插入排序的过程:
- 插入排序是将数据分为有序区和无序区,每次从无序区中取出一个元素,插入到有序区中合适的位置,直到无序区为空。
- 插入排序的时间复杂度取决于数据的初始状态,
- 如果数据已经基本有序,那么插入排序只需要进行少量的比较和移动操作,时间复杂度为O(n);
- 如果数据完全逆序,那么插入排序需要进行n(n-1)/2次比较和移动操作,时间复杂度为O(n^2)。
那么,如何让数据更接近于有序呢?
希尔排序的策略是
将数据按照一定的间隔分成若干个子序列,然后对每个子序列进行插入排序,这样可以使得数据在局部范围内有序,从而减少后续插入排序的工作量。
接着,再逐渐缩小间隔,重复上述过程,直到间隔为1时,对整个数据进行最后一次插入排序。这样可以保证数据在全局范围内有序。
2 希尔排序算法图解
以序列: {8, 9, 1, 7, 2, 3, 5, 6, 4, 0} 为例!
初始步长gap = length/2 = 5,意味着将整个数组分为了5组,即[8,3],[9,5],[1,6],[7,4],[2,0],对每组进行插入排序,得到序列:{3,5,1,4,0,8,9,6,7,2},可以看到:3,5,4,0这些小元素都被提到前面了。
缩小增量gap = 5/2 = 2,数组被分为两组,即[3,1,0,9,7],[5,4,8,6,2],对这两组分别进行直接插入排序,可以看到,整个数组的有序程度更进一步了。
再次缩小增量,gap = 2/2 = 1,此时整个数组为[0,2,1,4,3,5,7,6,9,8],进行一次插入排序,即可实现数组的有序化(仅需要简单微调,而无需大量移动操作)。
3. 希尔排序的实现
要实现希尔排序,我们需要确定一个合适的间隔序列,也就是每次分组的间隔大小。这个间隔序列会影响希尔排序的性能,不同的间隔序列可能会导致不同的时间复杂度。目前还没有一个统一的标准来确定最优的间隔序列,但是有一些经验公式可以参考。比如,我们可以采用以下公式来生成间隔序列:
- h(1) = n / 2
- h(i) = h(i-1) / 2
- h(k) = 1
其中,n是数组的长度,k是最大的下标,满足h(k) = 1。这个公式就是我们上面例子中使用的公式。当然,还有其他的公式,比如:
- h(1) = 1
- h(i) = 3 * h(i-1) + 1
- h(k) < n
这个公式可以生成一个更大的间隔序列,比如对于n=10,它会生成{1, 4, 13}。有研究表明,这个公式可以使得希尔排序的时间复杂度达到O(n^(3/2))。
无论采用哪种公式,我们都需要从大到小依次取出间隔值,然后按照间隔值来分组和排序。
4. Java 实现
下面是用Java语言实现的希尔排序算法:
public static void shellSort(int[] arr) {
// 数组长度
int n = arr.length;
// 初始间隔为n/2
int gap = n / 2;
// 循环直到间隔为1
while (gap > 0) {
// 对每个间隔进行插入排序
for (int i = gap; i < n; i++) {
// 取出当前元素
int temp = arr[i];
// 找到插入位置
int j = i - gap;
while (j >= 0 && arr[j] > temp) {
// 后移元素
arr[j + gap] = arr[j];
j -= gap;
}
// 插入元素
arr[j + gap] = temp;
}
// 缩小间隔
gap /= 2;
}
}
5. 希尔排序的性能分析
希尔排序的时间复杂度和空间复杂度都和间隔序列有关,一般来说:
- 时间复杂度:平均情况下为O(nlogn),最好情况下为O(n),最坏情况下为O(n^2)。
- 空间复杂度:O(1),只需要一个额外的变量来存储当前元素。
- 稳定性:不稳定,因为同一个间隔内的元素可能会被交换。
希尔排序是一种简单而高效的排序算法,它可以克服直接插入排序在处理大规模数据时的低效问题,同时又保持了插入排序在处理基本有序数据时的高效问题。它是一种适用于中等规模数据的排序算法,对于非常大或非常小的数据,可能不太合适。