c++初学者

专注技术开发

【转】字符串匹配算法(三)位运算的魔法——KR与SO

位运算经常能做出一些不可思议的事情来,例如不用临时变量要交换两个数该怎么做呢?一个没接触过这类问题的人打死他也想不出来。如果拿围棋来做比喻,那么位运算可以喻为编程中的“手筋”。

按位的存储方式能提供最大的存储空间利用率,而随着空间被压缩的同时,由于CPU硬件的直接支持,速度竟然神奇般的提升了。举个例子,普通的数组要实现移位操作,那是O(n)的时间复杂度,而如果用位运算中的移位,就是一个指令搞定了。

KR算法

KR算法之前第一章介绍中说是利用哈希,原文这么介绍的。而我的看法是,哈希只是一个幌子。这个算法的基本步骤同穷举法一样,不同在于每趟比较前先比较一下哈希值,hash值不同就不必比较了。而如果hash值无法高效计算,这样的改进甚至还不如不改进呢。你想想,比较之前还要先计算一遍hash值,有计算的功夫,直接比都比完了。

KR算法为了把挨个字符的比较转化为两个整数的比较,它把一个m长度的字符串直接当成一个整数来对待,以2为基数的整数。这样呢,在第一次算出这个整数后,以后每次移动窗口,只需要移去最高位,再加上最低位,就得出一个新的hash值。但是m太大,导致超出计算机所能处理的最大整数怎么办?不用担心,对整数最大值取模,借助模运算的特性,一切可以完美的进行。而且由于是对整数最大值取模,所以取模这一步都可以忽略掉。

这是KR算法的代码:
  1. #define REHASH(a, b, h) ((((h) - (a)*d) << 1) + (b))
  2. void KR(char *x, int m, char *y, int n) {
  3.    int d, hx, hy, i, j;
  4.    /* Preprocessing */
  5.    /* computes d = 2^(m-1) with
  6.       the left-shift operator */
  7.    for (d = i = 1; i < m; ++i)
  8.       d = (d<<1);
  9.    for (hy = hx = i = 0; i < m; ++i) {
  10.       hx = ((hx<<1) + x[i]);
  11.       hy = ((hy<<1) + y[i]);
  12.    }
  13.    /* Searching */
  14.    j = 0;
  15.    while (j <= n-m) {
  16.       if (hx == hy && memcmp(x, y + j, m) == 0)
  17.          OUTPUT(j);
  18.       hy = REHASH(y[j], y[j + m], hy);
  19.       ++j;
  20.    }
  21. }
我们可以看到,KR算法有O(m)复杂度的预处理的过程,总感觉它的预处理没有反映出模式本身的特点来,导致它的搜索过程依然是O(mn)复杂度的,只不过一般情况下体现不出来,在"aaaaaaaaaaaaaaaaaaaaaaaaa"中搜"aaaaa"就知道KR多慢了。

总的来说,KR算法比穷举强一点,比较次数的期望值是O(m+n)。

Shift Or 算法

为了最大限度的发挥出位运算的能力,Shift Or算法就有了一个最大缺陷:模式不能超过机器字长。按现在普遍的32位机,机器字长就是32,也就是只能用来匹配不大于32个字符的模式。而带来的好处就是匹配过程是O(n)时间复杂度的,达到自动机的速度了。而预处理所花费的时间与空间都为O(m+σ),比自动机少多了。

我们来看看它怎么巧妙的实现“只看一遍”的:

假设我们有一个升级系统,总共有m个级别。每一关都会放一个新人到第0级上,然后对于系统中所有的人,如果通过考验,升一级,否则,咔嚓掉。而对于升到最高级的人,那说明他连续通过了m次考验,这就是我们要选拔的人。

KR算法的思路就是上面的升级规则,给出的考验就是你的位置上的字符与给出的文本字符是否一致。升满级了,说明在连续m个位置上与不断给出的文本字符一致,这也就是匹配成功了。

明白了这个思路后,疑问就开始出来了:检查哪些位置与文本字符一致,需要m次吧?那么整个算法就是O(mn)了?

现在就该位运算出场了,对,这个算法的思路是很笨,但是我位运算的效率高呀,事先我算出字母表中每个字符在模式中出现的位置,用位的方式存在整数里,出现的地方标为0,不出现的地方标为1,这样总共使用σ个整数;同样,我用一个整数来表示升级状态,某个级别有人就标为0,没人就标为1,整个系统升级就恰好可以用“移位”来进行,当检查位置的时候只需要与表示状态的整数“或”1次,所以整个算法就成O(n)了。Shift-Or算法名字就是这样来的。

有一个地方很奇怪,0和1的设定和通常的习惯相反呀,习惯上,喜欢把存在设为1,不存在设为0的。不过这里没有办法,因为移位新移出来的是0。

这时我们来看代码就容易理解多了:
  1. #define WORDSIZE sizeof(int)*8
  2. #define ASIZE 256
  3. int preSo(const char *x, int m, unsigned int S[]) {
  4.         unsigned int j, lim;
  5.         int i;
  6.         for (i = 0; i < ASIZE; ++i)
  7.                 S[i] = ~0;
  8.         for (lim = i = 0, j = 1; i < m; ++i, j <<= 1) {
  9.                 S[x[i]] &= ~j;
  10.                 lim |= j;
  11.         }
  12.         lim = ~(lim>>1);
  13.         return(lim);
  14. }
  15. void SO(const char *x, int m, const char *y, int n) {
  16.         unsigned int lim, state;
  17.         unsigned int S[ASIZE];
  18.         int j;
  19.         if (m > WORDSIZE)
  20.                 error("SO: Use pattern size <= word size");
  21.         /* Preprocessing */
  22.         lim = preSo(x, m, S);
  23.         /* Searching */
  24.         for (state = ~0, j = 0; j < n; ++j) {
  25.                 state = (state<<1) | S[y[j]];
  26.                 if (state < lim)
  27.                         OUTPUT(j - m + 1);
  28.         }
  29. }
代码中lim变量其实就是一个标尺,例如出现最高级的状态是01111111,那么lim就成了10000000,因此只要小于lim,就表示最高级上的0出现了。

原文中对Shift-Or算法的描述还是很难懂的,如果对着那段说明去看代码,有点不知所云的感觉。我还是直接对着代码才想出这个升级的比喻来。

posted on 2008-11-11 13:08 大海 阅读(1112) 评论(0)  编辑 收藏 引用 所属分类: 图像算法C++


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理