字符串匹配算法(单模式串)

本文是数据结构与算法之美的学习笔记

字符串的匹配我们在平时工作中经常用到,每个语言中都有其对应的字符串的匹配方法,比如Java中的indexOf(),Python中的find()等他们底层都依赖了各种字符串的匹配算法。

字符串的匹配算法有:单模式串匹配算法(BF算法,RK算法,BM算法,KMP算法),
多模式串匹配算法(Trie树,AC自动机)

BF(Brute Force)算法

基础概念:如果我们在A字符串中查找B字符串,那么A就是主串,B就是模式串主串的长度设为n,模式串的长度设为m。

BF算法就是拿模式串m,从主串n的第0位开始匹配,如果匹配不成功,则后移一位继续匹配

第一步

主串 :baddef (匹配不上后移一位)

模式串:abc

第二步

主串 :baddef (匹配不上后移一位)

模式串:abc

第三步

主串 :baddef (匹配不上后移一位)

模式串:abc

第四步

主串 :baddef (匹配不上结束)

模式串:abc

缺点:效率不高,如果主串是aaaaaaaaaa…很多a,模式串是aaaab,那么需要对比的次数非常多

优点:简单不容易出错,在主串和模式串都不太长的情况下这种方法很实用

RK(Rabin-Karp)算法

在上面的BF算法中,如果主串是n,模式串是m,一位一位的往后移动,最坏的情况下将会有n-m+1个子串需要匹配才能找到跟模式串匹配的字符串,比较复杂。

RK算法:数字的匹配比字符串快速,就把主串中的这n-m+1的子串分别求哈希值,然后在分别跟模式串的哈希值进行比较。如果哈希值不一样那肯定不匹配,如果哈希值一样,因为哈希算法存在哈希冲突,这时候在拿模式串跟该子串对比一下就好了。

虽然模式串跟子串的对比速度提高了,但是我们事先需要遍历主串,逐个求子串的哈希值,这部分也挺耗时的,所以需要设计比较高效的哈希算法尽量的减少哈希冲突的产生。

BM(Boyer-Moore)算法

上面两种字符串匹配算法都有缺点,BF算法在极端情况下效率会很低,RK算法需要有一个很好的哈希算法,而设计一个好的哈希算法并不简单,有没有尽可能的高效,极端情况下效率退化也不大的算法呢,下面看看BM算法。

BM算法是一种非常高效的算法,各种记事本的查找功能一般都是采用的这种算法,有实验统计它的效率是KMP算法的3-4倍。

BF算法的核心思想是:在模式串和主串的匹配过程中,当遇到不匹配的字符的时候,BF和RK的做法是往后移动一位,然后从模式串的第一个字符开始从新匹配,而BM算法的思想是找到一种可以一下子移动好几位的规律,直接移动好几位,跳过那些肯定不能匹配的字符,这样匹配的次数就少很多。比如下面:

主串:abcacabdc

模式串:abd

第一次匹配到c,c在模式串中是不存在的,所以我们可以直接移动到c的下一位继续匹配。

BM算法包括两个规则,“坏字符规则”和“好后缀规则”

BM算法是从模式串的尾部开始比较的

(1)坏字符规则:

主串: abcacabdc

模式串:abd

从模式串的尾部开始匹配,如果匹配不成功,这个字符串就叫做“坏字符”,这里的坏字符就是c

然后查找模式串中是否有这个坏字符c,这里是没有,所以c之前的子串肯定不能跟模式串匹配成功,所以我们可以直接移动到c的后面一位继续匹配

主串:abcacabdc

模式串:abd

移动完之后在比较,这时候d跟a还是不匹配,a就是坏字符,这时候发现a是在模式串中存在的,所以我们就把模式串移动到模式串中的a跟这个坏字符a对齐的位置。

注意:如果模式串中有多个a这里选择模式串中最靠后的那个a,防止本来能匹配的被略过,滑动完如下

主串:abcacabdc

模式串:abd

上面就是使用坏字符规则来匹配,但是这也会有极端情况,比如主串aaaaaaaaaaaaaaaa,模式串baaa,这时候使用坏字符规则,会发现倒着移动了,这时候就需要用到好后缀规则了。

(2)好后缀规则:

好后缀和坏字符的思想一样

主串:abcacabcbcbacabc

模式串:cbacabc

上面的匹配从后往前,bc已经匹配成功,到了c和a匹配不成功了,这时候bc叫做好后缀,c叫做坏字符,那使用坏字符还是使用好后缀移动呢?我们可以计算好后缀和坏字符往后滑动的位数,取两个数中最大的往后滑动。

注意: 好后缀的滑动并不是直接滑动到bc的后面,这样会出现问题的,比如上面的例子,其实c是可以和后面的字符合成模式串的,直接移动到c的后面就错过了匹配。正确应该移动到c的下面如下

主串:abcacabcbcbacabc

主串:abcacabcbcbacabc

模式串:cbacbac

所以说,在好后缀移动的时候,我们需要在检查一下已经匹配好的好后缀的后缀(比如abc的后缀就是c,bc),是否跟模式串的前缀子(abc的前缀子串就是a,ab)串匹配。我们从中找出一个最长的匹配,把模式串移动到跟这个匹配到的串对齐的位置就好了。

BM算法完整代码:

// a,b 表示主串和模式串;n,m 表示主串和模式串的长度。
public int bm(char[] a, int n, char[] b, int m) {
  int[] bc = new int[SIZE]; // 记录模式串中每个字符最后出现的位置
  generateBC(b, m, bc); // 构建坏字符哈希表
  int[] suffix = new int[m];
  boolean[] prefix = new boolean[m];
  generateGS(b, m, suffix, prefix);
  int i = 0; // j 表示主串与模式串匹配的第一个字符
  while (i <= n - m) {
    int j;
    for (j = m - 1; j >= 0; --j) { // 模式串从后往前匹配
      if (a[i+j] != b[j]) break; // 坏字符对应模式串中的下标是 j
    }
    if (j < 0) {
      return i; // 匹配成功,返回主串与模式串第一个匹配的字符的位置
    }
    int x = j - bc[(int)a[i+j]];
    int y = 0;
    if (j < m-1) { // 如果有好后缀的话
      y = moveByGS(j, m, suffix, prefix);
    }
    i = i + Math.max(x, y);
  }
  return -1;
}

// j 表示坏字符对应的模式串中的字符下标 ; m 表示模式串长度
private int moveByGS(int j, int m, int[] suffix, boolean[] prefix) {
  int k = m - 1 - j; // 好后缀长度
  if (suffix[k] != -1) return j - suffix[k] +1;
  for (int r = j+2; r <= m-1; ++r) {
    if (prefix[m-r] == true) {
      return r;
    }
  }
  return m;
}

KMP(Knuth Morris Pratt)算法

模式串跟主串左端对齐,先比较第一个字符,如果不一样就,模式串后移,直到第一个字符相等

第一个字符匹配上之后在匹配第二个,直到有不相等的为止,比如下面

主串:cdababaeabac

模式串:ababacd

e和c不匹配,e就可以理解为坏字符,ababa可以理解为好前缀,那移动几位呢?

我们拿好前缀本身,在它的后缀子串中查找最长的那个可以跟好前缀的前缀子串匹配的。

移动位数 = 已匹配的字符数 - 对应的部分匹配值的长度

如何求这个对应的匹配值呢?,这个不涉及到主串只需要根据模式串就可以求出来。

比如这里的模式串是 ababacd

  • a的前缀和后缀都是空,共有元素为0
  • ab的前缀是[a]后缀是[b],共有元素为0
  • aba的前缀是[a,ab]后缀是[ba,a],共有元素a的长度是1
  • abab的前缀是[a,ab,aba]后缀是[bab,ab,b],共有元素是[ab]长度为2
  • ababa的前缀是[a,ab,aba,abab]后缀是[baba,aba,ba,a],最长共有元素是[aba]长度是3
  • ababac的前缀是[a,ab,aba,abab,ababa]后缀是[babac,abac,bac,ac,c]共有元素为0
  • ababacd的前缀是[a,ab,aba,abab,ababa,ababac]后缀是[babacd,abacd,bacd,acd,cd,d]共有元素是0

代码实现

// a, b 分别是主串和模式串;n, m 分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
  int[] next = getNexts(b, m);
  int j = 0;
  for (int i = 0; i < n; ++i) {
    while (j > 0 && a[i] != b[j]) { // 一直找到 a[i] 和 b[j]
      j = next[j - 1] + 1;
    }
    if (a[i] == b[j]) {
      ++j;
    }
    if (j == m) { // 找到匹配模式串的了
      return i - m + 1;
    }
  }
  return -1;
}
// b 表示模式串,m 表示模式串的长度
private static int[] getNexts(char[] b, int m) {
  int[] next = new int[m];
  next[0] = -1;
  int k = -1;
  for (int i = 1; i < m; ++i) {
    while (k != -1 && b[k + 1] != b[i]) {
      k = next[k];
    }
    if (b[k + 1] == b[i]) {
      ++k;
    }
    next[i] = k;
  }
  return next;
}

你可能感兴趣的:(数据结构与算法)