《数据结构--排序》之插入排序

1.序

按照严吴版的《数据结构》里面对插入排序的介绍,实现其算法:

插入排序即通过插入方式完成排序,其中包括直接插入排序,二分插入排序,二路插入排序,表插入排序,希尔插入排序,分别给予实现


2.直接插入排序

《算法导论》:直接插入排序类似于打牌时,最先手中一张牌也没有,然后一张一张摸起来,分别插入到指定的位置,然后牌拉完了,手里的牌也是有序的了

代码如下:

/*
 * 对sq+1,sq+2,...,sq+length进行直接插入排序
 */
void InsertSort(int *sq,int length){
	for(int i=2;i<=length;i++){
		sq[0]=sq[i];
		int j;
		for(j=i-1;sq[0]<sq[j];j--){
			sq[j+1]=sq[j];
		}
		sq[j+1]=sq[0];
	}
}
可见,直接插入排序时间复杂度为  n^2  ,当序列基本有序时,需要往前插入的次数会减少很多,效率还是较高的


3.二分插入排序

直接插入排序中,前面的序列已经是有序的,需要找到当前需要插入的数在前面的数的位置,然后进行插入,直接插入排序采用从后往前依次找插入位置,如果前面的序列是有序表的话,可以用二分查找方法来找到要插入的位置。二分查找,设置low,high,当没有找到待插入元素时,>=high的元素都是大于当前待插入元素的,所以找到了插入位置,即high之前。

代码如下:


/*
 * 对sq+1,sq+2,...,sq+length进行二分直接插入排序
 */
void BinaryInsertSort(int *sq,int length){
	int low,mid,high;

	for(int i=2;i<=length;i++){
		if(sq[i]<sq[i-1]){
			sq[0]=sq[i];

			low=1;
			high=i-1;

			while(low<=high){

				mid=(low+high)/2;

				if(sq[0]<sq[mid]){
					high=mid-1;
				}
				else if(sq[0]>sq[mid]){
					low=mid+1;
				}
				else{
					//有相同关键字
					low=high=mid;
					break;
				}
			}
			printf("low=%d,high=%d\n",low,high);

			int j;
			for(j=i-1;j>=high+1;j--){
				sq[j+1]=sq[j];
			}
			sq[high+1]=sq[0];
		}
	}	
}
可见二分插入排序,只是减少了关键字之间的比较次数,并不能减少交换次数,只是在查找插入位置时提高了速度,而且必须是有序表的限制


4.二路插入排序

二路插入排序是在二分插入排序的基础上改进的方法,目的是为了减少排序过程中交换记录的次数,但为此需要额外的n个辅助记录空间。用一个记录(目前取第一个记录)作为枢纽点,然后把比该记录关键字大大记录插入当前空间,比该记录大的插入到辅助空间,最后合并两个数组,完成排序。

代码如下:

/*
 * 对sq+1,sq+2,...,sq+length进行二路插入排序
 */
void DoubleRouterInsertSort(int *sq,int length){
	int *sqadd=(int *)malloc((length+1)*sizeof(int));
	*(sq+0)=*(sqadd+length)=*(sq+1);

	int insertpos;
	int pivotA=0;
	int pivotB=length;

	for(int i=2;i<=length;i++){
		if( (*(sq+i)) > *((sq+0)) ){
			//InsertB,插入到辅助空间
			insertpos=pivotB;
			while( (insertpos<=length) && ((*(sq+i))>(*(sqadd+insertpos))) ){
				*(sqadd+insertpos-1)=*(sqadd+insertpos);
				insertpos++;
			}
			*(sqadd+insertpos-1)=*(sq+i);
			pivotB--;
		}
		else{
			//InsertA,插入到本身空间
			if(pivotA==0){
				*(sq+1)=*(sq+i);
				pivotA=1;
			}
			else{
				insertpos=pivotA;
				while( (insertpos>=1) && ((*(sq+i))<(*(sq+insertpos))) ){
					*(sq+insertpos+1)=*(sq+insertpos);
					insertpos--;
				}
				*(sq+insertpos+1)=*(sq+i);
				pivotA++;
			}
		}
	}
	//合并 AB
	for(;pivotA<length;pivotA++){
		*(sq+pivotA+1)=*(sqadd+pivotB);
		pivotB++;
	}
	free(sqadd);
}
二路插入排序中,移动记录的次数约为  (n^2)/8  。因此,二路插入排序只能减少移动记录的次数,而不能绝对避免移动记录。并且当,*(sq+1)  为关键字最小/大的记录时,二路插入排序就完全失去其优越性。

5.表插入排序

前面3中插入排序都无法完全避免记录之间的交换,只有改变数据结构才能解决这个问题,表插入排序通过修改next域来避免直接的记录交换。

代码如下:

/*
 * 表插入排序
 */
typedef struct Node{
	int key;
	int next;
}Node;
/*
 * MAX_LEN 是最大的表长,可以不被全部填充
 * length 是表的实际长度
 * sq[0].key=MAX_INT 为表头节点
 */
typedef struct SList{
	Node sq[MAX_LEN+1];
	int length;
}SList;
void printSL(SList test){
	int pos=test.sq[0].next;
	while(pos!=0){
		printf("%d ",test.sq[pos].key);
		pos=test.sq[pos].next;
	}
	putchar('\n');
}
void printSN(SList test){
	for(int i=1;i<=test.length;i++){
		printf("%d ",test.sq[i].key);
	}
	putchar('\n');
}
/*
 * 用表插入法 对sl对应的表进行插入排序
 * 生成一个静态有序循环链表
 */
void TableInsertSort(SList &sl){
	/*
	 * 先让 sq[0],sq[1]构成循环链表
	 * 然后将sq[2],sq[3],...,sq[length]依次插入到循环链表
	 */
	sl.sq[0].next=1;
	sl.sq[1].next=0;

	int insertpos;
	int preinsertpos;	

	for(int i=2;i<=sl.length;i++){
		insertpos=sl.sq[0].next;
		preinsertpos=0;

		while(sl.sq[i].key>sl.sq[insertpos].key){
			preinsertpos=insertpos;
			insertpos=sl.sq[insertpos].next;
		}

		sl.sq[preinsertpos].next=i;
		sl.sq[i].next=insertpos;
	}
}

/*
 * 将静态循环链表整理成数组
 * 即将链表中第i个节点移动到sq[i]处
 */
void Arrange(SList &test){
	int q,p=test.sq[0].next;
	Node tmp;
	for(int i=1;i<test.length;i++){
		/*
		 * 因为 对于数组sq后面的分量的next可能指向前面的分量,而此时前面的分量可能已经移动到后面
		 * 而要通过这个前面的找到的分量来找到这个已经移动到后面的分量
		 * 可更改前面分量的next域,使其指向和他对换分量的位置
		 */
		while(p<i){
			p=test.sq[p].next;
		}
		q=test.sq[p].next;
		if(p!=i){
			//sq[p]<--->sq[i]
			tmp=test.sq[p];
			test.sq[p]=test.sq[i];
			test.sq[i]=tmp;

			test.sq[i].next=p;
		}
		p=q;
	}
}
可见,表插入排序基本操作依然是将一个记录插入到已排序的有序表中,和直接插入排序相比,不同之处是以修改  2n  次“指针”域代替了移动记录,排序过程中关键字之间的比较次数相同,因此表插入排序的时间复杂度依然是  n^2  。

6.希尔排序

希尔排序又称  “缩小增量排序”(diminishing increment sort),它也是一种插入排序类的方法,但在效率上有较大提高。

对直接插入排序的分析可知,其算法时间复杂度为  O(n^2)  ,但是,若待排序记录序列为 “正序” 时,时间复杂度可提高到 O(n)  。由此可设想,若待排序序列按关键字基本有序,n也很小时,直接插入排序的效率还是很高的,希尔排序正式从这两点出发,对直接插入排序的一种改进排序算法。

代码如下:

/*
 * 按跳跃距离dk值,对序列sq进行一趟希尔插入排序
 */
static void ShellInsert(int *sq,int length,int dk){
	int tmp;
	for(int i=dk+1;i<=length;i++){
		if( (*(sq+i))<(*(sq+i-dk)) ){
			tmp=*(sq+i);

			/*找到 *(sq+i) 的位置并插入*/
			int j;
			for(j=i-dk;(j>0)&&((*(sq+j))>tmp);j-=dk){
				*(sq+j+dk)=*(sq+j);
			}
			*(sq+j+dk)=tmp;
		}
	}
}
/*
 * 用 increment[i]=2^(times-i)+1 创建增量序列,times指定增量序列的大小
 */
static int* CreateIncrement(int times){
	int *increment=(int *)malloc(times*sizeof(int));
	if( NULL!=increment ){
		for(int i=0;i<times-1;i++){
			*(increment+i)=(int)(pow((double)(2),(double)(times-i-1))+1);
		}
		/*
		 * 保证最后一次增量为1,即直接插入排序
		 */
		*(increment+times-1)=1;
	}
	return increment;
}
/*
 * 对sq+1,sq+2,...,sq+length进行希尔排序
 */
const int SHELL_TIMES=5;
void ShellSort(int *sq,int length){
	/*
	 * 按增量序列 increment[0,1,2,...,length-1] 对顺序表sq作希尔排序
	 */
	int *dk=CreateIncrement(SHELL_TIMES);
	if(NULL!=dk){
		for(int i=0;i<SHELL_TIMES;i++){
			ShellInsert(sq,length,*(dk+i));
		}
		free(dk);
		dk=NULL;
	}
}
可见,希尔排序的特点是:子序列的构成不是简单地“逐段分割”,而是将相隔某个“增量”的记录组成一个子序列。因此,希尔排序中关键字小的记录不是一步一步向前挪动,而是跳跃式地向前移,从而使得最后一趟增量为1的插入排序时,序列已基本有序,这就是希尔排序效率提高的关键。


常见的两种增量序列:

....9,5,3,2,1                         dlta[k]=2^(t-k)+1

....125,40,13,4,1                dlta[k]=(3^(t-k-1))/2


7.总结

老是嘴上说什么什么排序,只有自己亲手去实现了才懂其中的奥妙。

你可能感兴趣的:(《数据结构--排序》之插入排序)