改进后的超自然合并排序算法

  1. 作者我的初衷是想编写一个自底向上的合并算法,但每次都要给排序函数子序列段的首尾坐标或者长度,还要分子序列段是奇数还是偶数,太麻烦了。于是我就想能不能事先存储要合并的各个要合并的子序列段的首尾坐标,然后两两配对之后删除在合并中第二个子序列段的相关信息,并给该次合并的第一个调子序列段调整尾坐标,使其表示合并后的新的子序列段。我是想用链表来存储,但一想到要分出两个值域去存储首尾坐标的话,加入数组很大,那么该链表至少也要跟数组这么大。典型的是利用空间换时间,而且转换效率还低。如图:在这里插入图片描述
  2. 于是我想转换另一种思路,链表的每个单元我只设置一个值域,用来记录一个子序列段的首坐标,然后根据子序列段是相邻的,可以用第二个子序列段的首坐标减去第一个的子序列段的首坐标,就可以得到第一个子序列段的长度,这样可以确定可一个子序列段。以此类推可以确定每个子序列段。如图:在这里插入图片描述
  3. 但我发现这样比较容易出错,很容易把人给搞混乱,标明黄色的地方要特殊处理。
    你能想出第二次合并后链表应存储的数字吗?
    这样也要占用额外的空间,虽然可以自动得出下一次需要合并的子序列的首尾元素位置,但有点复杂,容易出错。
    能不能再优化一下呢,不占用额外的空间也可也自动得出每次需要合并的子序列的首尾元素位置呢?于是我想 找一下其中的规律。如图:在这里插入图片描述
  4. 标红的这些个子序列段有一个特点(只标出了部分),那就是都参与合并排序的最后以此排序,切且它的元素数量一定为2n个。为什么一定参与 最后一次排序呢?因为元素量为2n个,所以这些个元素一定可以合并为一个子序列段(在合并排序中),并且他的元素量一定大于等于数组元素的一半,这也就意味着该序列会参与合并排序的最后一次合并。因为元素量是2n次方,所以在对这些个元素进行初始合并的时候,找到前后的两个子序列的首尾坐标变得很简单,且每次被合并的子序列的长度都是可以预支的。该条件可以这样来描述,用K来表示2n,N表示在标红区段中的子序列每次合并的长度的话,R表示每次循环中需要合并的子序列的个数的话。满足 K = N * R。但因为比较是两两进行的,不可能一个元素自己合并,所以实际应用时要改成: R = (K / R) / 2。也就是R不可能一开始分8个段。然后子序列的的坐标的确定的话,第一个子序列首从零开始,第二个为N,完成一次合并后,两个都加N,就可以表示下一次的两个需要合并的子序列了。
    具体代码如下:
#include<stdio.h> 
#include<stdlib.h>
#define less(A,B) A<B
#define coswap if(less(A,B)){Item t = A;A = B;B = t}
void twosort(int a[],int b [] ,int N,int r)//这里的N为待每个数组内需要排序的元素个数,r为数组数,然后这两个数相乘等于总的数组长度 
{
	//因为是自底向上,所以每次的比较元素个数起始就算2,但需要比较的部分是4次。即满足该条件:比较的元素个数乘以比较的部分是等于数组的总元素量。 
	int one = 0; //表示第一个子序列的首元素坐标 
	int two = N;//表示第二个数组的首元素坐标 
	int thr = 0;//给数组b做下标 
	int cno ;//第一个子序列的计数器 
	int cnt ;	
	for(int i=0;i<r;i++){

		cnt = 0;
		cno = 0;
		printf("第%d次比较的坐标:%d %d \n\n",i+1,one,two);
		while(cnt<N && cno<N){
		if(less(a[one],a[two])) {
			b[thr++] = a[one++]; 
			cno++;
		}
		else{
			b[thr++] = a[two++];
			cnt++;
		}
		}
		if(cnt>=N)while(cno<N){
			b[thr++] = a[one++];
			cno++;
		}else{
			while(cnt<N){
				b[thr++] = a[two++];
				cnt++; 
			}
		}
		one = one+N;//需要注意的是每完成一次遍历,第一部分的数组的下标位移对应的比较元素的个数即是N;第二部分的数组元素也是如此 
		two = one + N;//无需像之前那样繁琐的传每个子序列的上下标。 
		  for(int v=0;v<16;v++)printf(" %d ",b[v]);//这里是方便检验用的,可以删除。 
		  printf("\n");
		   
	}		  	 
} 

void twofen(int a[],int b[],int K) 
{
	int r;
    for(int N = 1;N<K;N*= 2){
    	r = (K/N)/2;
    	twosort(a,b,N,r);
    	for(int i=0;i<16;i++)a[i] = b[i];//在最后一遍排序之前的b是有序的 
	}
	
}
int main()
{
	int a[16] = {1,5,3,7,2,6,4,9,11,10,22,15,16,13,25,19};
	int b[17] = {0};
	twofen(a,b,16);
	
}
  1. 这样我们就轻松完成了对前2n个元素的排序。后面的元素思来想去,就觉得自然排序比较合适了,于是我们对后面的元素采取自然排序。这样的话,在最后我们会得到两个子序列段,然后再调用一次自然排序里面的排序函数就可以了,只不过要注意少了一次复制,要将a,和b的位置换一下。只要对自然排序函数稍微改变一下就行了。就后将两个排序函数的功能部分重新粘合一下,就得到了改进的自然排序算法。
    代码如下
#include<stdio.h>
#include<stdlib.h>
#define key(A)  A
#define less(A,B) A<B
#define Number 29
int sort(int a[],int b[],int First,int Fnum,int Last,int Lnum)
{
	int i = First;
	while(Fnum>0 && Lnum > 0){//一个序列中的元素取完之后就添加另一个序列元素了。
	
		if(less(a[First],a[Last])){
			b[i++] = a[First++];
			Fnum--;
		}else{
			b[i++] = a[Last++];
			Lnum--;
		}
	}	
	if(Fnum>0){
     	while(Fnum>0){
     		
       	b[i++] = a[First++];	
      	Fnum--;
	    }			
	}
	else{
		while(Lnum>0) {
			b[i++] = a[Last++];
			Lnum--;
		}
	}

}

void copy(int a[],int b[],int position)
{
	int i = position;
	int j = i;
	while(b[j]){
		a[i++] = b[j++];
	}
}

int huafen(int a[],int b[],int N,int position)//N为数组中最大元素值的下标,position为开始扫的位置。 
{
    int First;//前一段数组的首坐标 
    int Last;//后一段数组的首坐标
	int Fnum; //容纳第一段数组的元素量 
	int Lnum;//容纳后一段数组的元素量 
    int cnt = 1;//计数器
	int flag = 0;//判断First是否赋值 
	int Llag = 0;//判断First是否赋值 
	int segments = 1;

    for(int i=position;i<N;i++){	
    	if(less(a[i],a[i+1]) && i != N-1){
    		cnt++; 
		}
		else{	
			if(flag == 0 && i != N-1){
				First = i-cnt+1;//这里加1是因为cnt是从1开始的,而First和Last是从0开始的			
				Fnum = cnt;			
				printf("\nFirst == %d  Fnum == %d\n",First,Fnum);	
				flag = 1;//表示已赋值 
				cnt = 1;//重新开始计数 
				segments++;
			}else{				
				Last = i-cnt + 1;
				Lnum = cnt;				
				printf("\nLast == %d  Lnum == %d\n",Last,Lnum);	
				cnt = 1;//重新计数 
				Llag = 1;
				segments++;
			} 
		if(Llag == 1 && flag == 1){
		sort(a,b,First,Fnum,Last,Lnum);
		flag = 0;//排完序之后标志值设为零 
		Llag = 0;
		copy(a,b,First);			
		}
				
		}
			
	}
    return segments;
}

void AddNature(int a[],int b[],int N,int position)  
{
	int k = huafen(a,b,N,position); //对数组进行第一遍合并的时候同时获取子序列段数,根据子序列段数可以知道需要循环多少遍数组
	int bour;
	int i=2;//用于输出时做数组下标 
	int Num = 0;  //记录需要多少次扫描 
	float segments = k;
	while(segments>1){
		segments = segments/2;
		bour = segments;
		if(segments > bour){//应对子序列段是奇数的情况 ,也容易出现特殊情况,就是前面的排好序列之后刚好比后一个子序列的元素小,无形之中就减少
		//了一次排列。 
			bour++;
			segments = bour;
		}
		Num++;
	} 
		
	for(int i=0;i<Num-1;i++){
	   huafen(a,b,N,position);
	}

}
//以上为改进过后的自然排序数组
//以下为适用于2^N个元素排序的数组
void twosort(int a[],int b[],int N,int r)//这里的N为待每个数组内需要排序的元素个数,r为数组数,然后这两个数相乘等于总的数组长度 
{
	//因为是自底向上,所以每次的比较元素个数起始就算2,但需要比较的部分是4次。即满足该条件:比较的元素个数乘以比较的部分是等于数组的总元素量。 
	int one = 0; 
	int two = N;
	int thr = 0;
	int cno ;
	int cnt ;	
	for(int i=0;i<r;i++){

		cnt = 0;
		cno = 0;
		printf("第%d次比较的坐标:%d %d \n\n",i+1,one,two);
		while(cnt<N && cno<N){
		if(less(a[one],a[two])) {
			b[thr++] = a[one++]; 
			cno++;
		}
		else{
			b[thr++] = a[two++];
			cnt++;
		}
		}
		if(cnt>=N)while(cno<N){
			b[thr++] = a[one++];
			cno++;
		}else{
			while(cnt<N){
				b[thr++] = a[two++];
				cnt++; 
			}
		}
		one = one+N;//需要注意的是每完成一次遍历,第一部分的数组的下标位移对应的比较元素的个数即是N;第二部分的数组元素也是如此 
		two = one + N;//无需像之前那样繁琐的传每个子序列的上下标。 
		  for(int v=0;v<16;v++)printf(" %d ",b[v]);
		  printf("\n");
		   
	}		  	 
} 

void twofen(int a[],int b[],int K) 
{
	int r;
    for(int N = 1;N<K;N*= 2){
    	r = (K/N)/2;
    	twosort(a,b,N,r);
    	for(int i=0;i<16;i++)a[i] = b[i];//在最后一遍排序之前的b是有序的 
	}
	
} 
int main()
{
	int a[Number] = {20,1,10,3,6,9,5,36,78,96,23,19,64,28,99,45,12,5,24,44,56,23,32,54,76,54,49,39,67};
	int b[Number] = {0};
	int two = 1;
	for(;two<Number;two = two*2){		
	}
	two = two /2;
	twofen(a,b,two);
	AddNature(a,b,Number,two);
	int i=0;	
	int k=0;
	while(b[i])printf(" %d ",b[i++]);
	//因为a少了一次复制,所以这里我们把a,和b的位置对调一下 
	sort(b,a,0,two,two,Number - two);
	printf("\n");
	while(a[k])printf(" %d ",a[k++]);


}

  1. 总结:合并排序的本质是两两排序,理解对前2n个元素的重要性,因为它总是参与最后一次合并。另外,要敢于将想法付诸于行动。在此之前要分析下想法的是否合理,以免浪费时间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值