字符串检索算法:KMP和Trie树

目录

1.引言

2.KMP算法

3.Trie树

3.1.简介

3.2.Trie树的应用场景

3.3.复杂度分析

3.4.Trie 树的优缺点

3.5.示例


1.引言

        字符串匹配,给定一个主串 S 和一个模式串 P,判断 P 是否是 S 的子串,即找到 P 在 S 中第一次出现的位置。暴力匹配的思路是:从主串 S 的每个位置开始,逐个字符与模式串 P 比较。若匹配失败,主串指针回退到起始位置的下一个位置,重新开始匹配。 时间复杂度:最坏情况下为 O(n×m)(n 为主串长度,m 为模式串长度)。缺陷:当模式串存在重复前缀或后缀时,重复比较了很多已知信息,效率低下。于是就引出了KMP算法。

2.KMP算法

        要理解KMP算法,首先要搞清楚真前缀与真后缀。在一个字符串中,真前缀是指除了最后一个字符外,一个字符串的头部连续的若干字符;真后缀是指除了第一个字符外,一个字符串的尾部连续的若干字符。举个例子:

        字符串:"ABCDABD"

        真前缀:"A"、"AB"、"ABC"、"ABCD"、"ABCDA"、"ABCDAB"

        真后缀:"BCDABD"、"CDABD"、"DABD"、"ABD"、"BD"、"D"

递推计算next数组

        next 数组的求解基于“真前缀”和“真后缀”,即next[i]等于P[0]...P[i - 1]最长的相同真前后缀的长度(首先设置next[0]=-1,边界条件)。我们以表格为例:

i01234567
模式串ABCDABD'\0'
next[ i ]-10000120
  1. i = 0,对于模式串的首字符,我们统一为next[0] = -1
  2. i = 1,前面的字符串为A,其最长相同真前后缀长度为 0,即next[1] = 0
  3. i = 2,前面的字符串为AB,其最长相同真前后缀长度为 0,即next[2] = 0
  4. i = 3,前面的字符串为ABC,其最长相同真前后缀长度为 0,即next[3] = 0
  5. i = 4,前面的字符串为ABCD,其最长相同真前后缀长度为 0,即next[4] = 0
  6. i = 5,前面的字符串为ABCDA,其最长相同真前后缀为A,即next[5] = 1
  7. i = 6,前面的字符串为ABCDAB,其最长相同真前后缀为AB,即next[6] = 2
  8. i = 7,前面的字符串为ABCDABD,其最长相同真前后缀长度为 0,即next[7] = 0

        那么,为什么根据最长相同真前后缀的长度就可以实现在不匹配情况下的跳转呢?举个代表性的例子:假如i = 6时不匹配,此时我们是知道其位置前的字符串为ABCDAB,仔细观察这个字符串,首尾都有一个AB,既然在i = 6处的 D 不匹配,我们为何不直接把i = 2处的 C 拿过来继续比较呢,因为都有一个AB啊,而这个AB就是ABCDAB的最长相同真前后缀,其长度 2 正好是跳转的下标位置。

        思路如此简单,接下来就是代码实现了,如下:

// 生成Next数组, 示例:“GTGTGCF”
std::vector<int> buildNext(const std::string& pattern) {std::vector<int> next(pattern.size(), 0);int j = 0;for (int i = 2; i < pattern.length(); i++) {while (j != 0 && pattern[j] != pattern[i - 1]) {//从next[i+1]的求解回溯到 next[j]j = next[j];}if (pattern[j] == pattern[i - 1]) {j++;}next[i] = j;}return next;
}
int kmpSearch(const std::string& text, const std::string& pattern) {//预处理,生成next数组std::vector<int> next(std::move(buildNext(pattern)));int j = 0;//主循环,遍历主串字符for (int i = 0; i < text.length(); i++) {while (j > 0 && text[i] != pattern[j]) {//遇到坏字符时,查询next数组并改变模式串的起点j = next[j];}if (text[i] == pattern[j]) {j++;}if (j == pattern.length()) {//匹配成功,返回下标return i - pattern.length() + 1;}}return  -1;
}

复杂度分析:

  • 时间复杂度
    • 构建 next 数组:O(m)(每个字符最多被访问两次)。
    • 匹配过程:O(n)(主串指针 i 仅递增,不回退)。
    • 总复杂度:O(n+m),优于暴力匹配的 O(n×m)。
  • 空间复杂度:O(m)(存储 next 数组)。

3.Trie树

3.1.简介

        Trie树,即前缀树,又称单词查找树,字典树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。

  Trie树的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

它有3个基本性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。

举一个例子。给出一组单词,inn, int, at, age, adv, ant, 我们可以得到下面的Trie:

3.2.Trie树的应用场景

字符串检索,词频统计,搜索引擎的热门查询

        trie树在大数据查找和检索方面具有独特的优势,不过就是要求内存比较高,不过在没有内存限制的情况不适为一种好的方式,如:(节选自此文:海量数据处理面试题集锦与Bit-map详解)

a)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

b) 1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?

c)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。

d)一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析

e) 给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

(1) 请描述你解决这个问题的思路;
(2) 请给出主要的处理流程,算法,以及算法的复杂度。

字符串最长公共前缀

        Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:

      给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少.  解决方案:

        首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线  (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

       而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

        1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;

        2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

3.3.复杂度分析

  • 插入操作:时间复杂度为 O (m),这里的 m 指的是字符串的长度。
  • 查找操作:时间复杂度同样为 O (m)。
  • 空间复杂度:空间复杂度为 O (n),n 表示所有字符串中不同字符的总数,这一特点使得 Trie 树在处理大量字符串时非常高效。

3.4.Trie 树的优缺点

优点

  • 高效前缀匹配:快速查找所有以某前缀开头的字符串(如搜索提示)。
  • 避免重复存储:共享公共前缀,节省空间。
  • 时间复杂度稳定:插入、查询、删除的时间复杂度均为 O(n)(n 为字符串长度)。

缺点

  • 空间开销大:每个字符占用一个节点,可能浪费内存(尤其是字符集大时)。
  • 实现复杂:需要处理动态节点分配和指针操作。

3.5.示例

示例一:一个字符串类型的数组arr1,另一个字符串类型的数组arr2。

  • arr2中有哪些字符串,是arr1中出现的?请打印
  • arr2中有哪些字符串,是作为arr1中某个字符串前缀出现的?请打印
  • arr2中有哪些字符串,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。

实现代码如下:

#include <iostream>
#include <string>
#include <string.h>using namespace std;
const int MaxBranchNum = 26;//可以扩展class TrieNode{
public:string word;int path;  //该字符被划过多少次,用以统计以该字符串作为前缀的字符串的个数int End; //以该字符结尾的字符串TrieNode* nexts[MaxBranchNum];TrieNode(){word = "";path = 0;End = 0;memset(nexts,NULL,sizeof(TrieNode*) * MaxBranchNum);}};class TrieTree{
private:TrieNode *root;
public:TrieTree();~TrieTree();//插入字符串strvoid insert(string str);//查询字符串str是否出现过,并返回作为前缀几次int search(string str);//删除字符串strvoid Delete(string str);void destory(TrieNode* root);//打印树中的所有节点void printAll();//打印以str作为前缀的单词void printPre(string str);//按照字典顺序输出以root为根的所有单词void Print(TrieNode* root);//返回以str为前缀的单词的个数int prefixNumbers(string str);
};TrieTree::TrieTree()
{root = new TrieNode();
}TrieTree::~TrieTree()
{destory(root);
}void TrieTree::destory(TrieNode* root)
{if(root == nullptr)return ;for(int i=0;i<MaxBranchNum;i++){destory(root->nexts[i]);}delete root;root = nullptr;
}void TrieTree::insert(string str)
{if(str == "")return ;char buf[str.size()];strcpy(buf, str.c_str());TrieNode* node = root;int index = 0;for(int i=0; i<strlen(buf); i++){index = buf[i] - 'a';if(node->nexts[index] == nullptr){node->nexts[index] = new TrieNode();}node = node->nexts[index];node->path++;//有一条路径划过这个节点}node->End++;node->word = str;
}int TrieTree::search(string str)
{if(str == "")return 0;char buf[str.size()];strcpy(buf, str.c_str());TrieNode* node = root;int index = 0;for(int i=0;i<strlen(buf);i++){index = buf[i] - 'a';if(node->nexts[index] == nullptr){return 0;}node = node->nexts[index];}if(node != nullptr){return node->End;}else{return 0;}
}void TrieTree::Delete(string str)
{if(str == "")return ;char buf[str.size()];strcpy(buf, str.c_str());TrieNode* node = root;TrieNode* tmp;int index = 0;for(int i = 0 ; i<str.size();i++){index = buf[i] - 'a';tmp = node->nexts[index];if(--node->nexts[index]->path == 0){delete node->nexts[index];}node = tmp;}node->End--;
}int TrieTree::prefixNumbers(string str)
{if(str == "")return 0;char buf[str.size()];strcpy(buf, str.c_str());TrieNode* node = root;int index = 0;for(int i=0;i<strlen(buf);i++){index = buf[i] - 'a';if(node->nexts[index] == nullptr){return 0;}node = node->nexts[index];}return node->path;
}
void TrieTree::printPre(string str)
{if(str == "")return ;char buf[str.size()];strcpy(buf, str.c_str());TrieNode* node = root;int index = 0;for(int i=0;i<strlen(buf);i++){index = buf[i] - 'a';if(node->nexts[index] == nullptr){return ;}node = node->nexts[index];}Print(node);
}void TrieTree::Print(TrieNode* node)
{if(node == nullptr)return ;if(node->word != ""){cout<<node->word<<" "<<node->path<<endl;}for(int i = 0;i<MaxBranchNum;i++){Print(node->nexts[i]);}
}void TrieTree::printAll()
{Print(root);
}int main()
{cout << "Hello world!" << endl;TrieTree trie;string str = "li";cout<<trie.search(str)<<endl;trie.insert(str);cout<<trie.search(str)<<endl;trie.Delete(str);cout<<trie.search(str)<<endl;trie.insert(str);cout<<trie.search(str)<<endl;trie.insert(str);cout<<trie.search(str)<<endl;trie.Delete("li");cout<<trie.search(str)<<endl;trie.Delete("li");cout<<trie.search(str)<<endl;trie.insert("lia");trie.insert("lic");trie.insert("liab");trie.insert("liad");trie.Delete("lia");cout<<trie.search("lia")<<endl;cout<<trie.prefixNumbers("lia")<<endl;return 0;
}

示例二:实现 Trie 树,包含插入、查找、前缀搜索和删除功能。这个实现使用智能指针管理内存,确保内存安全。代码如下:

#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>class TrieNode {
public:std::unordered_map<char, std::unique_ptr<TrieNode>> children;bool is_end_of_word;TrieNode() : is_end_of_word(false) {}
};class Trie {
private:std::unique_ptr<TrieNode> root;// 辅助函数:递归删除单词bool remove(TrieNode* current, const std::string& word, int index) {if (index == word.length()) {if (!current->is_end_of_word)return false;current->is_end_of_word = false;return current->children.empty();}char ch = word[index];auto it = current->children.find(ch);if (it == current->children.end())return false;bool shouldDeleteCurrentNode = remove(it->second.get(), word, index + 1) && !it->second->is_end_of_word;if (shouldDeleteCurrentNode) {current->children.erase(ch);return current->children.empty();}return false;}public:Trie() : root(std::make_unique<TrieNode>()) {}// 插入单词void insert(const std::string& word) {TrieNode* current = root.get();for (char ch : word) {if (!current->children.count(ch)) {current->children[ch] = std::make_unique<TrieNode>();}current = current->children[ch].get();}current->is_end_of_word = true;}// 查找单词bool search(const std::string& word) const {const TrieNode* current = root.get();for (char ch : word) {auto it = current->children.find(ch);if (it == current->children.end())return false;current = it->second.get();}return current->is_end_of_word;}// 查找前缀bool startsWith(const std::string& prefix) const {const TrieNode* current = root.get();for (char ch : prefix) {auto it = current->children.find(ch);if (it == current->children.end())return false;current = it->second.get();}return true;}// 删除单词void deleteWord(const std::string& word) {remove(root.get(), word, 0);}
};// 使用示例
int main() {Trie trie;trie.insert("apple");std::cout << std::boolalpha;std::cout << trie.search("apple") << std::endl;   // 输出: truestd::cout << trie.search("app") << std::endl;     // 输出: falsestd::cout << trie.startsWith("app") << std::endl; // 输出: truetrie.insert("app");std::cout << trie.search("app") << std::endl;     // 输出: truetrie.deleteWord("apple");std::cout << trie.search("apple") << std::endl;   // 输出: falsestd::cout << trie.search("app") << std::endl;     // 输出: truereturn 0;
}

这个 C++ 实现具有以下特点:

  1. 内存安全:使用 std::unique_ptr 管理节点内存,避免内存泄漏
  2. 高效查找:利用 unordered_map 实现 O (1) 的子节点查找
  3. 完整功能:包含插入、查找、前缀搜索和删除操作
  4. 递归删除:删除操作会自动清理不再使用的节点

你可以根据需要扩展这个实现,例如添加统计单词数量、获取所有以特定前缀开头的单词等功能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/83044.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机组成原理:I/O

计算机组成:I/O I/O概述I/O系统构成I/O接口I/O端口两种编址区分I/O数据传送控制方式程序查询方式独占查询中断控制方式硬件判优法(向量中断法)多重中断嵌套DMA控制方式三种DMA方式DMA操作步骤内部异常和中断异常和中断的关系I/O概述 I/O系统构成 一个最基础I/O系统的构成:CPU…

ssti模板注入学习

ssti模板注入原理 ssti模板注入是一种基于服务器的模板引擎的特性和漏洞产生的一种漏洞&#xff0c;通过将而已代码注入模板中实现的服务器的攻击 模板引擎 为什么要有模板引擎 在web开发中&#xff0c;为了使用户界面与业务数据&#xff08;内容&#xff09;分离而产生的&…

NVMe简介2

共分2部分&#xff0c;这里是第2部分。 NVMe数据结构 NVMe协议中规定每个提交命令的大小为64字节&#xff0c;完成命令大小为16字节&#xff0c;NVMe命令分为Admin和IO两类&#xff0c;NVMe的数据块组织方式有PRP和SGL两种。提交命令的格式如图5所示。 图5 提交命令数据格 N…

高压启动电路--学习记录

常见反激的启动电路 优点&#xff1a;电路设计简单&#xff0c;价格便宜 缺点&#xff1a;损坏大&#xff0c;输入宽范围的时候&#xff0c;为了保证低压能正常启动&#xff0c;启动电阻阻值需要选小&#xff0c;那么高压时损耗会非常大&#xff0c;设计的不好很容易在高压时损…

VS打印printf、cout或者Qt的qDebug等传出的打印信息

在vs中打印printf、cout或者Qt的qDebug等常见的打印信息有时也是必要的&#xff0c;简单的叙述一下过程&#xff1a; 1、在vs中打开你的解决方案。 2、鼠标移动到你的项目名称上&#xff0c;点击鼠标右键&#xff0c;再点击属性&#xff0c;此刻会此项目的属性页。 3、在配置…

苍穹外卖--新增菜品

1.需求分析和设计 产品原型 业务规则&#xff1a; 菜品名称必须是唯一的 菜品必须属于某个分类下&#xff0c;不能单独存在 新增菜品时可以根据情况选择菜品的口味 每个菜品必须对应一张图片 接口设计&#xff1a; 根据类型查询分类(已完成) 文件上传 新增菜品 根据类型…

如何高效集成MySQL数据到金蝶云星空

MySQL数据集成到金蝶云星空&#xff1a;SC采购入库-深圳天一-OK案例分享 在企业信息化建设中&#xff0c;数据的高效流转和准确对接是实现业务流程自动化的关键。本文将聚焦于一个具体的系统对接集成案例——“SC采购入库-深圳天一-OK”&#xff0c;详细探讨如何通过轻易云数据…

【springcloud学习(dalston.sr1)】使用Feign实现接口调用(八)

该系列项目整体介绍及源代码请参照前面写的一篇文章【springcloud学习(dalston.sr1)】项目整体介绍&#xff08;含源代码&#xff09;&#xff08;一&#xff09; &#xff08;一&#xff09;Feign的理解 前面文章【springcloud学习(dalston.sr1)】服务消费者通过restTemplat…

SpringbBoot nginx代理获取用户真实IP

为了演示多级代理场景&#xff0c;我们分配了以下服务器资源&#xff1a; 10.1.9.98&#xff1a;充当客户端10.0.3.137&#xff1a;一级代理10.0.4.105&#xff1a;二级代理10.0.4.129&#xff1a;三级代理10.0.4.120&#xff1a;服务器端 各级代理配置 以下是各级代理的基本配…

实验九视图索引

设计性实验 1. 创建视图V_A包括学号&#xff0c;姓名&#xff0c;性别&#xff0c;课程号&#xff0c;课程名、成绩&#xff1b; 一个语句把学号103 课程号3-105 的姓名改为陆君茹1&#xff0c;性别为女 &#xff0c;然后查看学生表的信息变化&#xff0c;再把上述数据改为原…

typeof运算符和深拷贝

typeof运算符 识别所有值类型识别函数判断是否是引用类型&#xff08;不可再细分&#xff09; //判断所有值类型 let a; typeof a //undefined const strabc; typeof str //string const n100; typeof n //number const …

NAT/代理服务器/内网穿透

目录 一 NAT技术 二 内网穿透/内网打洞 三 代理服务器 一 NAT技术 跨网络传输的时候&#xff0c;私网不能直接访问公网&#xff0c;就引入了NAT能讲私网转换为公网进行访问&#xff0c;主要解决IPv4(2^32)地址不足的问题。 1. NAT原理 当某个内网想访问公网&#xff0c;就必…

Git的安装和配置(idea中配置Git)

一、Git的下载和安装 前提条件&#xff1a;IntelliJ IDEA 版本是2023.3 &#xff0c;那么配置 Git 时推荐使用 Git 2.40.x 或更高版本 下载地址&#xff1a;CNPM Binaries Mirror 操作&#xff1a;打开链接 → 滚动到页面底部 → 选择2.40.x或更高版本的 .exe 文件&#xf…

【教程】Docker更换存储位置

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 背景说明 更换教程 1. 停止 Docker 服务 2. 创建新的存储目录 3. 编辑 Docker 配置文件 4. 迁移已有数据到新位置 5. 启动 Docker 服务 6…

PostgreSQL 配置设置函数

PostgreSQL 配置设置函数 PostgreSQL 提供了一组配置设置函数&#xff08;Configuration Settings Functions&#xff09;&#xff0c;用于查询和修改数据库服务器的运行时配置参数。这些函数为数据库管理员提供了动态管理数据库配置的能力&#xff0c;无需重启数据库服务。 …

sql server 2019 将单用户状态修改为多用户状态

记录两种将单用户状态修改为多用户状态&#xff0c;我曾经成功过的方法&#xff0c;供参考 第一种方法 USE master; GO -- 终止所有活动连接 DECLARE kill_connections NVARCHAR(MAX) ; SELECT kill_connections KILL CAST(session_id AS NVARCHAR(10)) ; FROM sys.dm_ex…

主机A向主机B发送一个长度为L字节的文件,假设TCP的MSS为1460字节,则在TCP的序号不重复使用的前提下,L的最大值是多少?

&#x1f4d8;题干回顾&#xff1a; 主机A向主机B发送一个长度为L字节的文件&#xff0c;假设TCP的MSS为1460字节&#xff0c;则在TCP的序号不重复使用的前提下&#xff0c;L的最大值是多少&#xff1f; 这个问题关键在于“TCP序号不重复使用”。 ✅ 正确答案是&#xff1a;D.…

一次因校时服务器异常引起的性能差异分析

一次因校时服务器异常引起的性能差异分析 一.背景知识1. **TSC 频率**:硬件级高精度计时2. **gettimeofday**:用户态时间接口3. **adjtimex**:系统时钟的软件校准4. **`clock_adjtime(CLOCK_REALTIME, {modes=ADJ_TICK})`**: 用于修改系统时钟中断间隔(`tick` 值)。5. 关系…

acwing 4275. Dijkstra序列

题目背景 输入 输出 完整代码 #include<bits/stdc.h> using namespace std; int n,m,k,a[1010],dist[1010],g[1010][1010],st[1010];int dij(int u){memset(st,0,sizeof st);memset(dist,0x3f,sizeof dist);dist[u]0;for(int i0;i<n;i){int ta[i];for(int j1;j<n;…

[思维模式-37]:什么是事?什么是物?什么事物?如何通过数学的方法阐述事物?

一、基本概念 1、事&#xff08;Event) “事”通常指的是人类在社会生活中的各种活动、行为、事件或情况&#xff0c;具有动态性和过程性&#xff0c;强调的是一种变化、发展或相互作用的流程。 特点 动态性&#xff1a;“事”往往涉及一系列的动作、变化和发展过程。例如&a…