排序3:插入排序(2路插入排序)

         折半插入排序的优点在于利用折半查找的思想大大减少排序过程中产生的元素比较次数。然而,确定了元素插入位置后,移动元素次数却丝毫没有比直接插入排序的有所减少。有没有一种办法,不但能减少元素的比较次数,还能减少元素的移动次数呢?答案是肯定的,接下来让我们看看2路插入排序。
         仍以49、38、65、97、76、13、27、
49 为例,并且假设该序列存放到list数组中。 我们需要 1 个结果数组 result (与 list 长度相等)来保存排序结果以辅助处理过程。一开始, result 拥有 1 个元素,此为 list 的头个 元素 49,其位于result的头 。增加两个指针 first final ,分别指向当前 result 的头和尾元素,开始时均指向 49 。开始处理后,用两个新的指针 low high 分别指向当前 result first final ,再弄个 middle=0 ,即指向 result的头 49。 显然,最开始时,first、final、 low high middle 均为 0 。现在可以正式进行处理了,选取 list 里头的下一个元素 38 38 middle所指 49 比较,38<49 ,则 high=middle 1= 1 ,从而 low>high ,停止比较。由于 high 此时变为负数,要重新调整 high ,使其指向 result 末尾, high=7 。此 high 指向了 38 要插入的位置,而且不需要移动 result 里任何元素, 38 直接放进 high 所指位置, result内部 变为:49、*、*、*、*、*、*、38(‘*’表示对应位置上还没有放入元素)。然后更新 first high low high 分别重置为新的 first final middle 重置为 0 。接着处理 list 的下个: 65 ,直接与 middle 指向的 49 比,65>49 ,则 low=middle+1=1 ,从而 low>high ,停止比较。此时的 low 指向了 65 要插入的位置,而且不需要移动 result 里任何元素, 65 直接放进 low 所指向位置,result内部变为: 49、65、*、*、*、*、*、38 。然后更新 final low low high 分别重置为新的 first final middle 重置为 0 。然后便是 list 的下一个 97 ,使用与上述完全相仿的步骤发现,最后其插入位置为 2 ,也是由 low 来指明的。同样仍不需要移动 result 内任何元素,直接把 97 放入 low 指向位置,result内部变为: 49、65、97、*、*、*、*、38 。更新 final low ,同理重置 low high middle 。接着轮到 list 76 ,同理可求得其插入位置为 2 ,也由 low 来指明。由于2原先有97在此,需要让 97向后移动1个位置,到 下标 3 ,以腾出下标 2 放入 76,result内部变为: 49、65、76、97、*、*、*、38 。更新 final 3 ,同理重置 low high middle list 的下一个是 13 13 middle 49 相比较,13<49,则 high=middle 1= 1 ,变为负数的 high 需要重新调整至 result 的末尾 ,high=7,此时low=high ,因此继续进行比较, middle= low+high /2=7 13 与这时 middle 38 比较,13<38,则 high=middle 1=6 ,从而 low>high ,比较结束, high 指明了 13 插入的位置。此时,不需要移动 result 里的任何元素,直接把 13 放进 high 指向的位置,result内部变为: 49、65、76、97 、*、*、13、38 。更新 first high ,同理重置 low high middle 。接着是 list 27 ,同理可以求得插入位置为 6 ,由 high 指明。由于,6这个位置上已有13,此时要让 13向前移动1个位置, 至下标 5 ,腾出下标 6 放入 27,result内部变为: 49、65、76、97 、*、13、27、38 。更新 first 5 ,同理重置 low high middle 。最后来到 list 49 ,其处理过程完全与上文同理,只是 49 middle 相等,应执行的是 low=middle+1 而已。最终result内部变为: 49、 49 65、76、97 、13、27、38。同理更新first、final,由于处理过程结束了,low、high、middle可以不管了,最终的first应该为5,final应该为4(恰好为first 1)。从first开始到result的尾,然后再从result的头开始到final,顺次用相应的元素从list的头更新至list的尾,使得list最后变为:13、27、38、49、49、65、76、97。
          综合上述,我们不难发现,其实 2 路插入排序与折半插入排序原理是相同的,只是具体方式不同而已,主要体现在:1、折半插入排序自始自终都在 list 上进行处理,没有用到辅助的 result 。而 2 路插入排序用到了,且查找过程正是发生在result上而非list上;2、两者最核心的区别主要体现在处理结果的组织上,折半插入排序所有已经处理好的结果均顺次排成 1 个完整序列。而 2 路插入排序顾名思义有“两路”,已经处理好的结果被组织为两个部分,其一为从 result的头(即0) 到当前的 final ,称其为高部分,另一个是从当前的 first result 的末尾(此例为 7 ),称其为低部分。也正因为这样的区别,前者每回处理完后,只需将 high 重置为已更新的处理结果的最尾端。而后者的 first final 均有可能发生变化, low high 均需重置;3、由于每趟处理开始时, low high 有可能分别位于低、高部分,此时的 middle 无法像折半插入排序那样通过 middle= low+high / 2  求出,只能固定为 result 的开头 0 ;4、折半插入排序都是由 low 最终指明插入新元素位置,而 2 路插入排序则为:如果插入位置位于高部分,则由 low 来指明,若位于低部分,则由 high 来指明。另外,在高部分插入元素后,更新的是 final ,而在低部分插入元素,则更新 first 。其它的就没区别了,寻找插入位置时所进行的比较与折半插入排序是完全一样的!此算法目的在于减少折半插入排序中的元素移动次数。
         
从这也看出, 2 路插入排序是稳定排序。代码如下:
#include <string>

using namespace std;

void twoEndsInsertSort(int list[],int length)
{
	int * result=new int [length];
	result[0]=list[0];
	int first=0;
	int final=0;
	
	for(int i=1;i<length;++i)
	{
		int temp=list[i];
		
		/*
		  每趟处理的开头都可能会有以下两种情形:1、first与final分处两端。
		  此时,刚开始比middle小的话会导致B情形。若刚开始不比middle小,则导致D情形。
		  往后的比较不是导致A就是造成D了。显然,该趟处理的最终结果必然是因low>high结束,
		  插入位置可能在已有的低部分,也可能在已有的高部分;2、first与final共处高部分,
		  首先强调,这两者不可能开头共处低部分,result的初始化决定了这点。
		  此时,刚开始比middle小的话会导致C情形,且该趟处理将结束。
		  这样一来,该趟处理最终的插入位置也确定了,且会导致创建原先没有的低部分。 
		  而刚开始不比middle小,则导致D情形,且往后的比较不是导致A就是造成D。
		  该趟处理最终插入位置也就是已有的高部分。所以,每趟处理最终结果都将是后面的3种情形之一。 
		*/
		string flag="the big half";
		int low=first;
		int high=final;
		int middle=0;
		do
		{
			if(temp<result[middle])
			{
				high=middle-1; //A
				
				if(high<0)
				{
					flag="the small half";
					high=length-1; //B
					
					if(low==0) //C
					{
						flag="a new small half";
						break;
					}
				}
			}
			else //D
				low=middle+1;
				
			middle=(low+high)/2;
		}while(low<=high);
		
		if(flag=="a new small half")
		{
			result[length-1]=temp;
			first=length-1;
		}
		if(flag=="the small half")
		{
			for(int j=first;j<=high;++j)
				result[j-1]=result[j];
			result[high]=temp;
			
			--first;
		}
		if(flag=="the big half")
		{
			for(int j=final;j>=low;--j)
				result[j+1]=result[j];
			result[low]=temp;
			
			++final;
		}
	}
	
	int i=0;
	for(int j=first;j<length;++j)
		list[i++]=result[j];
	for(int j=0;j<=final;++j)
		list[i++]=result[j];
		
	delete [] result;
}
         设序列元素个数为n。2路插入排序里头,随序列元素个数变化而变化次数的操作主要还是元素的比较与移动,与折半插入排序同样地,先比较找出插入位置,再统一移动,所以该算法的时间复杂度应主要考虑比较与移动次数的和。不过显然,从上述可看出,由于继承了折半插入排序寻找插入位置的做法,比较次数相较直接插入排序大大减少(与折半插入排序的分析完全相同)。而由于有序部分“兵分两路”,导致连移动次数也相应减少!最好情况有两种:完全顺序或完全逆序,此时总移动次数为0,只有总比较次数为( n 1 )O(logn),总操作次数即为 n 1 O(logn);最坏情况为:序列头个元素最小,其余完全逆序,又或者序列头个元素最大,其余完全顺序。此时,总比较次数仍为 n 1 O(logn),而总移动次数为1+2+……+ n 2= (n 1)(n 2)/ 2(处理原序列第3个元素时才开始需要平移),总操作次数即为 n 1 O(logn)+ (n 1)(n 2)/ 2。综上所述,时间复杂度为O(n2 )。由于排序时使用了与初始序列元素个数相等的result,故空间复杂度为O(n)。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值