希尔排序

1.1 原理

希尔排序是一种基于直接插入排序的排序算法,也称为“缩小增量排序”

希尔排序法的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序

希尔排序:基于数组(顺序表)的结构进行排序

希尔排序是按其设计者希尔的名字命名的

他对普直接入排序的时间复杂度进行分析,得出了以下结论:

  1. 直接插入排序的时间复杂度最坏情况下为O(N^2),此时待排序列为逆序,或者说接近逆序
  2. 直接插入排序的时间复杂度最好情况下为O(N),此时待排序列为升序,或者说接近升序

于是希尔就想:若是能先将待排序列进行一次预排序,使待排序列接近有序(接近我们想要的顺序),然后再对该序列进行一次直接插入排序,此时已经排好序了,并且时间复杂度也降低了

希尔排序 = 预排序 + 一次直接插入排序

因为此时直接插入排序的时间复杂度为O(N)(直接插入排序对排序的数组接近有序的时候,时间复杂度为O(1)),那么只要控制预排序阶段的时间复杂度不超过O(^2),那么整体的时间复杂度就比直接插入排序的时间复杂度低了

  1. 首先选择一个增量序列,通常选择增量序列gap为n/2,n/4,n/8...1,其中n为数组的长度
  2. 根据增量序列将数组分成若干个子序列,对每个子序列进行插入排序
  3. 逐渐缩小增量序列,重复步骤2,直到增量为1,即对整个数组进行一次插入排序

例如,对该数组使用希尔排序进行排序(升序)
【排序算法】二、希尔排序(C/C++)-LMLPHP

取第一次增量gap为:gap = 10/2,10为数组大小,此时相隔距离为5的元素被分为一组(共分了5组,每组有2个元素),然后分别对每一组进行直接插入排序
【排序算法】二、希尔排序(C/C++)-LMLPHP
取第二次增量gap为:gap = 5/2,5为第一次增量值,此时相隔距离为2的元素被分为一组(共分了2组,每组有5个元素),然后再分别对每一组进行直接插入排序
【排序算法】二、希尔排序(C/C++)-LMLPHP
取第三次增量gap为:gap = 2/2,2为第二次增量值,此时gap为1,即整个序列被分为一组,进行一次直接插入排序(对整个数组)
【排序算法】二、希尔排序(C/C++)-LMLPHP
动图演示如下:
【排序算法】二、希尔排序(C/C++)-LMLPHP

  • gap越大,数据挪动得越快;gap越小,数据挪动得越慢
  • 前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数

:一般情况下,取序列的一半作为增量,然后依次减半,直到增量为1(也可自己设置)

希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在不同的书中给出的希尔排序的时间复杂度都不固定
【排序算法】二、希尔排序(C/C++)-LMLPHP
【排序算法】二、希尔排序(C/C++)-LMLPHP
Knuth序列是一种希尔排序中常用的增量序列,由Donald Knuth提出。Knuth序列的计算公式为:h = 3h + 1,其中h是增量序列的值,下面代码gap使用这个公式(有兴趣自己了解)

Knuth进行了大量的试验统计,暂时就按照:O(N^1.25)O(1.6 * N^1.25)来算,按O(N^1.3)计算(最好情况下)

  • 希尔排序时间复杂度O(N*logN) ~ O(N^2),以2为底,最好O(N^1.3),最坏 O(N^2)
  • 希尔排序空间复杂度O(1)

1.2 代码实现(C/C++)

C语言代码如下:(升序)

// 希尔排序(基于直接插入排序)
void ShellSort(int* arr, int n) // arr:需要排序的数组; n:数组的大小
{
	int gap = n;
	while (gap > 1)
	{
		// gap 大于 1, 进行预排序
		// gap == 1, 相当于直接插入排序(注:循环进来 gap 经过 gap = gap / 3 + 1 这才变成 1 )
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; ++i)
		{
			int end = i; // 记录有序列的最后一个下标
			int tmp = arr[end + gap]; // 保存等待插入的值

			while (end >= 0)
			{
				if (arr[end] > tmp) // arr[end]比要插入的数大,向后移动
				{
					arr[end + gap] = arr[end]; // 向后移,进行覆盖,tmp已经保存被覆盖的值
					end -= gap;
				}
				else // arr[end]比要插入的数小,已有序,跳出循环
				{
					break; 
				}
			}
			arr[end + gap] = tmp; // 进行插入
		}
	}
}

C++代码:(升序)

// 希尔排序(基于直接插入排序)
void ShellSort(vector<int>& arr)
{
	int n = arr.size();
	int gap = n;
	while (gap > 1)
	{
		// gap 大于 1, 进行预排序
		// gap == 1, 相当于直接插入排序(注:循环进来 gap 经过 gap = gap / 3 + 1 这才变成 1 )
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; ++i)
		{
			int end = i; // 记录有序列的最后一个下标
			int tmp = arr[end + gap]; // 保存等待插入的值

			while (end >= 0)
			{
				if (arr[end] > tmp) // arr[end]比要插入的数大,向后移动
				{
					arr[end + gap] = arr[end]; // 向后移,进行覆盖,tmp已经保存被覆盖的值
					end -= gap;
				}
				else // arr[end]比要插入的数小,已有序,跳出循环
				{
					break;
				}
			}
			arr[end + gap] = tmp; // 进行插入
		}
	}
}

1.3 特性总结

  • 希尔排序是对直接插入排序的优化
  • gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。整体而言,可以达到优化的效果
  • 希尔排序时间复杂度O(N*logN) ~ O(N^2),以2为底
  • 空间复杂度O(1)
  • 稳定性:不稳定

--------------------- END ----------------------

「 作者 」 枫叶先生
「 更新 」 2024.1.9
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。
01-13 08:14