希尔排序又叫递减增量排序算法
,它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的;希尔排序是一种不稳定的排序算法。
1. 希尔排序的基本原理
我们之前讲过直接插入排序,它的算法复杂度为O(n^2),整体来说它的效率很低;但是在两种情况下它表现得很好,我们这里将这两种情况归纳为直接插入排序的两种性质:
-
当待排序的原序列中大多数元素都已有序的情况下,此时进行的元素比较和移动的次数较少;
-
当原序列的长度很小时,即便它的所有元素都是无序的,此时进行的元素比较和移动的次数还是很少。
希尔排序去模拟这两种条件,让插入排序效率更高,这就涉及到分组插入排序。
它首先将待排序的原序列划分成很多小的序列,称为子序列。然后对这些子序列进行直接插入排序,因为每个子序列中的元素较少,所以在它们上面应用直接插入排序效率较高(利用了上面的性质2)。这样的过程可能会进行很多次,每一次都称为一趟,每一趟都将前一趟得到的整个序列划分为不同的子序列,并再次对这些子序列进行直接插入排序。最后由这些子序列组成的整个序列中的所有元素就基本有序了,此时再在整个序列上进行最后一次的直接插入排序(利用了上面的性质1),此后整个序列的排序就完成了。
2. 子序列的划分方式
2.1 平均划分不可行的原因
希尔排序最关键的地方就是如何对整个序列进行划分
。我们最直接的想法可能就是按顺序对整个序列进行平均划分,比如有n个元素的序列,我们要把它划分成i个子序列,每个子序列有m个元素(假设n = i * m,当n不能被i整除时可以让最后一个子序列的元素少于其它子序列)。该想法就是让原序列的第0 ~ m-1个元素为第一个子序列(第一个元素的下标为0),第m ~ 2m-1个元素为第二个子序列,以此类推,最后的第n-m ~ n-1个元素为最后一个子序列。
这样的划分虽然简单但是它却不适合希尔排序,这样划分并对子序列进行直接插入排序后,虽然每个子序列中的元素都是有序的,但整个原序列依旧是很无序的。为了便于理解为什么这种方式不行,我们用下图来对它进行说明。
在图1中,原序列有9个元素,我们将它按顺序划分为3个子序列。即最前面的3个元素为第一个子序列,中间3个元素为第二个子序列,最后3个元素为最后一个子序列;图中我们用不同的颜色表示不同的子序列。
可以看到,对每个子序列应用直接插入排序后,虽然每个子序列都是有序的,但整个序列还是很无序的。此时,在整个序列上进行直接插入排序的效率还是很低。整个序列依旧无序的原因是每个元素只在它所在的子序列中移动,它的新位置离它的最终位置(即整个序列排好序后的位置)还是很远。
因此,子序列划分的方法必须保证对子序列进行排序后,每个元素在整个序列中的移动范围更大。这样跳跃式的位置移动,才可能让每个元素离它的最终位置较近,因而整个序列才是比较有序的。
2.2 增量方式进行子序列划分
希尔排序采用的是按增量的方式进行子序列划分,将整个序列中下标值相差固定值(增量)的所有元素划分到同一个子序列中。比如,整个序列有9个元素,而增量为3,那么第0、3、6个元素属于第一个子序列,第1、4、7个元素属于第二个子序列,第2、5、8个元素属于最后一个子序列。
为了便于理解,我们同样用图2来展示这种增量划分方式。我们同样用不同的颜色表示属于不同子序列的元素,并标出了每个子序列的元素的下标值。可以看到,以增量方式划分的子序列在整个序列中是交错出现的。
按照增量划分的时候,假设增量为increment,那么整个序列也将划分为increment个子序列。可以这样理解,我们遍历整个序列中的每个元素,并为每个元素指定它所属的子序列。首先是下标为0的元素,它属于第一个子序列;然后是下标为1的元素,它属于第二个子序列;以此类推,可知前increment个元素(下标为0 ~ increment-1)属于不同的子序列,共计increment个。从下标为increment的元素开始,每一个元素的下标值减去increment都大于或等于0,即这些元素都属于一个已存在的子序列。因此,整个序列将被划分为increment个子序列。
在实际的应用中,待排序的原序列可能有很多个元素,成千上万甚至上亿。此时,为了充分利用希尔排序的效率,应该进行多趟排序
,每一趟用不同的(严格说是递减的)增量
对整个序列进行划分。即首先用增量i1对原序列进行划分,并对每个子序列进行直接插入排序;然后对前一步得到的整个序列用一个新的且较小的增量i2(i2小于i1)进行划分,并对每个子序列进行直接插入排序;然后又对前一步得到的整个序列用一个更小的增量i3(i3小于i2)进行划分,并对每个子序列进行直接插入排序。以此类推,直到最后增量为1,此时可以认为整个序列就是一个唯一的子序列,对它进行直接插入排序之后整个原始序列都是有序的了,希尔排序算法结束。
3. 希尔排序的代码实现
- 先分组,递减增量使得每一组元素变少,降低元素个数
- 然后对每一组分别进行插入排序
- 整体更趋近于元素序列基本有序
- 最后一次的增量值必为1,进行直接插入排序
3.1 核心代码
//希尔排序
void shell_sort(int arr[], int length)//升序
{
int increasement = length;
int i, j, k;
do
{
//确定分组增量,递减,每次计算increasement,最后肯定为1,再对所有数据进行一次插入排序
increasement = increasement / 3 + 1;
for (i = 0; i < increasement; i++) //确定每一组第一个元素
{
//j开始为一组的第二个元素,跨度为increasement
for (j = i + increasement; j < length; j += increasement)
{
if (arr[j] < arr[j - increasement])
{
int temp = arr[j];
for (k = j - increasement; k >= 0; k -= increasement)
{
if (arr[k] > temp)
{
arr[k + increasement] = arr[k];
}
else
{
break;
}
}
arr[k + increasement] = temp;
}
}
}
} while (increasement > 1);
}
3.2 整体代码实现
#include <iostream>
using namespace std;
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//打印数组
void printArr(int arr[])
{
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
}
//希尔排序
void shell_sort(int arr[], int length)//升序
{
int increasement = length;
int i, j, k;
do
{
//确定分组增量,递减,每次计算increasement,最后肯定为1,再对所有数据进行一次插入排序
increasement = increasement / 3 + 1;
for (i = 0; i < increasement; i++) //确定每一组第一个元素
{
//j开始为一组的第二个元素,跨度为increasement
for (j = i + increasement; j < length; j += increasement)
{
if (arr[j] < arr[j - increasement])
{
int temp = arr[j];
for (k = j - increasement; k >= 0; k -= increasement)
{
if (arr[k] > temp)
{
arr[k + increasement] = arr[k];
}
else
{
break;
}
}
arr[k + increasement] = temp;
}
}
}
} while (increasement > 1);
printArr(arr);
}
int main()
{
int arr[] = { 8,2,3,9,6,4,7,1,5,10 };
shell_sort(arr, 10);
system("pause");
return 0;
}
运行结果: