本文和大家一起聊聊字典树,从字典二字可知,于功能而言,字典树是类似于英汉字典
的一棵信息树。字典树有 2
大特点:
常用字典树
存储字符串(单词)信息,使用字典树
能方便实现字符串的存储、查询、统计、排序……一系列操作。
字典树
是树结构的典型应用,如下图所示,为一棵字典树。字典树
的叶节点起标志性作用,标记字符串的结束,类似于C++
字符串的结束符号\0
。
通过对结构的观察,可以大致了解字典树的几个特点:
二叉树
,字典树的每一个节点可以有多个子节点,除根节点外每个节点存储一个字符
信息(常用于字符信息存储,但不仅限于字符信息)。字符串
信息。正因为这个特性,字典树
如其名,可存储大量的单词。如从根节点开始,找到它的第一个子节点a
,然后找到a
的子节点c
,再顺着 c
找到e
。这样就能得到子符串ace
。公共前缀
的字符串不需要重复存储,公用共同的祖先,如ace
和act
的公共祖先节点是ac
。这也是字典树的一大特点,相比较其它的存储方案,具有高度的空间利用率。字典树常用的API
有插入
和查询
,其它可根据应用场景的需要进行扩展。
字典树的物理实现可以使用矩阵
和链式
存储 2
种方案,本文分别探讨这 2
种方案的实现过程。
为了简化操作,实现过程中使用了STL
的vector
容器存储任一节点的子节点,当然,也可以使用带有头节点的链表。
节点类型: 存储字典树的节点信息。
#include
#include
using namespace std;
/*
*字典树节点
*/
struct TrieNode {//节点对应字符char data;//所有子节点指针vector childs;TrieNode(){this->data='#';}
};
字典类: 维护字典的常用API
,此处先提供基本的API
,后面根据需要再扩展。
/*
*字典树类
*/
class Trie {private://根节点TrieNode* root;//字典树上所有字符串(单词)vector words;public:/**构建函数*/Trie() {//初始化节结点this->root=new TrieNode();}/**返回节结点*/TrieNode* getRoot() {return this->root;}/** 查询节点是否包括值为 ch 的子节点*/TrieNode* findChild(TrieNode* parent,char ch);/**插入新的单词(字符串)*/void insert(string word);/**查询字典树是否存在指定的字符串(单词)*/TrieNode* search(string word);/** 查询字典树上所有字符串(单词)*/void getAllWords(TrieNode *node,string word);/**显示字典树上的所有单词*/void showAllWords()
}
API
insert
函数功能描述: 提供添加字符串(单词)的功能,是构建字典树的第一重要环节。
**实现流程:**现以添加abc
字符串为例,讲解添加函数的实现过程。
abc
,先读入字符a
,以根节点为当前节点,查询当前节点是否存在值为a
的子节点。a
的子节点,并且当前节点指针
指向新建的节点。继续从字符串中分割出字符b
,检查当前节点是否存在值为b
的子节点。没有则为当前节点创建值为b
的新节点,重设当前节点的指针为新建节点。
同理,分割出c
,因当前节点不存在值为c
的子节点,为当前节点构建新节点。且重设当前节点的指针。
#
的叶节点作为结束标志符。abd
字符串(单词)时,当前节点需要重置为根结点。分割abd
字符串时,因为值为a
和b
的节点已经存在,则仅让当前节点向下滑动。d
字符,因当前节点不存在值为d
的子节点,新建值为d
的子节点。字符串分割完毕,为d
节点添加结束子节点。如下图所示。编码实现:
实现辅助函数findChild
:
/*
* 查询节点是否包括值为 ch 的子节点
*/
TrieNode* Trie::findChild(TrieNode* parent,char ch){//获取当前节点的所有子节点vector childs= parent->childs;//遍历查询for(int i=0; idata==ch ) {//存在return childs[i];}}//不存在return NULL;
}
实现insert
函数:
/*
* 插入新的单词(字符串)
* 参数:word 需要添加的字符串
*/
void Trie::insert(string word) {//从根节点开始TrieNode* currentNode=this->root;//分割字符串for(int i=0; idata=word[i];//成为当前节点的子节点currentNode->childs.push_back(newNode);//重设当前节点currentNode= newNode;} else {//节点存在currentNode= childNode;}}//分割完毕,添加标志性节点TrieNode* flagNode=new TrieNode();currentNode->childs.push_back(flagNode);
}
测试插入函数:
int main(int argc, char** argv) {Trie* trie=new Trie();trie->insert("abc");trie->insert("abd");TrieNode* root=trie->getRoot();cout<<"根节点的子节点:"<childs[0]->data<childs[0]->childs[0]->data<childs[0]->childs[0]->childs[0]->data<childs[0]->childs[0]->childs[1]->data<
输出结果:
search
函数功能描述: 查询给定的字符串(单词)是否存在于字典树中。
实现流程: 查询和插入流程相似。如果检查到存在与分割出来的字符值相等的子节点便继续向下查询,否则,认为查询失败。
不要求一定的是完整字符串(单词),仅是前缀也可以。
如下图所示,虽然字典树中不存在ab
字符串(单词),因为存在ab
前缀,也认为是存在的。
编码实现:
/*
* 查询字典树是否存在指定的字符串(单词)
*/
TrieNode* Trie::search(string word) {//从节结点开始TrieNode* currentNode=this->root;//分割需要查询的字符串for(int i=0; i
测试查询:
int main(int argc, char** argv) {Trie* trie=new Trie();trie->insert("abc");trie->insert("abd");TrieNode* root=trie->getRoot();TrieNode* node= trie->search("ab");if(node!=NULL){cout<<"字典树中存在 ab"<
输出结果:
getAllWords
函数功能描述: 返回字典树上的所有字符串(单词)。
实现流程: 对于整棵树的搜索常用的方案有深度
和广度
搜索。针对此需求使用深度
搜索便能查询出树上的所有单词。
编码实现:
实现辅助函数 showAllWords
:显示字典树上的所有单词。
/*
*显示所有单词
*/
void showAllWords() {cout<<"字典树上的所有单词:"<words.size(); i++) {cout<words[i]<
实现getAllWords
函数:
/*
* 显示字典树上所有字符串
*/
void Trie::getAllWords(TrieNode *node,string word) {//当前节点TrieNode* currentNode=node;//当前节点的子节点vector childs=currentNode->childs;for(int i=0; idata=='#') {//如果节点值为结束符号,则获取到了完整单词this->words.push_back(word);} else {//否则,继续递归string word_=word;word_.append( {childs[i]->data} );getAllWords(childs[i], word_ );}}
}
测试代码:
int main(int argc, char** argv) {Trie* trie=new Trie();trie->insert("abc");trie->insert("abd");TrieNode* root=trie->getRoot();trie->getAllWords(root,"");trie->showAllWords();return 0;
}
输出结果:
使用矩阵
存储树节点之间的关系也是一种常见方案。
基本存储思想:
对每一个节点进行编号。
以父节点的编号为矩阵的行号,子节点的编号为列号,行与列对应的单元格中存储节点的关系描述(或值、权重)。
在存储字典树信息时,对上述的存储方案可以稍加改进一下。
如先确定根节点的编号为0
,子节点a
在矩阵中的列号由其对应的ASCII
码决定,当然,会对其范围缩小。
对应单元格中存储字符出现的顺序编号,并以此编号为此结点的唯一标志符号(类似于主键)。
这个编号与字符添加顺序有关,与字符本身无关。如下图所示:
b
是字符a
的子节点。添加过程如下图所示。上述存储的优点:
26
之内。b
节点下还有字符w
的子节点,便能轻易知道其存储位置是 [2]['w'-'a']
并能获取w
节点的编号。编码实现:
与前文的链式存储相毕较,仅是改变了存储方式,逻辑是没有发生任何变化。
#include
#include
using namespace std;
/*
* 字典树类
*/
class Trie {private:/** 矩阵:存储结点之间的关系* 1、矩阵的行数由结点数量决定, 简化问题,此处设置为 100* 2、矩阵的列数由字符数量决定*/int matrix[100][26];//所有节点由内部统一编号int number;//字符串的节束标志char endFlag[100];//存储字典树上的所有单词vector allWords;public:/** 初始化*/Trie() {this->number=0;for(int i=0; i<100; i++)for(int j=0; j<26; j++)matrix[i][j]=0;}/** 插入函数*/int insert(string word) {//当前节点指向节结点int current=0;//遍历字符串for(int i=0; inumber;}//重设当前节点current=matrix[current][ word[i]-'a' ];}//添加结束标志endFlag[current]='#';}/**查询指定的字符串是否存在字典树中*/int search(string word) {//从根节点开始int current=0;//遍历字符串for(int i=0; iallWords.push_back(word);//恢复word.pop_back();} else {string word_=word;word_.append( { char(i+'a') } );//递归调用getAllWords( matrix[current][i], word_);}}}}/**显示字典树上的所有单词*/void showAllWords() {cout<<"字典树上的所有单词:"<allWords.size(); i++ )cout<allWords[i]<insert("abc");trie->insert("abd");int res= trie->search("ab");cout<getAllWords(0,"");trie->showAllWords();return 0;
}
输出结果:
使用数组存储的优点:
至此,想必对字典树有了较好的理解,下文再提供 2
个案例 ,深入体会字典树的神奇之处。
所谓自动补全:指当用户输入单词前缀,则会显示所有与此前缀有关联的单词。此功能在关键字搜索应用中经常可以看到。
如果字典树中存在单词集["cat","caton","cater","this"]
,当用户输入cat
时,则自动显示所有以cat
为前缀的单词:caton
、cater
。
实现原理:
编码实现:
在前文的矩阵实现方案中添加如下函数。
class Trie{//省略…… /**自动补全函数*/void autoComplete(string prefix) {//查询前缀字符串在字典树中是存在int nodeId= this->search(prefix);if(nodeId==-1) {return;}//存在,则从此节点开始进行深度搜索this->getAllWords(nodeId,prefix);}//省略……
}
//测试
int main() {Trie* trie=new Trie();trie->insert("cat");trie->insert("caton");trie->insert("cater");trie->insert("this");trie->autoComplete("cat");trie->showAllWords();return 0;
}
输出结果:
所谓字符串的公共前缀指字符串前面相同的部分。如caton
和cater
的公共前缀是cat
。与自动补全功能是相逆的操作。
基本思想:
在树中求解节点的公共祖先问题可以有多种方案,本文侧重于字典树的理解,仅提供下面的穷举算法。其它方案可以自行查阅相关书籍。
/*
* 穷举算法求解 2 个字符串的求公共前缀
*/
string getMaxPrefix(string word,string word_) {string prefix="";//指向根结点int current=0;int idx=0;//最多查询结点数int len=word.size()>word_.size()?word_.size():word.size();while(idx
测试:
int main() {Trie* trie=new Trie();trie->insert("cat");trie->insert("caten");trie->insert("cater");trie->insert("this");string prefix=trie->getMaxPrefix("caten","cater");cout<<"公共前缀:"<
输出结果:
本文介绍了字典树的逻辑结构,并且使用链式和矩阵2
种方案实现了字典的物理存储。并且通过 2
个具有代表性的案例让大家更深入理解字典树的实际应用。
上一篇:攻防世界-first
下一篇:JDBC教程上篇