折半插入排序的基本思想是,从已排好序的子数组中快速查找待插入点。
设计折半插入排序的难点在于对边界的处理,怎么判断循环终止条件可以保证没有错误,并且更加清晰和有条理。
为描述方便,设插入元素的关键字为 v,关键字数组为 K。
(规定若 v < K[ m ],找低半区; 若 v >= K[m],找高半区。子数组下标从 1 开始,长度为 len。)
当子数组变成只有两个元素时,
不妨表示为:low = i; high = i+1;
此时:m = (low+high)/2 = i;
由于这样不对称的原因,如果用 [ low, m ] 和 [ m, high ] 两个半区来划分子数组 [ low, high ]。
高半区按这个规则划下去不会再缩减元素个数,始终保持两个。
所以若循环终止条件为 while( low < high ),就有可能产生死循环。
解决方案有:
1.改循环终止条件为 while( low < high-1),循环终止时,low == high-1,可分成三类情况讨论:
①若 low == 1,表明 v < K[ high ],但 v 与 K[ 1 ] 还未比较过,此时若 v < K[ 1 ],插入点为 1,否则插入点为 2;
②若 high == len,表明 v >= K[ low ],但 v 与 K[ len ] 还未比较过,此时若 v < K[ len ],插入点为 len,否则插入点为 len+1;
③其它情况,必然 K[ low ] <= v < K[ high ],则插入点为 high。
2.低半区不变,高半区改为[ m+1, high ]
可避免高半区死循环的发生,循环终止时,low == high,那么,
若 v >= K[ high ],则插入点为 high+1,否则插入点为 high。
3.低半区改为[ low, m-1 ],高半区改为[ m+1, high ],循环终止条件改为 while( low <= high )
循环终止时,low == high+1,插入点为 low。这是因为,在循环的最后一步中,m == low == high,那么,
若 v >= K[ m ],高半区 [ m+1, high ] 不包含任何元素,插入点就刚好在 m 的后边,即 m+1;
若 v < K[ m ],低半区 [ low, m-1 ] 也不包含任何元素,插入点就刚好在 m 的前边,即 m。
这两种情况下,插入点均等于 low,达到了统一。
综上所述,第 3 种方案在代码上最易实现,写法上简洁。
附方案3代码:
// 关键字数组 K 下标从 1 开始,K[0]作为哨兵,n 为关键字的个数
void BinInsertSort( int K[ ], int n )
{
int i, j;
int low, high, m;
for( i = 2; i <= n; ++i ) {
K[0] = K[i];
low = 1; high = i-1;
while( low <= high ){
m = (low+high) / 2;
if( K[0] < K[m] ) high = m-1;
else low = m+1;
}
for( j = i-1; j >= low; --j)
K[j+1] = K[j];
K[low] = K[0];
}
}