c++初学者

专注技术开发

【转】字符串匹配算法(四)可以滑动多远

记得在穷举法中,每一趟比较后,无论成与不成,都将模式向右滑动一个位置,然后继续比较。有没有办法能利用之前的比较结果,使得模式滑动的更远一点呢?

在介绍经典的KMP算法前,我先介绍几个简单的滑动类算法。

Not So Naive
同名字一样,这个算法的确有点幼稚,它根据模式的前两个字符是否相同来滑动比穷举法稍长一点的距离:如果前两个字符相同,那么文本中与第二个字符不同则必然也与第一个不同;如果前两个字符不同,则与第二个相同的文本字符必然与第一个不同。

那么这两种情况下不用比较都可以断定,文本字符与模式的第一个字符肯定不相同,于是能比穷举法多滑动1个位置。

代码见下:
  1. void NSN(char *x, int m, char *y, int n) {
  2.    int j, k, ell;
  3.   
  4.    /* Preprocessing */
  5.    if (x[0] == x[1]) {
  6.       k = 2;
  7.       ell = 1;
  8.    }
  9.    else {
  10.       k = 1;
  11.       ell = 2;
  12.    }
  13.   
  14.    /* Searching */
  15.    j = 0;
  16.    while (j <= n - m)
  17.       if (x[1] != y[j + 1])
  18.          j += k;
  19.       else {
  20.          if (memcmp(x + 2, y + j + 2, m - 2) == 0 &
  21.              x[0] == y[j])
  22.             OUTPUT(j);
  23.          j += ell;
  24.       }
  25. }
这个算法仅需要常数时间和空间的预处理,比较过程中,先比较模式第二个字符,然后比较其余位置,为的就在某些情况下省掉第一个字符的比较,达到滑动的目的。不过复杂度依然是O(mn)的,比起穷举法或者有轻微改善吧。

想法的确够幼稚,仅仅只考虑了两个模式字符,滑动的步子也太小,能否考虑的更多一点呢?下面请看Quick Search算法。

Quick Search
见到这个名字,不禁让人想起快速排序了,快速排序在最坏情况下是n平方的复杂度,而通常情况下速度超级快,Quick Search莫非也是这样的?没错,就是这样,这个算法在模式长度短而字母表大时,有着优异的表现,尽管它的搜索时间复杂度是O(mn)。

算法的思想是这样,如果文本中某个字符根本就没在模式中出现过,那么就不需要再去和模式中的任何一个比较;如果该字符出现过,那么为了不漏掉可能的匹配,只好与最晚出现过的位置对齐进行比较了。

代码如下:
  1. void preQsBc(char *x, int m, int qsBc[]) {
  2.    int i;
  3.    for (i = 0; i < ASIZE; ++i)
  4.       qsBc[i] = m + 1;
  5.    for (i = 0; i < m; ++i)
  6.       qsBc[x[i]] = m - i;
  7. }
  8. void QS(char *x, int m, char *y, int n) {
  9.    int j, qsBc[ASIZE];
  10.    /* Preprocessing */
  11.    preQsBc(x, m, qsBc);
  12.  
  13.    /* Searching */
  14.    j = 0;
  15.    while (j <= n - m) {
  16.       if (memcmp(x, y + j, m) == 0)
  17.          OUTPUT(j);
  18.       j += qsBc[y[j + m]];               /* shift */
  19.    }
  20. }
理解这个算法,请看22行,无论这一趟比较是否成功,都进行模式串的滑动,这个滑动就是根据窗口之外的第一个字符位于模式串的位置来决定的,你可以把窗口外第一个字符是否能匹配看成下一趟比较的前提。

现在你知道为何这个算法最适合在短模式和大字母表下运行了,因为字母表大,模式短,则文本字符不在模式中出现的几率就大,因此更大可能性得进行最长距离的滑动,而且模式短,花在比较上的时间就短,可以尽量多滑动。

美中不足的是这个算法最坏情况下复杂度还是O(mn),尽管预处理中已经利用上了每一个模式字符了。通过滑动能找到一个线性算法吗?仔细审视一下比较过程,造成算法非线性的根本原因是什么?没错,文本串回溯了。让我们来看看一个真正的线性算法——MP,以及它的改进——KMP。

MP/KMP
本着文本串不回溯的目标,MP算法横空出世,它的一个重要指导思想是,凡是比较过,被认定为相同的文本字符,绝不再拿出来比。道理上也是能说得通的,因为既然和模式串一部分相同,那么它的信息就已经存在于模式串中了。预处理时,模式串自己和自己的一部分进行比较,存储下自身的相似信息——Next数组。

以后在比较时,如果某处失配了,根据之前预处理的结果,可以直接滑动到自身相似的那一部分与文本串对齐,然后从失配处继续比较,避免了文本串回溯。

伟大的计算机科学家Knuth,就是写TAOUP的那位,对MP算法进行了些许修正,加上了自己的名字,成了KMP。Knuth注意到,如果滑动前的那个模式字符与滑动后的模式字符相同的话,那么再比较必然再次失配,导致又一次滑动,与其多级滑动,不如一滑到底。

代码:
  1. void preMp(char *x, int m, int Next[]) {
  2.    int i, j;
  3.    i = 0;
  4.    j = Next[0] = -1;
  5.    while (i < m) {
  6.       while (j > -1 && x[i] != x[j])
  7.          j = Next[j];
  8.       i++;
  9.       j++;
  10.       // 下面注掉的三行去掉注释就成KMP了
  11.       //if (x[i] == x[j])
  12.       //   Next[i] = Next[j];
  13.       //else
  14.          Next[i] = j;
  15.    }
  16. }
  17. void MP(char *x, int m, char *y, int n) {
  18.    int i, j, Next[XSIZE];
  19.    /* Preprocessing */
  20.    preMp(x, m, Next);
  21.    /* Searching */
  22.    i = j = 0;
  23.    while (j < n) {
  24.       while (i > -1 && x[i] != y[j])
  25.          i = Next[i];
  26.       i++;
  27.       j++;
  28.       if (i >= m) {
  29.          OUTPUT(j - i);
  30.          i = Next[i];
  31.       }
  32.    }
  33. }
MP和KMP算法都达到了O(m)的预处理时间和空间,O(n+m)的比较时间,算法实现是如此简单优美,算法思想是如此无可挑剔,还能滑的更远吗?我们拭目以待。

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


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