一、什么是Trie树
Trie树,即字典树,又叫前缀树或单词查找树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的
它有3个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有儿子节点包含的字符都不相同,若此字典树保存英文单词,则任意结点最多有26个孩子结点
二、字典树的功能
可以用较少的空间保存大量的数据,比如用字典树保存4个字符“c”,“s”,“d”,“n”,那保存的效果就相当于了“c”,“cs”,“csd”,“csdn”四个单词,共10个字符。再比如我们需要保存“eat”,“eating”,“in”,“inn”,“inner”,“locate”,“local”共7个单词,画个图说明一下
从图中可以看出,往字典树中保存一个单词时,我们将每个单词最后一个字符标记为红色表示保存了这个单词。这样我们只是用了3个分支就保存了7个单词,而没有用7个分支。为什么呢?因为部分单词有公共的前缀,这样我们就不需要保存公共部分了。利用了公共前缀这一特点,只需要用较少空间就可以保存大量数据(可根据树还原出原始数据)
此时比如我们要查找locate这个单词,我们从根结点开始,肯定会直接查找存储字符为“l”的单词,直接跳过左边的两个分支,这就使得我们能够很快的查找某个单词。当查找到字符“a”要往后查找时会直接找到保存字符“t”的分支,而不会找保存字符为“l”的分支,最后就可以找到单词“locate”
- 而实际上,我们是用数组实现的字典树,数组中都存放着指向下一个结点的指针,所有具有“亲兄弟关系”的结点保存于同一个数组(长度为26)。
- 结点的数据结构如下:
实际上,将每条边理解成一个字符更容易理解数据结构,若插入单词“b”,那么沿着边“b”到达的这个结点的属性“isWord”被置为true
代码实现
class Trie {
public:
Trie* children[26];//数组里放的不是对象,而是指向对象的指针,通过->调用对象的方法
bool isWord;
Trie() {
memset(children, 0, sizeof(children));
isWord = false;
}
void insert(string word) {
Trie* cur = this;
for (char c : word) {
int index = c - 'a';
if (cur->children[index] == NULL) {
cur->children[index] = new Trie();
}
cur = cur->children[index];
}
cur->isWord = true;
}
bool search(string word) {
Trie* cur = this;
for(char c : word){
int index = c - 'a';
cur = cur->children[index];
if(cur == NULL) return false;//搜索的单词太长
}
return cur->isWord;//单词太短或相等
}
bool startsWith(string prefix) {
Trie* cur = this;
for (char c : prefix) {
int index = c - 'a';
if (cur->children[index] == NULL) {
return false;
}
cur = cur->children[index];
}
return true;
}
};
例题:
这题很显然可以用字典树做,为什么呢?
- 首先这题若某个单词是另一个单词的后缀,那我们就不用计算该单词的长度,只需要关注那个长度更长的单词即可,可以看到S中出现了“time”而没有出现“me”。
- 只要是让我们关注前缀后缀的题目,都可以使用字典树。
本题思路:
- 把题目给的数组按照单词长度降序排序(若先插入短的,再插入长的,长度会统计两次)
- 长的单词先插入字典树(比如time)并返回长单词的长度+1(#),当插入单词每个字符时,判断是否到达叶节点,若没到达叶节点表示当前单词是之前单词的后缀,返回长度0 。不计入最终长度的统计
class Trie{
public:
Trie* children[26];
Trie(){
memset(children, 0, sizeof(children));
}
int insert(string word){
Trie* cur = this;//指向当前树对象的指针
bool isNewWord = false;
for(int i=word.size()-1; i>=0; i--){
int index = word[i] - 'a';
if(cur->children[index] == NULL){
isNewWord = true;
cur->children[index] = new Trie();
}
cur = cur->children[index];
}
return isNewWord ? word.size()+1 : 0;//新单词返回单词长度+1,当前单词是以前单词的前缀,返回0
}
};
class Solution {
public:
static bool cmp(string str1, string str2){
return str1.size() > str2.size();//长度降序排列
}
int minimumLengthEncoding(vector<string>& words) {
sort(words.begin(), words.end(), cmp);//按长度排序
Trie* root = new Trie();//指向对象的指针
int res = 0;
for(string word : words){
res += root->insert(word);
}
return res;
}
};
方法二:逆置并按照字典序排序
参考题解:无需字典树,轻轻一反转,结果就出来
class Solution {
public:
int minimumLengthEncoding(vector<string>& words) {
int n = words.size();
for(int i = 0; i < n; i++){
reverse(words[i].begin(), words[i].end());
}
sort(words.begin(), words.end());
int ans = 0;
for(int i = 0; i < n - 1; i++){
if(words[i + 1].find(words[i]) == 0){
// 当前单词是下一个单词的前缀
continue;
}
ans += words[i].size() + 1;
}
ans += words[n - 1].size() + 1; //按照字典序排序后,最后一个元素不会是谁的后缀
return ans;
}
};