排序算法三:Shell插入排序
声明:引用请注明出处http://blog.csdn.net/lg1259156776/
引言
在我的博文《“主宰世界”的10种算法短评》中给出的首个算法就是高效的排序算法。本文将对排序算法做一个全面的梳理,从最简单的“冒泡”到高效的堆排序等。
上一篇博文《排序算法二:二分(折半)插入排序》讲述了直接插入排序,本文讲述第三种插入排序算法:Shell插入排序。实际上它是改进自插入排序和冒泡排序。
排序相关的的基本概念
-
排序:将一组杂乱无章的数据按一定的规律顺次排列起来。
- 数据表( data list): 它是待排序数据对象的有限集合。
- 排序码(key):通常数据对象有多个属性域,即多个数据成员组成,其中有一个属性域可用来区分对象,作为排序依据。该域即为排序码。每个数据表用哪个属性域作为排序码,要视具体的应用需要而定。
-
分类
- 内排序:指在排序期间数据对象全部存放在内存的排序;
- 外排序:指在排序期间全部对象个数太多,不能同时存放在内存,必须根据排序过程的要求,不断在内、外存之间移动的排序。
排序算法的分析
排序算法的稳定性
如果在对象序列中有两个对象r[i]和r[j] ,它们的排序码k[i]==k[j] 。如果排序前后,对象r[i]和r[j] 的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。
排序算法的评价
时间开销
- 排序的时间开销可用算法执行中的数据比较次数与数据移动次数来衡量。
- 算法运行时间代价的大略估算一般都按平均情况进行估算。对于那些受对象排序码序列初始排列及对象个数影响较大的,需要按最好情况和最坏情况进行估算
空间开销
算法执行时所需的附加存储。
Shell插入排序
基本思想
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt−l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。该方法实质上是一种分组插入方法。实际上当dt=1时,完全就是直接插入排序。但是经过多个分组的直接插入排序,最后一次完整的插入排序前数据表几乎已经是排好序了,因此shell sort充分利用了直接插入排序在数据表几乎已经有序的条件下工作高效的特点。
算法的C plus plus实现
根据算法的基本思想,shell插入排序的实现还是比较容易的,其C++实现如下:
#include <iostream>
#include <iomanip>
using namespace std;
void print(int ar[], int sz, int step)
{
for(int i = 0; i < sz; ++i) {
if(((i + 1) % step) != 0)
cout << setw(3) << ar[i];
else
cout << setw(3) << ar[i] << endl;
}
cout << endl;
}
void ShellSort(int a[], int sz)
{
int i, j;
int step, temp;
step = sz / 2 ;
while(step) {
print(a, sz, step);
cout << "==>" << endl;
for (i = step; i < sz; i++) {
temp = a[i];
j = i;
while (j >= step && a[j - step] > temp) {
a[j] = a[j - step];
j = j - step;
}
a[j] = temp;
}
print(a, sz, step);
cout << "current array" << endl;
print(a, sz, sz);
cout << "----------------" << endl;
step = step / 2.2;
}
}
int main(void)
{
int a[] = {13, 14, 94, 33, 82, 25, 59, 94, 65, 23, 45, 27, 73, 25, 39, 10};
const size_t sz = sizeof(a)/sizeof(a[0]);
cout << "Initial array" << endl;
print(a,sz,sz);
cout << "-------------" << endl;
ShellSort(a,sz);
cout << "Sorted array" << endl;
print(a,sz,sz);
return 0;
}
核心部分ShellSort中内层的while循环实现的是查找相应组内的插入位置,并提前进行位置的交换。而for循环控制的是从每个组内的第2个数开始重复在该组前面有序的数据表中实现直接插入排序。与直接插入排序的区别是shell插入排序一次for将所有组内的第i个数插入到各自前i-1个有序表中。
输出为:
Initial array
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10
-------------
13 14 94 33 82 25 59 94
65 23 45 27 73 25 39 10
==>
13 14 45 27 73 25 39 10
65 23 94 33 82 25 59 94
current array
13 14 45 27 73 25 39 10 65 23 94 33 82 25 59 94
----------------
13 14 45
27 73 25
39 10 65
23 94 33
82 25 59
94
==>
13 10 25
23 14 33
27 25 45
39 73 59
82 94 65
94
current array
13 10 25 23 14 33 27 25 45 39 73 59 82 94 65 94
----------------
13
10
25
23
14
33
27
25
45
39
73
59
82
94
65
94
==>
10
13
14
23
25
25
27
33
39
45
59
65
73
82
94
94
current array
10 13 14 23 25 25 27 33 39 45 59 65 73 82 94 94
----------------
Sorted array
10 13 14 23 25 25 27 33 39 45 59 65 73 82 94 94
算法分析
- 增量序列的选择
Shell排序的执行时间依赖于增量序列。
好的增量序列的共同特征:- 最后一个增量必须为1;
- 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了目前较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6nl.25之间。
- Shell排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:- 当数据表初态基本有序时直接插入排序所需的比较和移动次数均较少。
- 当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
- 在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di−1作为距离排过序,使数据表较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插人排序有较大的改进。
- 稳定性
希尔排序是不稳定的。两个相同关键字在排序前后的相对次序是可能发生变化的。
这个方法对于大的数据集效率其实并不高,但是它是一个对小数量数据表(小于1000)进行排序的最快算法之一。另外,相对使用的内存较少。
2015-9-24 艺少