希尔排序的理解
作为插入排序的升级版,希尔排序的优化体现在何处呢?具体需要如何实现呢?
一、写在前面
【Definition】An inversion in an array of numbers is any ordered pair ( i, j ) having the property that i < j but A[i] > A[j].
逆序:是指序列中大小关系不正确的数目。
举个栗子:
34, 8, 64, 51, 32, 21 这样一个数字序列具有9个逆序。
分别是(34, 8) (34, 32) (34, 21) (64, 51) (64, 32) (64, 21) (51, 32) (51, 21) (32, 21)。
从逆序角度理解插入排序
我们的插入排序在每一步时都要像打扑克一样,将未排好序的牌中取一张,插入到已排序的牌堆中,在插入的过程,我们可以理解成为逆序减少的过程,因为每次都是相邻元素的交换,很容易理解交换一次最多消除一个逆序。
而N个元素逆序的最大值是
N
(
N
−
1
)
2
\frac{N(N-1)}{2}
2N(N−1),不难理解插入排序的时间复杂度是
O
(
N
2
)
O(N^2)
O(N2)。
二、希尔排序的思想
自然语言(文字)描述
希尔排序为了使得交换减少,采取的策略是进行较远元素的比较,这样一次交换即可实现多个逆序的减少,如{5,4,2,6},如果采取插入排序,需要交换三次;而希尔排序如果进行5与2的交换,则一次可以减少三个逆序,成功使序列有序。
而这样的分组一般是有规律的,采取不同的分组策略会有不同的时间复杂度。
最常用到的递增序列:
h
t
=
⌊
N
/
2
⌋
,
h
k
+
1
=
⌊
h
k
/
2
⌋
h_t= \lfloor N/2 \rfloor,h_k+1 = \lfloor h_k/2\rfloor
ht=⌊N/2⌋,hk+1=⌊hk/2⌋
伪代码描述
void Shellsort( ElementType A[ ], int N )
{
int i, j, Increment;
ElementType Tmp;
for ( Increment = N / 2; Increment > 0; Increment /= 2 )
/*h sequence :希尔序*/
for ( i = Increment; i < N; i++ ) { /* insertion sort */
Tmp = A[ i ];
for ( j = i; j >= Increment; j - = Increment )
if( Tmp < A[ j - Increment ] )
A[ j ] = A[ j - Increment ];
else
break;
A[ j ] = Tmp;
} /* end for-I and for-Increment loops */
}
二话不说,先上栗子:
上图是以{5,3,1}作为希尔递增序列(increment sequence)完成的希尔排序:
- 首先是间隔为5进行排序,我们可以分成五组,在图中的第一行,用蓝、橙、粉红、绿和紫罗兰色进行了分组,组内分别进行插入排序,即81,35,41先进行排序,结果为35,41,81分别填入原来这三个数所在的三个空,其它四个组以此类推。
- 接着是间隔为3进行排序,我们可以分成三组,在图中的第二行,如{35,28,75,58,95}就是一组,排序结果应为{28,35,58,75,95},重新填入所在位置,其它两个组以此类推。
- 进行完这两步,我们发现序列基本有序,这个时候再进行插入排序就可以减少比较多的交换了。
三、希尔排序的C语言实现
#include <stdio.h>
#include <stdlib.h>
#define SIZE 20
void Shell_sort(int a[],int size);
void prt(int a[],int len){
for(int i=0;i<len;i++){
printf("%4d",a[i]);
}
printf("\n");
}
int cnt = 0;
int main(){
int a[SIZE];
srand(1);
for(int i=0;i<SIZE;i++){
a[i] = rand() % SIZE;
}
prt(a,SIZE);
printf("-------------------------------------------------\n");
Shell_sort(a,SIZE);
printf("The number of swap :%d\n",cnt);
int suc =1;
for(int i=0;i<SIZE-1;i++){
if(a[i] > a[i+1]){
suc =0;break;
}
}
if(suc ){
printf("ok\n");
}else{
printf("fail\n");
}
}
void Shell_sort(int a[],int n){
int Increment, i, j, temp;
//希尔序:这里采取的递增序列是 n/2,n/4,...,1
for (Increment = n/2; Increment >= 1; Increment = Increment /2) {
//插入第一个元素,直接跳过这一步,从插入第二个元素开始.因为这个小组的元素中隔了Increment 个,所以排的时候也要隔Increment 个
for (i = Increment ; i < n; i++) {
//一次进行各个小组的排序,并非是先进行一次排序(顺序是按照再数组中储存的位置)
if(a[i] < a[i-Increment])
{
temp = a[i];
for (j = i-Increment ; j >= 0 && temp < a[j];j -= Increment) {
a[j+Increment] = a[j];
cnt++;
}
a[j+Increment] = temp;
}
}
prt(a,n);
}
}
变更实验数据可以更改随机数种子,srand(num).
四、实验结果
希尔排序
插入排序
五、提高与改进
使用 Hibbard’s Increment Sequence:
h
k
=
2
k
+
1
h_k=2^k+1
hk=2k+1 ---- 连续增量没有公共因子(互素)。
时间复杂度可以减少为
O
(
N
5
/
4
)
O(N^{5/4})
O(N5/4).
还有更加复杂的序列,总之使用到了互素的性质进一步减少每次分组之间的重叠,增大分组的作用。
Sedgewick’s best sequence: 9 ∗ 4 i – 9 ∗ 2 i + 1 9*4^i – 9*2^i + 1 9∗4i–9∗2i+1 or 4 i – 3 ∗ 2 i + 1 4^i – 3*2^i + 1 4i–3∗2i+1.
程序实现:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define SIZE 500000
#define MAXN 40
void Shell_sort(int a[],int size);
//打印输出数组
void prt(int a[],int len){
for(int i=0;i<len;i++){
printf("%5d ",a[i]);
if(i%15 == 14){
printf("\n");
}
}
printf("\n");
}
//计数交换次数
int cnt = 0;
int shell_series[MAXN];
int index = 0;
int main(){
int a[SIZE];
//生成随机数组
srand(1);
for(int i=0;i<SIZE;i++){
a[i] = rand() % SIZE;
}
// prt(a,SIZE);
printf("-------------------------------------------------\n");
//希尔排序
//希尔序:Sedgewick's best sequence
int ix,iy;
for(index = 0,ix = 0,iy = 2;;ix++,iy++){
int num1 = 9*pow(4,ix)-9*pow(2,ix)+1;
int num2 = pow(4,iy)-3*pow(2,iy)+1;
if(num2 < SIZE && num1 < num2){ //当num1 < num2 < n时,依次读入num1以及num2
shell_series[index++] = num1;
shell_series[index++] = num2;
}
else if(num1 < SIZE && num1 > num2){ //当num2 < num1 < n时,依次读入num2以及num1
shell_series[index++] = num2;
shell_series[index++] = num1;
}
else if(num2 < SIZE){ //当num2 < n < num1时,读入num2
shell_series[index++] = num2;
}
else if(num1 < SIZE){ //当num1 < n < num2时,读入num1
shell_series[index++] = num1;
}
else if(num1 > SIZE && num2 > SIZE) //当n < num1 && n < num2,结束寻找
break;
}
Shell_sort(a,SIZE);
printf("The number of swap :%d\n",cnt);
//判断结果正确性
int suc =1;
for(int i=0;i<SIZE-1;i++){
if(a[i] > a[i+1]){
suc =0;break;
}
}
if(suc ){
printf("ok\n");
}else{
printf("fail\n");
}
}
void Shell_sort(int a[],int n){
prt(shell_series,index);
int Increment, i, j, temp;
index --;
for (Increment = shell_series[index]; index >= 0; Increment = shell_series[--index]) {
//插入第一个元素,直接跳过这一步,从插入第二个元素开始.因为这个小组的元素中隔了Increment 个,所以排的时候也要隔Increment 个
for (i = Increment ; i < n; i++) {
//一次进行各个小组的排序,并非是先进行一次排序(顺序是按照在数组中储存的位置)
if(a[i] < a[i-Increment]) //当前小组进行插入排序
{
temp = a[i]; //找到正确的位置
for (j = i-Increment ; j >= 0 && temp < a[j]; j -= Increment) {
a[j+Increment] = a[j];
cnt++;
}
//插入当前元素
a[j+Increment] = temp;
}
}
// prt(a,n);
}
}
以500000为数据量,进行测试,上面为采用Sedgewick’s best sequence的结果,优于下面的一般希尔排序(简单二分)。