先导简介
先来看看trie树的基本介绍:
Trie树(也被称为字典树或前缀树)是一种经常用于字符串处理的树型数据结构。它特别适用于前缀匹配、搜索和插入字符串的操作。 Trie树的基本特点是每个节点代表一个字符,并且从根节点到达某个节点的路径上的字符连接起来构成一个字符串。根节点表示空字符串,每个字符的子节点按照字符顺序存储,并通过指针连接在一起。 Trie树的主要优点是可以高效地存储和检索字符串数据,尤其适用于大量字符串的前缀匹配和搜索场景。然而,Trie树的缺点是可能会占用较大的空间,因为每个节点都需要存储指向所有可能字符的指针。针对这个问题,可以使用各种优化技术,如压缩Trie、三分搜索Trie等。
下面实现的trie树,涉及以下核心技术和基本功能:
插入,删除,词频统计,串排序,模糊匹配,
文本相似度分析,前缀匹配,AC自动化,多模式串匹配
RAII技术自动析构
递归深拷贝
Modern C++编程(ranges rawStr... )
模板
双哈希等等
由C++17/20 实现
并且在最后提供源码 和 测试程序
首先导入一堆头文件
先来看看类定义
类的定义
这里Node是一个嵌套在class trie定义内的struct类型,表示trie树上的节点
Node::hasher:
using hasher = std::unordered_map< char, unique_ptr< Node > >;
hasher作为一个容器,存放每个节点的孩子(指针)域。
注意到许多多叉树的节点结构基本都是 pair< Key, pChild >[ ORDER阶数 ] 类型,比如rb-tree等等,所以hasher容器类型可以这样定义。
特别的是,为了RAII模式管理内存安全,减小手动负担,我用智能独占指针管理Node *的内存
Node::frequency:
frequency是optional< size_t >类型的,初始化为nullopt,表示当前节点并不是一个串的末尾。
这里需要说明相关的语法,当【frequency -> explicit operator bool() (隐式转换)为假】 或者 【frequency.has_value()->bool为假】时,说明该节点出不是任何字符串的结尾,反之,可以通过std::optional相关接口获取内部保有的std::size_t 的值,其含义为该节点的本位字符串出现的频次。
之所以这么定义,是为了避免对于一些返回值类型有二义(不一定为有效值)的函数接口,比如search,count等,大多数
情况下需要自定义return_t结构类型 : (举例) std::pair< Node *, bool >等等,比较冗余
Node::container:
using container = std::unordered_set< std::string >;
container在正常情况下,存储以当前位置为结尾的单个字符串(本位字符串)( 仅当frequency.has_value() == true时为有效节点, 此时container.size() == 1U)
而在trie成为AC自动机后,container需要存储所有与本位串具有最大公共后缀的字符串,为后续的多模式串匹配提供便利
failed:
参考KMP算法,在模式串pattern的迭代过程中失配的情况,利用前后缀性质回跳,即相当于Next数组
求法为BFS,再后面的程序有介绍到。
前缀匹配
这里实现起来比较简单,唯一值得注意的是:
添加图片注释,不超过 140 字(可选)
我对范围for的向前迭代容器pattern做了视图的预处理,用的语法是range-for \operator|()(管道),ranges范围算法库,callable concept等等。这一系列行为让_Char只遍历小写英文字母,方便后续处理(因为懒,有些特判不想做)
构造、析构
数据成员root已经用成员初始化列表初始化了,所以默认构造用的编译器默认提供的版本,移动构造由于unique_ptr的所有权转让,也用了默认版本。
需要注意的是拷贝构造,由于这是一个动态链表实现的树结构,需要深拷贝,而且是递归复制的“克隆”;
这里使用双哈希的方法,在DFS遍历other trie树时,在node指针树上游走同时,平行地拷贝节点。
具体实现逻辑像下面的图示:
插入字符串
插入字符串操作和前面的前缀匹配的实现几乎一样。
只是在这里略有不同:
删除字符串
删除操作更是简单,只需要复用前缀匹配的接口,检查是否为目标字符串,为真就直接把frequency数据域赋空。
查找字符串(透明接口)
无需多言。