KMP算法2.0+Trie字符串集合

Kmp算法

最近在学算法,发现一个更好的kmp算法的模板,在保留之前那篇关于kmp
算法1.0的博客的基础下决定整理分析kmp2.0版

这一段专门提出来作为模板记录:

for (int i = 2, j = 0; i <= n; i++)
	{
    
    
		while (j && pat[j + 1] != pat[i]) j = Next[j];
		if (pat[j + 1] == pat[i]) j++;
		Next[i] = j;
	}
	for (int i = 1, j = 0; i <= m; i++)
	{
    
    
		while (j && pat[j + 1] != str[i]) j = Next[j];
		if (pat[j + 1] == str[i]) j++;
		if (j == n)
		{
    
    
			cout << i - n << " ";
			j = Next[j];
		}
	}

题目链接–KMP字符串
完整代码

#include<iostream>
using namespace std;
const int N = 1e5 + 5, M = 1e6 + 5;
char str[M], pat[N];
int Next[N];
int main()
{
    
    
	int n, m;
	cin >> n >> pat+1 >> m >> str + 1;//字符串都从第1位开始存储
	//构造Next数组
	for (int i = 2, j = 0; i <= n; i++)
	//关于为什么i从2开始,因为字符串的前缀后缀匹配最长长度的比较要从比总字符串小一除
	//开始比较,原字符串aba,aba就是本身,不分前缀后缀,匹配过程要求至少要比原串短一
	//个字符,所以i要从第二个字符开始枚举。
	{
    
    
		while (j && pat[j + 1] != pat[i]) j = Next[j];
		if (pat[j + 1] == pat[i]) j++;
		//Next[i]记录的是以i为终点与以1为起点匹配的前缀和后缀的最长长度
		//j指向的是前缀的重点,i指向的是后缀的终点(这一句是关键)
		//这一段自己模拟走一步就懂了
		Next[i] = j;
	}
	for (int i = 1, j = 0; i <= m; i++)
	{
    
    
		while (j && pat[j + 1] != str[i]) j = Next[j];
		if (pat[j + 1] == str[i]) j++;
		if (j == n)
		{
    
    
			cout << i - n << " ";
			j = Next[j];//网上回溯
		}
	}
	return 0;
}

在这里插入图片描述

Tire树(字典树)

一般来说字典树的创建都是通过指针,但当面对数据范围不是很大的时候,可以考虑用数组存储,本题的数组存储法更为巧妙
例题链接–Trie字符串统计

int son[N][26], cnt[N], idx;

先解释一下这两个数组和idx的含义:
son是用来存储当前节点的孩子节点的位置的,cnt是用来存储以当前节点为结尾的字符串的个数,idx是用来更新孩子节点位置的
这里的idx用的最为巧妙:
仔细观察数组发现存储Trie树的数组只是一个二维数组,但按照一般Trie树数组存储的方法,每一个节点都有一个长度为26的数组指向该节点的子节点,比如“abc”,根节点的孩子数组长度为26,其中a,b,c是已经存储的,接着到a节点,a节点有一个孩子数组长度也是26,b,c相同,所以对于一个两层的Trie树需要的空间大小是26+26*26
在这里插入图片描述
但这题的idx却是一直递增的,if (!son[p][u]) son[p][u] = ++idx;表示要存储的这个新的字符在树里面是不存在的,son[p][u] = ++idx;就表示对son[p][u]这个节点分配新的空间,++idx表示son[p][u]的孩子节点的指向。
比如:idx初始为0,插入 “abcde” “abcef”,两个字符串前三个字符存储的位置是一样的,在第0,1,2层,a,b的idx(孩子节点的位置)都为1 2,u=str[i]-‘a’,所以第一个字符串‘a’对应为第0层,idx++,那么a的孩子就在第一层
/son[0][0]=1,表示第0层的’a’字符对应的孩子节点在第一层
其他两个同理,这时候第一个字符串到d了,开始时son[[3][‘d’-‘a’]==0,idx++,开辟新的空间,表示该节点的孩子节点的层数为4。这是第一个字符串的操作,那么对于第二个字符串呢,当字符指向e时(str[3]),e字符在第三层并没有被存储,经过第一个字符串的存储,idx已经为5,这个5是第一个字符串结尾字符e的孩子数组指向的位置,当面对第二个字符串的e,idx++,所以e的孩子数组的指向在第6层,与第一个字符串中字符d(str[3])孩子数组的指向位置不是同一个。
在这里插入图片描述
cnt[N]存储的以当前节点为结尾字符串的个数,这个理解了idx的作用和整个结构的存储方式就不难理解。比如"abc",就是cnt[3]++,在存储”abe“,就是cnt[4]++.

模板:

void Insert(char str[])
{
    
    
	int p = 0;
	for (int i = 0; str[i]; i++)
	{
    
    
		int u = str[i] - 'a';
		if (!son[p][u]) son[p][u] = ++idx;
		p = son[p][u];
	}
	cnt[p]++;
}
int Query(char str[])
{
    
    
	int p = 0;
	for (int i = 0; str[i]; i++)
	{
    
    
		int u = str[i]-'a';
		if (!son[p][u]) return 0;
		p = son[p][u];
	}
	return cnt[p];
}

完整代码:

#include<iostream>
using namespace std;
const int N = 1e5 + 5;
int son[N][26], cnt[N], idx;
void Insert(char str[]);
int Query(char str[]);
int main()
{
    
    
	int n;
	cin >> n;
	while (n--) {
    
    
		char c, str[N];
		cin >> c >> str;
		if (c == 'I') Insert(str);
		else cout << Query(str) << endl;
	}
	return 0;
}
void Insert(char str[])
{
    
    
	int p = 0;
	for (int i = 0; str[i]; i++)
	{
    
    
		int u = str[i] - 'a';
		if (!son[p][u]) son[p][u] = ++idx;
		p = son[p][u];
	}
	cnt[p]++;
}
int Query(char str[])
{
    
    
	int p = 0;
	for (int i = 0; str[i]; i++)
	{
    
    
		int u = str[i]-'a';
		if (!son[p][u]) return 0;
		p = son[p][u];
	}
	return cnt[p];
}

猜你喜欢

转载自blog.csdn.net/weixin_50816938/article/details/119010598