有序向量
查找
二分查找(版本A)
//在有序向量[lo, hi)内查找元素e,返回其秩
template <typename T> static Rank binSearch(T* A, T const& e, Rank lo, Rank hi)
{
while(lo < hi) //每次迭代至多多两次比较,有三个分支
{
Rank mi = (lo + hi)/2; //以中点为轴点
if(e < A[mi])
hi = mi; //深入前半段继续查找
else if(A[mi] > hi)
lo = mi + 1; //深入后半段继续查找
else
return mi; //e = mi, 在mi处命中
}
return -1; //查找失败
}
这种二分查找方法采用了减而治之的思想,每次迭代之多经过两次比较,,或者找到目标元素,或者将问题转化为一个规模更小的新问题。
缺点:
- 但这种方法有一个问题,就是当向量中可能有个目标元素e时,无法保证返回其中秩最大的那个; 当查找失败时,只简单的返回-1.
- 最坏情况下算法的整体性能比较不稳地,主要是因为向左向右深入时不均衡导致的。
复杂度:O(logn)(每次迭代,有效的查找区间宽度将按1/2的比例以几何级数的速度递减)
二分查找(版本B)
改进思路:使算法无论朝着向左深入还是向右深入的方向都只进行一次比较。
template <typename T> static Rank binSearch(T* A, T const& e, Rank lo, Rank hi)
{
//在有序向量[lo, hi)内查找元素e,返回其秩
template <typename T> static Rank binSearch(T* A, T const& e, Rank lo, Rank hi)
{
while(1 < hi - lo) //每步迭代仅需要进行一次比较判断,有两个分支;成功查找不能提前终止
{
Rank mi = (lo + hi)/2; //以中点为轴点
(e < A[mi] ? hi = mi : lo = mi); //经比较后确定深入[lo, mi)、[mi, hi)哪个区间
}
return (e == A[lo] ? lo : -1); //查找成功返回对应的秩,失败则返回-1。
}
版本B的算法其复杂度O(logn)不变。在这一版本中,只有当向量有效区间缩短至1是循环才终止。不能如版本A那样,一旦命中就返回。一次最后的情况效率有所下降,但最坏的情况效率相应的有所提高,整体性能更加趋于稳定。
语义约定
以上两种查找算法都没有限制如果目标元素有多个时,返回哪个目标元素的。而查找算法一般都有一个语义约定,即:返回不大于目标元素e的最后一个元素。
也就是分以下两种情况:
- 当有多个目标元素e时,应返回最靠后(秩最大)者。
- 当查找失败时,应返回小于e的最大者(含哨兵[lo-1])。
版本C
经过以上的语义约定,进行如下改进
template <typename T> static Rank binsearch(T* A, T const& e, Rank lo, Rank hi)
{
while(lo < hi) //每步迭代值做一次比较,有两个分支。
{
Rank mi = (lo + hi)/2;
(e < A[mi]) ? hi = mi : lo = mi + 1; //经比较确定进入那个区间[lo, mi),(mi, hi).
}
return --lo; //循环结束时,lo为大于e的元素最小秩,故lo - 1即不大于e的元素最大秩。
}
//该算法目标元素有多个时,返回秩最大的那个,当查找失败时,能够返回失败的位置。
版本C与版本B 的差别主要在于循环终止条件变为当有效区间缩减至0时迭代终止。另外在每次转入后端分支时,子向量的左边界去做mi+1而不是mi。
表面上看,可能遗漏元素A[mi],但这种担心大可不必。版本C算法有一下不变性:A[0, lo)中的元素皆不大于e;A[hi, n) 的元素皆大于e。(读者自行验证)
所以循环终止时,lo = hi,此时区间被分成了[0, lo),[hi, n)两部分,由以上的不变性可知:A[lo-1]为不大于e的最大的元素。所有无论是查找成功或失败,只需返回--lo
即可。当成功时:--lo
就是目标元素;失败时--lo
就是小于e的最大的那个元素的秩。
复杂度:O(logn) (遵守了语义约定,且整体性能更加稳定)