数据结构——希尔排序

在经典排序算法的插入排序里,主要是有以下三种:直接插入排序、希尔排序、折半插入排序。本文主要介绍的是希尔排序是设计思想和编程实现。

算法思想

希尔排序(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;	
}

敬请批评指正!

猜你喜欢

转载自blog.csdn.net/qq_51231048/article/details/127153744