相信每个人都曾感受过找东西的痛苦,大多数人也感受过计算机参与资料管理后所带来的便捷,而学过编程的也曾为了某个问题(比如实现“如果不存在则加入”这样的算法描述——排列组合算法的初级阶段)而实现过查找。在SGI-STL的stl_algo.h里面有这样一段代码:
template <class _InputIter, class _Tp>
inline _InputIter find(_InputIter __first, _InputIter __last,
const _Tp& __val,
input_iterator_tag)
{
while (__first != __last && !(*__first == __val))
++__first;
return __first;
}
这个应该能代表我们曾经写过的所有顺序查找代码,关于为什么写成了这个样子,《C++沉思录》第17章有详细的说明。或许VC6的STL代码更容易理解些:
template<class _II, class _Ty> inline
_II find(_II _F, _II _L, const _Ty& _V)
{for (; _F != _L; ++_F)
if (*_F == _V)
break;
return (_F); }
要是这样就完事了该多好,但是,上面的代码只是对于无规则数据的通用查找方法,如果我们想更快,就必须给数据排列添加规则,这样我们就能利用规则来提高查找效率。O(n)和O(logn)毕竟是不能同日而语的,更不要提O(1)了。
正是人们对于速度的追求才导致了这一部分由上面的3行代码变成了庞大无比的一大系算法(其实这也是不得以的事情,5分钟的等待就足以令一个人发狂了)。对于速度的明显提高是建立在n很大的基础上的,这样就不得不涉及到外存,你可以看到,下面的很多演化是因为外存的介入,如果不注意这点,常常会对我们所做的努力感到疑惑。
回忆一下查英汉字典的过程,将有助于我们发现一些查找策略——理论来自于实践,好像不会再有人对这句话有异议了吧?
首先,字典的正文是字典有序的(这个提法有些古怪,和字典的排序方法一样的称作字典有序,然后我又说字典是字典有序的——反正查字典的人都明白字典是怎么排序的,我就不解释了),因此,有些英汉字典没有目录——只要前言和附录不是太多,一般都不会有目录——实际上我们查字典也通常不用目录。具体怎么查呢?
大体上翻一下——a打头就少翻点,p打头就多翻点——如果当前页的末单词(一般来说在页的右上角的单词)排在所查单词的前面,就往后翻——翻多少页自己估量——如果当前页的首单词排在所查单词的后面,就往前翻——翻多少页自己估量。重复这个过程,直到所查单词位于当前页的首末单词之间,然后在当前页查找——这个过程也可以前后比较,但通常都是从前向后搜索一遍。
回忆这个过程,我们能得到很多启示,首先就是“折半查找”思想。
折半查找
当前值大于所查值就往前搜索,小于就往后搜索,这是折半查找的基本思想。想当年曾经为这个算法花费了不少脑细胞,看来还是我太笨的缘故:
int binfind(int a[], int N, int x)
{
int low = 0, high = N - 1, mid;
while (low <= high)
{
mid = (low + high) / 2;
if (a[mid] == x) return mid;
if (a[mid] < x) low = mid + 1;
else high = mid - 1;
}
return N;
}
没有写成模板形式的,是因为支持折半查找的限制比较多,上面的虽然适用范围很小,但是很容易理解。注意到“查字典”的向前(向后)翻的页数是自己估量的,一个查字典熟练的人和一个生手的差别就常常体现在这里。在计算机里,这个“估量”就是插值。这很容易理解——一本书1000页,怎样快速翻到第300页?很显然,翻到书厚度3/10的地方,当然,这不会很准确,但离第300页已经很近了。这个例子的启示是,在均匀序列里,插值应该是有效的。
int insfind(int a[], int N, int x)
{
int low = 0, high = N - 1, mid;
if (x > a[high]) return N;//防止越界
while (low < high)
{
mid = low + (x - a[low])*(high - low)/(a[high] - a[low]);
if (a[mid] == x) return mid;
if (a[mid] < x) low = mid + 1;
else high = mid - 1;
}
return N;
}
测试程序
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include "find.h"
#define N 30000
int a[N];
class timer//单位ms
{
public:
void start() { start_t = clock(); }
clock_t time() { return (clock() - start_t); }
private:
clock_t start_t;
};
int main()
{
timer TIMER; int i, t;
for (i = 0; i < N; i++) a[i] = 2*i+1;
TIMER.start();
// for (i = 0; i < N; i++) seqfind(a, N, rand()%N);
t = TIMER.time();
printf("N=%d %dseqfind TimeSpared: %dms\n", N, N, t);
// printf("%d", a[seqfind(a, N, 2763)]);
TIMER.start();
for (i = 0; i < N; i++) binfind(a, N, rand()%N);
t = TIMER.time();
printf("N=%d %dbinfind TimeSpared: %dms\n", N, N, t);
// printf("%d", a[binfind(a, N, 2763)]);
TIMER.start();
for (i = 0; i < N; i++) insfind(a, N, rand()%N);
t = TIMER.time();
printf("N=%d %dinsfind TimeSpared: %dms\n", N, N, t);
// printf("%d", a[insfind(a, N, 2763)]);
return 0;
}
测试结果
N=30000 30000seqfind TimeSpared: 12107ms
N=30000 30000binfind TimeSpared: 40ms
N=30000 30000insfind TimeSpared: 10ms
注意,这个测试里面包含了查找失败的情况,否则简单的顺序查找的性能不会那么差。