希尔排序


我对希尔排序进行学习的博客:希尔排序–简单易懂图解

前言

可以说是一个加强版的插入排序。在插入排序中,最好的时间复杂度是 O ( N ) O(N) O(N)可以说是极其舒服的线性时间复杂度,而这最好的情况,就是当数组中所有的数都是有序的时候,这是 O ( N ) O(N) O(N)的复杂度。或者稍微比较好的情况,就是数组中大部分数都是有序的时候,那么,插入排序的时间复杂度,将趋近于 O ( N ) O(N) O(N),也就是线性的复杂度。而希尔排序的思想本质就是让数组中的数在尽可能少的次数内,变得有序起来,从而缩短整体时间复杂度,当然这思想没有脱离插入排序的基本做法,所以希尔排序的最坏时间复杂度,依然是 O ( N 2 ) O(N^2) O(N2),但在平均的情况下,希尔排序的时间复杂度将会是挺可观的。

基本做法

对数组进行分组(逻辑上的分组),然后对每个分组进行插入排序,使分组中的数变得有序,因为被分组后,每个分组中的数都不是很多,所以单次分组后的插入排序相对来说不会有过高的时间复杂度。然后经过多次分组然后执行插入排序后,数组中的数在整体上的有序程度会逐步拔高,直到最后一次对整体的插入排序,时间复杂度基本趋近于线性 O ( N ) O(N) O(N)。而这个分组的依据就很关键了,在希尔排序中使用的则是与计算机紧密相关的数2

例:长度为8的数组
每次对数组(整除2所得到的间隔值(gap))进行分组,然后对每个分组进行插入排序,因为最后间距(gap)必定是1,所以,前面的可以说是利用数字2的特性,进行的数组有序程度的提高,然后最后进行一个时间复杂度相对低的整体插入排序。最终达到整体时间复杂度的降低。

第一次分组: 8 / / 2 = 4 8 // 2 = 4 8//2=4

第二次分组: 4 / / 2 = 2 4 // 2 = 2 4//2=2

第三次分组: 2 / / 2 = 1 2 // 2 = 1 2//2=1

代码构造思路

注:下面所说的分组有序数组都是逻辑上的。

首先最外层的循环,自然是要套上每次的分组依据,也就是gap值的循环for (int gap = len >> 1; gap > 0; gap >>= 1)。数组中的每个数相隔gap个位置为一组。其次的第二层循环则是要循环上当前需要排序的整个数组for (int i = gap; i < len; ++i),而第二层循环从gap开始,则是因为,数组中gap值前面位置的数在各自分组中都是第一个数,而gap值的位置开始到最后的数,一开始都还没有插入到各自的有序分组之中,所以每个分组,当前只有一个数,而分组中只有一个数,自然这个分组当前是有序的。最后的insertI(nums, gap, i),其实就跟插入排序差不了多少,把当前数插入到有序数组中正确的位置即可。

代码

class Solution {
public:
	Solution() {}
	~Solution() {}

	void insertI(vector<int>& nums, int gap, int i) {
		int insertNum = nums[i];
		int j;
		for (j = i - gap; j >= 0 && insertNum < nums[j]; j -= gap) {
			nums[j + gap] = nums[j];
		}
		nums[j + gap] = insertNum;
	}

	vector<int> sortArray(vector<int>& nums) {
		int len = nums.size();
		for (int gap = len >> 1; gap > 0; gap >>= 1) {
			for (int i = gap; i < len; ++i) {
				insertI(nums, gap, i);
			}
		}
		return nums;
	}
};

时间复杂度

平均情况 最好情况 最坏情况
O ( N 1.3 ) O(N^{1.3}) O(N1.3) O ( N ) O(N) O(N) O ( N 2 ) O(N^2) O(N2)

注:这平均情况的时间复杂度不太好算。

空间复杂度

辅助存储
O ( 1 ) O(1) O(1)

PS:这个算法很明显是不稳定的