折半插入排序的优点在于利用折半查找的思想大大减少排序过程中产生的元素比较次数。然而,确定了元素插入位置后,移动元素次数却丝毫没有比直接插入排序的有所减少。有没有一种办法,不但能减少元素的比较次数,还能减少元素的移动次数呢?答案是肯定的,接下来让我们看看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。
仍以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路插入排序是稳定排序。代码如下:
从这也看出,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)。