直接插入排序把原始序列分成两部分,已有序和仍无序部分。每趟排序,从仍无序的部分中选取头个元素,在已有序的部分中寻找插入位置来插入。寻找插入位置的过程显然是个顺序查找的过程。这里,读者或许会提出个问题:已有序部分既然有序了,寻找插入位置实际上是个查找过程,为何不使用折半查找的思想而仍然用顺序查找的思想呢?说的没错,接下来要介绍的折半插入排序正是读者们想看到的。
继续以序列49、38、65、97、76、13、27、 49 为例看看折半插入排序的过程。实际上,该过程与直接插入排序几乎是一样的,唯一不同仅在于寻找插入位置的方式是用折半查找法而非顺序查找法而已。开头直接让49成为有序部分,用low表示该有序部分的头、用high表示该有序部分的尾,middle则表示该有序部分的中间位置,middle=(low+high)/ 2(得出来的结果向下取整,比如,除得2.5的话就取2)。显然,此时low=high=middle=0。取未排序部分的第1个元素38,直接与middle所指的元素49比,38<49,则high=middle-1=-1,出现了low>high的情况,比较过程结束。low指出了38应该插入的位置,则把有序部分位于low及之后的元素全部顺次往后移动1个位置(此时只需移动49),腾出low放入38,结果为: 38 、 49、65、97、76、13、27、 49 。重新让low、high分别指向现在的有序部分的头、尾,重置middle,接着取65。此刻的middle指向38,则65与38比,65>38,从而low=middle+1。仍然有low ≤ high,因而继续,更新middle=(low+high)/ 2=1,则middle指向了49。65>49,则low=middle+1=2,此时low>high,停止比较。low指明了65应该插入的位置,把low及其后的元素顺次往后移动1个位置,腾出low所指位置放入65(由于此时low及其后的所有元素都是无序部分,因而实质上没有任何平移动作),结果为: 38 、 49、65、97、76、13、27、 49 。重新让low、high分别指向现在的有序部分的头、尾,重置middle。接下来,按同样的道理依次处理97、76、13、27、 49 ,相信读者知道如何做下去了,完整过程就不赘述了。这里提示一下,在通过折半查找来寻找插入位置的过程中,若有待处理元素与middle所指元素相等,则同样令low=middle+1,这点与纯粹的折半查找过程不大一样。
折半插入排序是稳定排序。代码如下:
继续以序列49、38、65、97、76、13、27、 49 为例看看折半插入排序的过程。实际上,该过程与直接插入排序几乎是一样的,唯一不同仅在于寻找插入位置的方式是用折半查找法而非顺序查找法而已。开头直接让49成为有序部分,用low表示该有序部分的头、用high表示该有序部分的尾,middle则表示该有序部分的中间位置,middle=(low+high)/ 2(得出来的结果向下取整,比如,除得2.5的话就取2)。显然,此时low=high=middle=0。取未排序部分的第1个元素38,直接与middle所指的元素49比,38<49,则high=middle-1=-1,出现了low>high的情况,比较过程结束。low指出了38应该插入的位置,则把有序部分位于low及之后的元素全部顺次往后移动1个位置(此时只需移动49),腾出low放入38,结果为: 38 、 49、65、97、76、13、27、 49 。重新让low、high分别指向现在的有序部分的头、尾,重置middle,接着取65。此刻的middle指向38,则65与38比,65>38,从而low=middle+1。仍然有low ≤ high,因而继续,更新middle=(low+high)/ 2=1,则middle指向了49。65>49,则low=middle+1=2,此时low>high,停止比较。low指明了65应该插入的位置,把low及其后的元素顺次往后移动1个位置,腾出low所指位置放入65(由于此时low及其后的所有元素都是无序部分,因而实质上没有任何平移动作),结果为: 38 、 49、65、97、76、13、27、 49 。重新让low、high分别指向现在的有序部分的头、尾,重置middle。接下来,按同样的道理依次处理97、76、13、27、 49 ,相信读者知道如何做下去了,完整过程就不赘述了。这里提示一下,在通过折半查找来寻找插入位置的过程中,若有待处理元素与middle所指元素相等,则同样令low=middle+1,这点与纯粹的折半查找过程不大一样。
折半插入排序是稳定排序。代码如下:
void binaryInsertSort(int list[],int length)
{
for(int i=1;i<length;++i)
{
int temp=list[i];
int low=0;
int high=i-1;
do
{
int middle=(low+high)/2;
if(temp<list[middle])
high=middle-1;
else
low=middle+1;
}while(low<=high);
for(int j=i-1;j>=low;--j)
list[j+1]=list[j];
list[low]=temp;
}
}
设序列元素个数为n。对于折半查找而言,只要序列的元素个数固定,无论怎样的序列,把所有元素都各自查找一遍,或者通过折半查找法去构造相应的折半查找树(从无到有地构造),这两个过程中,累计的元素间总比较次数是一致的(这个属于查找的相关知识)。 折半插入排序过程中,除了开头那个直接作为有序部分的元素外,其余元素都必然要通过折半查找法在有序部分中寻找插入位置。在序列元素个数固定的情况下,无论序列如何组织,这些元素在折半插入排序过程中累计下来的总比较次数显然也就是一样的,没有什么最好最坏之分(该过程其实正好对应上述通过折半查找法构造折半查找树的情形)。折半查找的原理告诉我们,每个元素寻找插入位置的时间复杂度平均起来为O(logn),则折半插入排序过程中,n-1个元素通过折半查找在有序部分寻找插入位置,时间复杂度为(n-1)O(logn)。另外,我们可以看出,折半插入排序的元素比较与元素移动是分开进行的,先比较完得出插入位置,再统一移动元素。因此,对于该算法进行时间复杂度分析,主要看比较次数与移动次数的和。至于最好情况,就是序列完全顺序,此时,总移动次数必然为0,总操作次数也就是(n-1)O(logn);最坏情况为完全逆序,第 i 趟排序需要移动元素 i 次,n-1趟的话就要1+2+……+n-1=n(n-1)/ 2次,总操作次数也就是n(n-1)/ 2 +(n-1)O(logn)。综上所述,折半插入排序的时间复杂度为O(
n2
)。由于该算法没有使用随序列元素个数变化而变化数量的
辅助
存储空间,则空间复杂度为O(1)。