在经典排序算法的插入排序里,主要是有以下三种:直接插入排序、希尔排序、折半插入排序。本文主要介绍的是希尔排序是设计思想和编程实现。
算法思想
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。希尔排序是把记录按下标的一定增量(gap)分组,对每个分组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
改进思想
我们复习一下直接插入排序的时间复杂度:
最差的情况:序列的顺序和目标顺序是逆序时间复杂度是此时O(n^2)。
最理想的情况:序列的顺序和目标顺序一致,时间复杂度为O(n)。
通俗来说就是:使用直接插入排序时,待排序序列顺序越靠近目标顺序时效率最高对吧?那么希尔排序里引入的增量的目的就是:每次进行一次预排序,让整个序列在下一轮排序时相比之前都更为有序,以此类推越往后该序列越来越接近理想情况,时间复杂度也就越来越好了。
增量gap的选取
gap越小,就越接近直接插入排序,如果gap=1,那就是直接插入排序了。那么gap每一轮
就都比上一轮小一点。一般的操作是,初始gap为元素个数。每用gap进行一次分组前,gap缩小为原来的三分之一或者二分之一,直到最后gap的值为1,也就是对这个基本有序的序列进行最后的直接插入排序。
分组举例
初始状态:
Array:1 2 5 8 0 9 0 4 5
Index:0 1 2 3 4 5 6 7 8
本次分组增量gap:4,当前序列为:
Array:1 2 5 8 0 9 0 4 5
Index:0 1 2 3 4 5 6 7 8
gap为4,那么就分成以下子序列进行排序:[1,0,5]、[2,9]、[5,0]、[8,4]
转成下标说明就是:下标为0、4、8的3个元素进行直接插入排序,下标为1、5这两个元素进行直接插入排序,下标为2、6这两个元素进行直接插入排序,下标为3、7这两个元素进行直接插入排序。
本次分组增量gap:2,当前序列为:
Array:0 2 5 8 1 9 0 4 5
Index:0 1 2 3 4 5 6 7 8
gap为4,那么就分成以下子序列进行排序:[0,5,1,0,5]、[2,8,9,4]
转成下标说明就是:下标为0、2、4、6、8的5个元素进行直接插入排序,下标为1、3、5、7这4个元素进行直接插入排序。
本次分组增量gap:1,当前序列为:
Array:0 2 0 4 1 8 5 9 5
Index:0 1 2 3 4 5 6 7 8
gap=1时,就是对整个序列进行直接插入排序了。这次排序完成后,整个序列就完成排序工作了。
Array:0 0 1 2 4 5 5 8 9
Index:0 1 2 3 4 5 6 7 8
关于直接插入排序的过程这里就不做过多叙述,想了解直接插入排序细节可以移步至我之前的帖子数据结构——直接插入排序查看。
核心算法
void Sort(int a[],int gap){
int t,ti;//直接插入排序时被选中的元素及其下标
for(int i=0;i<maxnum/gap;i++){
//根据gap划分每个子序列的起点
for(int j = i;j<maxnum;j+=gap){
//每个子序列进行直接插入排序
t = a[j]; ti = j;
while(ti>j-gap&&j-gap>-1){
//
if(t<a[ti-gap]){
//平移元素
a[ti] = a[ti-gap];
ti -= gap;//本次循环t应该插入的地方
}else{
break;
}
}
a[ti] = t;//最终确定t插入的地方
}//2for
}//1for
}
void ShellSort(int a[]){
int gap = maxnum;
while(gap>0){
gap = (gap)/3 + 1;//根据gap 最开始对于元素个数 n,之后每预排一组数据,gap 就减小2倍或者3倍的思想变化
Sort(a,gap);
if(gap==1)
break;
}
}
代码说明
在核心函数里,有两个for循环和一个while循环。
第一个for循环:i表示每一个子序列的起点。你可能会好奇:某个子序列的起点不会撞上其他子序列的某一段嘛?
那就举个例子:假如maxnum=10,此时的gap=10/2 + 1=6。每个子序列分别为[0 6]、[1 7]、[2 8]、[3 9]、[4]、[5];gap=3时,每个序列分别是,[0 3 6 9]、[1 4 7]、[2 5 8]。是不是发现,不会有重复的下标啦?
第二个for循环就是生成分组了:以上述例子gap=3时举例:第一个分组对应的下标为:[0,3,6,9],第二个分组对应的下标为[1,4,7],第三个分组对应的下标为[2,5,8]。
第三个循环是while循环,这个就是进行直接插入排序的过程了。因为我们每个分组的元素之间位置差不是1而是一个gap了,所以j的每次增减量为1个gap。
示例1
示例2
源代码
/*
广西师范大学 计算机科学与工程学院
GuangXi Normal University
College of Computer Science and Engineering
Student STZ
*/
#include<iostream>
#include<stdio.h>
#include <stdlib.h>
#include <time.h>
using namespace std;
#define maxnum 10
void initial(int a[]){
int max=10;
srand((unsigned)time(NULL));//时间种子
for(int i=0;i<maxnum;i++){
a[i] = rand()%max;//随机生成10个数
}
}
void showArray(int a[]){
cout<<"Array:";
for(int i=0;i<maxnum;i++){
cout<<a[i]<<" ";
}
cout<<endl;
cout<<"Index:";
for(int i=0;i<maxnum;i++){
cout<<i<<" ";
}
cout<<endl<<endl;
}
void Sort(int a[],int gap){
int t,ti;//直接插入排序时被选中的元素及其下标
for(int i=0;i<maxnum/gap;i++){
//根据gap划分每个子序列的起点
for(int j = i;j<maxnum;j+=gap){
//每个子序列进行直接插入排序
t = a[j]; ti = j;
while(ti>j-gap&&j-gap>-1){
//
if(t<a[ti-gap]){
//平移元素
a[ti] = a[ti-gap];
ti -= gap;//本次循环t应该插入的地方
}else{
break;
}
}
// cout<<"抽出的元素:"<<t<<" 空位下标:"<<ti<<endl;
a[ti] = t;//最终确定t插入的地方
// showArray(a);
}//2for
}//1for
}
void ShellSort(int a[]){
int gap = maxnum;
while(gap>0){
gap = (gap)/3 + 1;//根据gap 最开始对于元素个数 n,之后每预排一组数据,gap 就减小2倍或者3倍的思想变化
cout<<"本次分组增量gap:"<<gap<<",本轮排序后:"<<endl;
Sort(a,gap);
showArray(a);
if(gap==1)
break;
}
showArray(a);
}
int main(){
// int a[]={1,2,5,8,0,9,0,4,5};
int a[10];
initial(a);
cout<<"初始状态:\n";
showArray(a);
ShellSort(a);
return 0;
}
敬请批评指正!