希尔排序可以看作“带间隔的插入排序”。当 gap 较大时,元素可以一次跨过很远的位置,快速消除严重逆序;当 gap 最终变为 1 时,算法退化为普通插入排序,但此时序列通常已经接近有序,所以最后一轮成本会明显下降。它不稳定的原因是相同关键字可能在不同 gap 子序列中被跨距离移动,破坏原来的相对顺序。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
template <classKEY, classOTHER> voidshellSort(SET<KEY, OTHER> a[], int size){ int step, i, j; SET<KEY, OTHER> tmp; for (step = size / 2; step > 0; step /= 2) { for (i = step; i < size; ++i) { tmp = a[i]; // 保存当前待插入元素 for (j = i - step; j >= 0 && a[j].key > tmp.key; j -= step) { a[j + step] = a[j]; // 按gap间隔后移 } a[j + step] = tmp; // 插入到当前gap子序列中的正确位置 } } }
效率分析
不同的增量序列有不同的时间性能;
希尔建议 gap 从 N/2 开始,逐渐平分减小到 1;
最坏时间复杂度为 O(n2),但平均时间复杂度通常可粗略记为 O(n3/2),不稳定的排序算法。
希尔排序的复杂度很难像插入排序那样用一个简单式子完全概括,因为它高度依赖 gap 序列。复习时可以先记住两个事实:最后一趟一定是普通插入排序;前面的较大 gap 会让序列提前接近有序,从而降低最后一趟的移动成本。
选择排序
首先,从待排序的 n 个元素中选出最小的元素,存放在序列的起始位置;
然后,再从剩余的 n-1 个元素中选出最小的元素,放在已排序序列的末尾;
最后,重复上述过程,将每次得到的元素排成一个序列直到所有元素均排序完成。
选择排序的循环不变量是:第 i 趟结束后,前 i 个位置已经放入全局最小的 i 个元素。它的比较次数几乎不受输入有序程度影响,因为每一趟都必须扫描完整的未排序区间来找到最小值。
// 一趟划分的实现 template <classKEY, classOTHER> intdivide(SET<KEY, OTHER> a[], int low, int high){ SET<KEY, OTHER> k = a[low]; // 选择基准元素 do { while (low < high && a[high].key >= k.key) --high; // 从右向左找第一个小于k的元素 if (low < high) a[low++] = a[high]; // 将找到的元素放到左边 while (low < high && a[low].key <= k.key) ++low; // 从左向右找第一个大于k的元素 if (low < high) a[high--] = a[low]; // 将找到的元素放到右边 } while (low != high); a[low] = k; // 将基准元素放到正确位置 return low; // 返回基准元素的位置 }
快速排序中最关键的是 divide:它不是完整排序,而是把基准元素 k 放到最终位置,并保证左侧元素不大于它、右侧元素不小于它。此后基准元素再也不需要移动,递归只处理左右两个子区间。第一次看这段代码时,可以把 low 和 high 想成两个从两端向中间靠拢的“空位搬运指针”:右边找到小元素填左空位,左边找到大元素填右空位,直到两个指针相遇。
性能分析
最坏情况:每次枢纽元素都为最大或最小,时间复杂度为 O(n2)。
T(N)=T(N−1)+cN=T(1)+c(2+3+⋯+N)=O(N2)
改进方法:随机选取界点,或者最左,最右,中间三个元素的中位数作为界点,通常可以避免最坏情况。
最好情况:每次枢纽元素都能将序列分成两半,时间复杂度为 O(nlogn)。
T(N)=2T(N/2)+cN=cNlog2N+N=O(NlogN)
平均情况:每一个元素都可以当作界点,共 n 种情况,平均时间复杂度为 O(nlogn),不稳定的算法。
// 合并两个有序表 template <classKEY, classOTHER> voidmerge(SET<KEY, OTHER> a[], int left, int mid, int right){ SET<KEY, OTHER> *temp = new SET<KEY, OTHER>[right - left + 1]; int i = left, j = mid, k = 0; // mid表示右半区的起始位置
while (i < mid && j <= right) { if (a[i].key <= a[j].key) { // 相等时先取左侧,保持稳定性 temp[k++] = a[i++]; } else { temp[k++] = a[j++]; } }
while (i < mid) { temp[k++] = a[i++]; } while (j <= right) { temp[k++] = a[j++]; }
for (i = 0, k = left; k <= right;) { a[k++] = temp[i++]; } delete[] temp; // 释放临时数组 }