希尔排序原理
希尔排序(Shell’s Sort),也称为“缩小增量排序”,是一种插入排序类的算法。最简单的插入排序,我在上一个专栏的一篇文章C++抽象编程——算法分析(8)——插入排序算法与分析有提到过,这里就不再赘述,这里就只介绍一些我以前没写过的算法。
希尔排序是一种改进的插入排序算法。其基本思想如下:将整个待排序列分割成若干个自序列,然后对每个子序列分别进行直接插入排序算法。待整个序列中的每条记录都呈现出有序状态的时候,再对整体进行排序。(是不是有点devid and conquer算法的味道?)。其实直接插入排序算法在元素较小的序列中效率还是很高的,所以希尔排序就是基于这个原理来进行分割重组的。
希尔排序过程分析
假设我们需要对下面的一组数据进行希尔排序:
49 38 65 97 76 13 27 48 55 04
- 根据给定的增量序列,对数据进行分组(即子序列),假设一开始增量 gap = 5(每隔5个单位取一个数).那么就分成下面的5组序列
(49, 13),(38, 27),(65, 48),(97,55),(76, 04)
在这一步尤其注意,这里说的是每隔5个单位取一个数,不是每5个连续单位取一个数!!我一开始理解的时候理解错就很棘手。比如有人会理解成第一个序列是(49 38 65 97 76),这是不对的。而且同样注意,增量是从第二个元素开始数起的。 - 然后对组的元素进行组内插入排序,也就是形成这样的一个情形:
(13, 49),(27, 38),(48, 65),(55, 97),(04, 76) - 将较小的数字往前移动,构成第一趟的排序(注意:我们刚刚的排序是在同一个子序列中进行的比较,所以这个时候组间的较小的元素不是一步步往前挪动,而是跳跃式的往前移动(即插入)),就像这样:
如果这种方式不好看,我们可以换种方式来看:
这样,第一趟排序的顺序就出来了。 - 在第一趟排序的基础上,我们取增量gaps = 3,同上操作可以得到第二趟的排序:
- 因此,这个时候我们取增量gaps = 1,也就是这个时候退化为简单的插入排序。这个时候序列基本有序,适合用简单的插入算法。所以,第三趟的排序就是:
**04 13 27 38 48 49 55 65 76 97 **
排序完成。
下面再给出一个实例:
希尔排序的不确定性
我们从上面的步骤可以看出,这个增量gap的值是我们事先给定的,不同的gap值会产生不同的排序序列,也就是说,排序的时间与gap的取值有关,当gap = 1的时候,退化为简单的排序算法。对于如何取这样的gap值,是个难解的问题。常见的gap取值可以参见:Shellsort
希尔排序的实现
先贴一段伪代码:
# Sort an array a[0...n-1].
gaps = [701, 301, 132, 57, 23, 10, 4, 1]
# Start with the largest gap and work down to a gap of 1
foreach (gap in gaps)
{
# Do a gapped insertion sort for this gap size.
# The first gap elements a[0..gap-1] are already in gapped order
# keep adding one more element until the entire array is gap sorted
for (i = gap; i < n; i += 1)
{
# add a[i] to the elements that have been gap sorted
# save a[i] in temp and make a hole at position i
temp = a[i]
# shift earlier gap-sorted elements up until the correct location for a[i] is found
for (j = i; j >= gap and a[j - gap] > temp; j -= gap)
{
a[j] = a[j - gap]
}
# put temp (the original a[i]) in its correct location
a[j] = temp
}
}
如果理解有困难,我大致讲一下,思路是这样的,先把给定的增量放在一个数组中存放,这个时候,第一层循环是遍历我们的增量,第二层循环是从我们增量的元素下标后面开始遍历后面的元素,最后一层是遍历我们的分组,并且根据情况判断是否进行交换。我在写的时候,特意写了一个C++代码,也能成功运行,代码贴上:
#include <iostream>
#include <vector>
using namespace std;
/*函数原型*/
void shellsort(vector<int> & vec,vector<int> gaps);
/*主函数*/
int main() {
vector<int> vec,gaps;
cout << "请输入待排序的序列" << endl;
for (int i = 0; i < 12; i++) {
int n;
cin >> n;
vec.push_back(n);
}
cout << "请输入增量:" << endl;
for (int j = 0; j < 3; j++)
{
int m;
cin >> m;
gaps.push_back(m);
}
shellsort(vec, gaps);//shellsort(vec);//使用希尔排序对vector进行排序
cout << "希尔排序后:" << endl;
for (int k = 0; k < vec.size(); k++) {
cout << vec[k] << " ";
}
return 0;
}
void shellsort(vector<int> &vec, vector<int> gaps) {
int i,j,k,temp;
for (i = 0; i < gaps.size();i++) {//遍历所给的增量gaps
//对于vec增量后的每一个元素,都执行下面的操作
for (k = gaps[i]; k < vec.size();k++){
temp = vec[k];//暂存第增量个元素
//cout << "这个时候的temp值为:" << temp;
//将此时增量后面的第k个元素作为分组的末尾元素,用插入算法比较
for (j = k; j >= gaps[i] && vec[j - gaps[i] ]> temp; j -= gaps[i]) {
vec[j] = vec[j - gaps[i]];//将vec下标较小值部分赋值给较大的那边
//cout << " 最里面的vec[j]的值" << vec[j];
}
vec[j] = temp;
//cout << "外部的vec[j]= " << vec[j] << endl;
}
}
}
测试数据跟结果如下:
写代码的过程
瞎扯一下,写这段代码着实不是很容易。在初期,我犯了一个错误,就是到后面把gaps[i]当k使用了,因为有一段语句是将gaps[i]的值赋值给k。而我忽略了k是有在递增变化的,而gaps[i]在跳出循环时始终是不变的。在寻找问题的过程中,我添加了代码(就是我注释掉的那三句),用来追踪循环中的值变化,从而找到相应的问题。这是一种常用的找循环错误的方法。有兴趣的可以试试将gaps[i]改为k,并取消注释。观察出现的问题。同样可以在里面加一句输出语句,把每一趟的情况输出出来。