5

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

 3 years ago
source link: https://blog.csdn.net/mingyunxiaohai/article/details/87563292
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

chsmy2018 2019-02-17 22:37:22 3463
分类专栏: 数据结构与算法

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

字符串的匹配我们在平时工作中经常用到,每个语言中都有其对应的字符串的匹配方法,比如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;
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK