字符串算法---Trip树

引言

  • 在常见的字符串匹配中:给定n个字符换,查找某一个字符串,如果用暴力法的话,时间复杂度显然为 O ( m n ) O(mn) O(mn),m为 a v g ( Σ 字符串 ) avg(Σ字符串) avg(Σ字符串)
  • 暴力匹配所需字符串的代码如下:
    #include<iostream>
    #include<string>
    using namespace std;
    const int N = 100;
    string ss[N];
    int main()
    {
    	int n;
    	cin>>n;
    	for(int i = 0; i < n; i++)
    		cin>>ss[i];
    	
    	string p;
    	cin>>p;
    	for(int i = 0; i < n; i++)
    	{
    		if(p.size() != ss[i].size()) continue;
    		for(int j = 0; j < p.size(); j++)
    		{
    			if(p[j] != ss[i][j]) break;
    			else if(p[j] == s[i][j] && j == p.size()-1) 
    			{
    				cout<<"find it";
    				return 0;
    			}
    			else continue;
    		}
    	}
    	cout<<"fail to find";
    	return 0;
    }
    
  • 显然,如果要匹配的n个字符串的数量很大的话,那么暴力法的效率将会非常的低。那么,有什么好的,更高效的匹配方法呢?

Trip树

  • 在日常生活中,大家都有查英文字典的经历,比如查找单词cat,那么把字典先翻到c处,再翻到第二个a,第三个单词t处,就可以找到我们所需要的单词。查询次数仅仅为3次。也即查找某个单词的次数仅仅为其单词的长度
  • 字典树(Trip,又名前缀树),便是模拟查字典的这个操作的一个糕级 高级数据结构。字典树是一颗多叉树,如英文字母的字典树为26叉树,阿拉伯数字0~9则为10叉树。
  • 那么,该如何构造字典树呢?我们先给定以下单词:
    tree,trip,man,mom,may,mana,将其建立成为一颗以下26叉树:
    前缀树样例
  • 如图,多个具有相同前缀的单词,共用前缀。为确定每一个单词出现过,我们用橙色标出,代码则可设置一个相应的标记,表示存在这个单词 (题目给出的,生活中有该单词,题目未给也不存在)
  • 如图可发现,我们仅用很少的结点,存储了好几个单词。由此图我们可以得出前缀树的一些性质:
    • 根节点不放字符!
    • 从根节点到某一结点,路径上的所有字符连接起来就是该节点对应的字符串。
    • 一个完整的单词被一条链存储,且一个结点的所有孩子结点都具有相同的前缀。
  • 给一道例题进行详解(核心代码为insert()建树操作):

维护一个字符串集合,支持两种操作:
1.I x向集合中插入一个字符串 x x x.
2.Q x 询问一个字符串在集合中出现了多少次。

输入格式:

第一行包含整数 N,表示操作数。接下来 N行,每行包含一个操作指令,指令为 I xQ x 中的一种。

输出格式:

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x
在集合中出现的次数。每个结果占一行。

数据范围: 1 < = N < = 2 ∗ 1 0 4 1 <= N <= 2*10^4 1<=N<=2104

#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int N = 2e5+10;

int son[N][26], cnt[N], idx = 0; //0号结点是根结点,空结点。

//新插入一个字符串(可重复插入)
void insert(string  ss)
{
	int p = 0; //从根节点往下遍历,找到空结点后插入新字符,并cnt标记出现次数++
	for(int i = 0; i < ss. size(); i++) //树叉深度为ss的长度
	{
		int u = ss[i] - 'a'; //找到26个单词中的某个
		if(!son[p][u]) son[p][u] = ++idx; //向下遍历,不存在该字母,则建立该字母结点,存在则直接执行下面语句
		p = son[p][u]; //子结点
	}
	cnt[p]++; //最后会停在字母最后一个单词处
}

//查询和插入类似
int Query(string ss)
{
	int p = 0; //根结点
	for(int i = 0; i < ss.size(); i++)
	{
		int u = ss[i] - 'a';
		if(!son[p][u]) return 0; //不存在该单词
		p = son[p][u];
	}
	return cnt[p];
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	int n;
	cin>>n;
	while(n--)
	{
		char op;
		string ss;
		cin >> op;
		cin >> ss;
		if(op == 'I')
		{
			insert(ss); //新添加一个字符串,字符串可重复出现,通过cnt标记出现次数
		}
		else
		{
			//查询某个字符串出现次数
			cout<<Query(ss)<<endl;
		}
	}
	return 0;
}
  • 由以上代码看出:

    • 前缀树是一个牺牲空间换取时间的算法,其son[N][26]每层存1个字符,故图上虽然显示多个结点在同一层,但实际上,每一层有26个结点,但只有一个结点用上。
    • cnt[]用于统计第几层有单词,单词出现次数。
  • 字典树的应用广泛,是后续的回文树,AC自动机等算法的基础。

  • 常见的应用场景有:

    • 字符串的快速检索,时间复杂度为 O ( l o g M ) , M 为字符串长度 O(logM),M为字符串长度 O(logM)M为字符串长度
    • 字符串排序
    • 求单词的最长公共前缀。
    • 求N个整数的最大异或对。
  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
BF算法(Brute-Force算法),也称为朴素匹配算法,是一种简单直观的字符串匹配算法。它的基本思想是从主串的第一个字符开始,依次与模式串的每个字符进行比较,如果匹配成功,则继续比较下一个字符,否则主串指针后移一位,重新开始匹配。这个过程类似于暴力破解密码的过程,因此也被称为暴力匹配算法。 下面是BF算法的C语言实现: ```c #include <stdio.h> #include <string.h> int BF(char* s, char* p) { int i = 0, j = 0; int s_len = strlen(s); int p_len = strlen(p); while (i < s_len && j < p_len) { if (s[i] == p[j]) { i++; j++; } else { i = i - j + 1; j = 0; } } if (j == p_len) { return i - j; } else { return -1; } } int main() { char s[] = "hello world"; char p[] = "world"; int pos = BF(s, p); if (pos != -1) { printf("匹配成功,位置为:%d\n", pos); } else { printf("匹配失败\n"); } return 0; } ``` 在上面的代码中,BF函数接受两个参数,分别是主串s和模式串p。在函数中,我们使用两个指针i和j分别指向主串和模式串的第一个字符,然后依次比较它们的字符是否相等。如果相等,则继续比较下一个字符,否则主串指针后移一位,重新开始匹配。如果模式串匹配成功,则返回匹配的位置,否则返回-1。 需要注意的是,BF算法的时间复杂度为O(m*n),其中m和n分别为主串和模式串的长度。因此,当主串和模式串的长度较大时,BF算法的效率会比较低。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值