KMP算法详解
前言
算法是一种非常高效地解决字符串匹配算法,算法思想主要是动态规划,如果你想要真正的理解并能够灵活运用需要对动态规划有一定的理解,并且在接下来的扩展与自动机的讲解都是基于动态规划这种算法思想的基础上,如果你还不是很懂动态规划,建议可以先从背包开始。
KMP能够解决什么问题?
例如求解一个字符串是否在文本串中出现过,或者出现了多少次?问题的定义很简单,通过程序解决这个问题也没有难度,难度在于我们如何高效的来解决这个问题,好了,我们先来描述一下暴力解法,遍历文本串,如果当前字符于字符串的首字母相等,那么我们就判断一次是否包含,很显然这种做法非常简单,但是相应的复杂度也很高
/**
* 判断字符串是否在文本串t中出现
* @param s 待判断的字符串
* @param t 文本串
* @return true / false
*/
public boolean isContainSubStr(String s, String t) {
for (int i = 0; i < t.length(); i++) {
if (t.charAt(i) != s.charAt(0)) {
continue;
}
int ti = i, si = 0;
while (ti < t.length() && si < s.length() && t.charAt(ti) == s.charAt(si)) {
ti++; si++;
}
if (si == s.length()) {
return true;
}
}
return false;
}
KMP究竟优化了什么?
在我们的暴力程序中,如果的时候,我们是将指向下一个与相同的位置上,同时的下标又是从开始遍历,想象一种场景,假设,在遍历的时候发现,面对这种情况,算法能够将直接跳到下标为的位置,也就是,这个时候发现的,相比于暴力,这块就少遍历了下标为和两次。我们现在只要知道它的是怎么优化的,具体是怎么做的,继续往下看!
KMP是怎么优化的?
上面那个例子,为什么在的时候,的下标能够直接跳到下标呢,仔细观察的同学应该就发现了,的最长前缀等于后缀的长度为,也就是,我们只需要把字符串以为下标的前后缀最长长度求出来,我们用数组存储,在指针失配的时候,我们就可以获取当前的前一个下标的最长前后缀长度,如;这就是为什么字符串失配后直接跳到下标的原因。
如何求字符串前后缀相等的最长长度?
动态规划思想,我们先定义状态,表示字符串中最长前后缀的长度,如何求解呢,求解之前我们假设已经知道了的结果,那么我们就可以通过递推出,定义,也就是我们知道了,可能这样描述比较抽象,我们举个例子,拿来举例,,现在求,此时 ,如果怎么办,那么我们继续把j指向的位置,直到匹配,为什么这么做呢,就是尽可能的减少重复计算,这块比较抽象,需要自己理解,如果,那么;
程序实现
public static int[] getNext(String s) {
int[] next = new int[s.length()];
for (int i = 1; i < s.length(); i++) {
int j = next[i - 1];
while (j > 0 && s.charAt(i) != s.charAt(j)) j = next[j - 1];
if (s.charAt(i) == s.charAt(j)) j++;
next[i] = j;
}
return next;
}
扩展KMP算法详解
扩展KMP是用来解决什么问题的?
扩展主要是用来求字符串的所有后缀匹配字符串前缀的最长长度,举个例子,,求的所有后缀匹配的最长前缀长度,结果为;
扩展KMP的算法思想
我们首先声明两个数组与,记录单个字符串的所有后缀与本身的前缀最长长度,数组则记录字符串的所有后缀匹配字符串前缀的最长长度。
维护最右区间匹配段
在该算法中,我们从到顺次计算的值。在计算的过程中,我们会利用已经计算好的; 对于i,我们称区间是的匹配段,我们要维护最长的匹配段,记作,根据定义我们可知是字符串的前缀。记录这个有什么用呢?比如我们已经记录了的最右区间匹配段,在求解时,如果的话,那么 ,如果的话,那么,为什么呢,因为后缀匹配的最长前缀长度就是,加上,那么,这里比较抽象最好动手画一画哦
算法详解
求解字符串的所有后缀匹配字符串前缀的最长长度步骤如下:
- 第一步获取字符串的数组
- 第二步判断是否成立,如果成立
- 第三步,如果第二步,说明的长度可能比 的长度要大,那么就要进行暴力匹配计算
- 第四步,如果计算到新的 大于,那么更新区间
程序实现
public static int[] getZNext(String s) {
int[] next = new int[s.length()];
for (int i = 1, l = 0, r = 0; i < s.length(); i++) {
if (i <= r && next[i - l] < r - i + 1) {
next[i] = next[i - l];
} else {
next[i] = Math.max(0, r - i + 1);
while (i + next[i] < s.length() && s.charAt(next[i]) == s.charAt(i + next[i])) ++next[i];
}
if (i + next[i] - 1 > r) {
l = i; r = i + next[i] - 1;
}
}
next[0] = s.length();
return next;
}
public static int[] getZFunction(String s, String t) {
int[] z = new int[s.length()];
int cnt = 0;
while (cnt < Math.min(s.length(), t.length()) && s.charAt(cnt) == t.charAt(cnt)) cnt++;
z[0] = cnt;
int[] next = getZNext(t);
for (int i = 1, l = 0, r = 0; i < s.length(); i++) {
if (i <= r && next[i - l] < r - i + 1) {
z[i] = next[i - l];
} else {
z[i] = Math.max(0, r - i + 1);
while (i + z[i] < s.length() && s.charAt(i + z[i]) == t.charAt(z[i])) ++z[i];
}
if (i + z[i] - 1 > r) {
l = i; r = i + z[i] - 1;
}
}
return z;
}
AC自动机
前言
如果想要学习自动机,建议真正的理解了字典树与,特别是在指针失配时是如何做的,如果没有掌握这些前置知识,不建议直接学习自动机
AC自动机是用来解决什么问题的?
自动机是用来解决多字符串匹配问题的,简单讲就是判断多个字符串在文本串中是否出现过,或者每个字符串在文本串中出现过多少次的问题,是不是感觉与有点相似的感觉,没错,就是一样的,区别就在于是针对单个字符串匹配的,如果一次要判断多个字段串是否匹配的话,那就得建树了,由于是字符串,那么最合适的就是字典树了,不懂字典树的建议去学习一波,很简单,至少比简单!
AC自动机算法思想之Fail指针
这个与求指针如出一辙
如何求Fail指针
指针用来求的,对于直接与根节点相连的节点来说,如果这些节点失配,他们的指针直接指向即可,其他节点其指针求法如下:
假设当前节点为,其孩子节点记为。求的指针时,首先我们要找到其的指针所指向的节点,假如是的话,我们就要看的孩子中有没有和节点所表示的字母相同的节点,如果有的话,这个节点就是的指针,如果发现没有,则需要找这个节点,然后重复上面过程,如果一直找都找不到,则的指针就要指向。
程序实现
static class Tire {
static final int MAXN = 1000010;
static int siz = 0;
static int[] val = new int[MAXN];
static int[] fail = new int[MAXN];
static int[][] ch = new int[MAXN][26];
public static void tire_clear() {
siz = 1; Arrays.fill(ch[0],0);
}
public static int idx(char c) {
return c - 'a';
}
/**
* 往树中插入字符串
* @param s 字符串
* @param v 权值
*/
public static void tire_insert(String s, int v) {
int u = 0, len = s.length();
for (int i = 0; i < len; i++) {
int c = idx(s.charAt(i));
if (ch[u][c] == 0) {
Arrays.fill(ch[siz],0);
val[siz] = 0;
ch[u][c] = siz++;
}
u = ch[u][c];
}
val[u] = v;
}
/**
* 构建Fail指针
*/
public static void tire_build() {
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < 26; i++) {
if (ch[0][i] != 0) queue.add(ch[0][i]);
}
while (!queue.isEmpty()) {
int u = queue.poll();
for (int i = 0; i < 26; i++) {
if (ch[u][i] != 0) {
fail[ch[u][i]] = ch[fail[u]][i];
queue.add(ch[u][i]);
} else {
ch[u][i] = ch[fail[u]][i];
}
}
}
}
/**
* 求出现在文本串中的字符串数量
* @param s 文本串
* @return 出现数量
*/
public static int tire_query(String s) {
int u = 0, res = 0;
for (int i = 0; i < s.length(); i++) {
u = ch[u][idx(s.charAt(i))];
for (int j = u; j > 0 && val[j] != -1; j = fail[j]) {
res += val[j]; val[j] = -1;
}
}
return res;
}
}