1.二分查找算法
二分查找在顺序存储且区间有序的情况下,查找的效率在时间复杂度上为O(logn),算法的核心是减而治之(decrease-and-conquer)在此不做详细介绍,只谈优化。下面请看普通二分查找的代码:
typedef int Rank;
//需要注意边界问题,这里的hi等于区间元素个数,因而While循环不取等号,里边有相应的处理操作。
template <typename T> static Rank binSearch1(T* A, T const & e, Rank lo, Rank hi)
{
while (lo < hi)
{
Rank mi = (lo + hi) >> 1; //以中点为轴点
if (e < A[mi]) hi = mi; //深入前半段[lo, mi)继续查找
else if (A[mi] < e) lo = mi + 1; //深入后半段(mi, hi)继续查找
else return mi; //在mi处命中
}
return -1; //查找失败
} //有多个命中元素时,不能保证返回秩最大者;查找失败时,简单地返回-1,而不能指示失败的位置
从上图可以看出
成功时的平均查找长度为:29/7=4.14…
失败时的平均查找长度为:36/8=4.5
分析:该算法每一次深入时左右比较次数不相同,这就导致该算法的整体性能不是很稳定,并且算法的效率也可以在原理上进行再次优化,出于对平均查找长度的考虑,我们可以看到,二分查找的查找长度为O(1.5logn),针对前边的系数,我们可以进一步优化该算法,这就引入了我们的斐波那契查找算法。
2.斐波那契查找算法(不均衡补偿)
具体代码如下:
fib.h
class Fib { //Fibonacci数列类
private:
int f, g; //f = fib(k - 1), g = fib(k)。均为int型,很快就会数值溢出
public:
Fib(int n) //初始化为不小于n的最小Fibonacci项
{
f = 1; g = 0; while (g < n) next();
} //fib(-1), fib(0),O(log_phi(n))时间
int get() { return g; } //获取当前Fibonacci项,O(1)时间
int next() { g += f; f = g - f; return g; } //转至下一Fibonacci项,O(1)时间
int prev() { f = g - f; g -= f; return g; } //转至上一Fibonacci项,O(1)时间
};
fibsearch.h
#include"fib.h"
typedef int Rank;
template <typename T> static Rank fibSearch1(T* A, T const & e, Rank lo, Rank hi)
{
Fib fib(hi - lo); //用O(log_phi(n = hi - lo)时间创建Fib数列
while (lo < hi)
{ //每步迭代可能要做两次比较判断,有三个分支
while (hi - lo < fib.get()) fib.prev(); //通过向前顺序查找(分摊O(1))——至多迭代几次?
Rank mi = lo + fib.get() - 1; //确定形如Fib(k) - 1的轴点
if (e < A[mi]) hi = mi; //深入前半段[lo, mi)继续查找
else if (A[mi] < e) lo = mi + 1; //深入后半段(mi, hi)继续查找
else return mi; //在mi处命中
} //成功查找可以提前终止
return -1; //查找失败
} //有多个命中元素时,不能保证返回秩最大者;失败时,简单地返回-1,而不能指示失败的位置
3.二分查找(分支平衡)
上面斐波那契查找算法是在二分查找每次向左向右的成本不一样的基础上,将搜索的倾向偏向于成本低的那条路径(左侧),使得进入成本较高的那条路径(右侧)概率变低。 当然,这种左右分支转向的代价不平衡的问题,可以直接解决,具体代码如下:
// 二分查找算法(版本B):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
template <typename T> static Rank binSearch2(T* A, T const & e, Rank lo, Rank hi)
{
while (1 < hi - lo)
{ //每步迭代仅需做一次比较判断,有两个分支;成功查找不能提前终止
Rank mi = (lo + hi) >> 1; //以中点为轴点
(e < A[mi]) ? hi = mi : lo = mi; //经比较后确定深入[lo, mi)或[mi, hi)
} //出口时hi = lo + 1,查找区间仅含一个元素A[lo]
return (e == A[lo]) ? lo : -1; //查找成功时返回对应的秩;否则统一返回-1
}
分析:相对于版本1,最好(坏)情况下更坏(好);各种情况下的,整体性能更加稳定。
4.语义约定
各自遵守以上约定的代码如下:
// 二分查找算法(版本C):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
template <typename T> static Rank binSearch3(T* A, T const & e, Rank lo, Rank hi)
{
while (lo < hi)
{ //每步迭代仅需做一次比较判断,有两个分支
Rank mi = (lo + hi) >> 1; //以中点为轴点
(e < A[mi]) ? hi = mi : lo = mi + 1; //经比较后确定深入[lo, mi)或(mi, hi)
} //成功查找不能提前终止
return --lo; //循环结束时,lo为大于e的元素的最小秩,故lo - 1即不大于(小于等于)e的元素的最大秩
} //有多个命中元素时,总能保证返回秩最大者;查找失败时,能够返回失败的位置
template <typename T> static Rank fibSearch2(T* A, T const & e, Rank lo, Rank hi)
{
Fib fib(hi - lo); //用O(log_phi(n = hi - lo)时间创建Fib数列
while (lo < hi)
{ //每步迭代仅仅做一次比较判断,有两个分支
while (hi - lo < fib.get()) fib.prev(); //通过向前顺序查找(分摊O(1))——至多迭代几次?
Rank mi = lo + fib.get() - 1; //确定形如Fib(k) - 1的轴点
(e < A[mi]) ? hi = mi : lo = mi + 1; //比较后确定深入前半段[lo, mi)或后半段(mi, hi)
} //成功查找不能提前终止
return --lo; //循环结束时,lo为大于e的元素的最小秩,故lo - 1即不大于e的元素的最大秩
} //有多个命中元素时,总能保证返回最秩最大者;查找失败时,能够返回失败的位置