原文链接:https://zhuanlan.zhihu.com/p/133535431
查字典
当我们从字典中查找 “algorithm” 这个单词的时候,我们肯定不会傻傻地像二分查找一样首先从中间开始。相反,我们会从首字母为 a 的地方开始查找,然后根据第二个字母在字母表中的位置,找到相应的位置再继续查找,这样重复这个过程,直到我们查找到这个单词。
接下来我们就来介绍一下类似于上述过程的插值查找。
插值查找
插值查找(interpolation search)实际上是二分查找的改良版。假设有这样一个数组 ,我们可以发现,每个相邻元素的差均为
,满足均匀分布。如果要查找元素
,我们首先可以计算数组中小于等于 70 的元素占所有元素的比例的期望值
,而数组的长度
我们知道等于
,所以我们期望查找的索引值就为
,对应的元素为
,恰好就是我们要找的元素。这样,原本用二分法需要查找 3 次的用插值查找只用查找 1 次,大大提高了查找的效率。
这里,我们用一个公式来表示每次查找的期望索引值:
其中,l
和 r
分别代表数组的第一个和最后一个索引,key
代表待查找的元素。
跟二分查找一样,如果一次查找失败,数组的长度就相应地减小,再代入上面的公式继续查找,直到查找成功或失败。
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int formula(int l, int r, int key, int* num)
{
return (r - l) * (key - num[l]) / (num[r] - num[l]);
}
int interpolationSearch(int* num, int key, int n)
{
int l = 0;
int r = n - 1;
int x;
while(l < r) {
x = l + formula(l, r, key, num);
x = max(l, min(x, r)); //不能超过数组的右边界
if(num[x] == key)
return x;
else if(num[x] < key)
l = x + 1;
else
r = x - 1;
}
return -1;
}
int main(int argc, char* argv[])
{
int num[11] = {-10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90};
int key = 70; //要查找的值
int index = -1; //要查找的值得索引,初始化为-1
index = interpolationSearch(num, key, 11);
if(index != -1)
cout << index << endl;
else
cout << "No Such Number!" << endl;
return 0;
}
复杂度分析
插值查找的平均复杂度为 ,但证明过程相当的复杂,这篇论文给出了详细的证明过程,感兴趣的同学可以自己去看看,这里我们就不再讨论了。
要是数组不是均匀分布的,插值查找的复杂度会退化到线性的复杂度 。举一个极端的例子,假设数组为
,我们要查找元素
。第一轮查找我们计算出索引值为
,第二轮为
,第三轮为
,这样我们查找了三次。推广到含有
个元素的数组就需要查找
次,所以复杂度就为
。
因此,插值查找的高效性只针对均匀分布的数组,而对于分布不均匀的数组,插值查找便不再适用了。