1.字典树/Trie树/前缀树
字典树又名前缀树,Trie树,是一种存储大量字符串的树形数据结构,相比于HashMap存储,在存储单词(和语种无关,任意语言都可以)的场景上,节省了大量的内存空间。
下图演示了一个保存了8个单词的字典树的结构,8个单词分别是:“A”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”。
怎么理解这颗树呢?你从根节点走到叶子节点,尝试走一下所有的路径。你会发现,每条从根节点到叶子节点的路径都构成了单词(有的不需要走到叶子节点也是单词,比如 “i” 和 “in”)。trie树里的每个节点只需要保存当前的字符就可以了(当然你也可以额外记录别的信息,比如记录一下如果以当前节点结束是否构成单词)。
2.Leetcode208 实现 Trie (前缀树)
既然让实现前缀树,我们就需要知道前缀树的基本结构,每一个节点都有一个指针数组,指向下一层的节点,同时我们要判断是否是一个完整的单词,我们还需要bool变量。所以Trie树的每一个节点需要两个成员变量
- bool变量判断是否是单词的结尾
- 指针数组指向下一层的节点。
完整代码如下:
const int MAXN = 26;
class Trie {
public:
bool is_str; // 标识当前结点是否为一个完整的字符串
Trie *next[MAXN]; // 下一个结点的指针数组
Trie() {
is_str = NULL;
memset(next,0,sizeof(next)); // 0就代表指针不存在
}
/** Inserts a word into the trie. */
void insert(string word) {
Trie *cur = this; // cur初始化为根结点指针
for(char w : word){ // 遍历word中的每一个字符
if(cur->next[w-'a']==NULL){ // 下一个结点不存在,新增一个结点
Trie* new_node = new Trie();
cur->next[w-'a'] = new_node;
}
cur = cur->next[w-'a'];
}
cur->is_str = true; // 当前结点已经是一个完整的字符串了
}
/** Returns if the word is in the trie. */
bool search(string word) {
Trie *cur = this;
for(char w : word){
if(cur!=NULL)
cur = cur->next[w-'a']; // 更新cur指针的指向,使其指向下一个结点
}
return (cur!=NULL&&cur->is_str); // cur指针不为空且cur指针指向的结点为一个完整的字符串,则成功找到字符串
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
Trie *cur = this;
for(char w : prefix){
if(cur!=NULL)
cur = cur->next[w-'a'];
}
return (cur!=NULL); // 相比search(),这里只需判断cur指针是否为空就行了
}
};
另一种构建方式:
class TrieNode {
public:
bool is_str;
TrieNode* next[26];
TrieNode()
{
is_str = false;
memset(next,0,sizeof(next));
}
};
class Trie {
TrieNode* root;
public:
/** Initialize your data structure here. */
Trie() {
root = new TrieNode();
}
/** Inserts a word into the trie. */
void insert(string word) {
TrieNode* cur = root;
for(int i=0;i<word.size();i++)
{
if(cur->next[word[i]-'a'])
{
cur = cur->next[word[i]-'a'];
}else
{
cur->next[word[i]-'a'] = new TrieNode();
cur = cur->next[word[i]-'a'];
}
}
cur->is_str = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
TrieNode* cur = root;
for(int i=0;i<word.size();i++)
{
if(cur->next[word[i]-'a'])
{
cur = cur->next[word[i]-'a'];
}else
{
return false;
}
}
return cur->is_str;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
TrieNode* cur = root;
for(int i=0;i<prefix.size();i++)
{
if(cur->next[prefix[i]-'a'])
{
cur = cur->next[prefix[i]-'a'];
}else
{
return false;
}
}
return (cur!=NULL);
}
};
3.LeetCode820 单词的压缩编码
题目描述:
这道题其实就是求出公共后缀,只要是后缀或者前缀都可以通过构建前缀树得方式去解决,对于后缀我们把单词反转从而构建就能够求得共有的后缀问题。
比如示例中的[“time”, “me”, “bell”]的逆序就是[“emit”, “em”, “lleb”]。我们可以发现em是emit的前缀。所以"em"就可以忽略了。我们必须要先插入单词长的数组,否则会有问题。比如如果我先插入了"em",再插入"emit",会发现两个都可以插入进去,很显然是不对的,所以在插入之前需要先根据单词的长度由长到短排序。
代码如下:
class TrieNode {
public:
bool is_new;
TrieNode* next[26];
TrieNode()
{
is_new = true;
memset(next,0,sizeof(next));
}
};
class Trie {
TrieNode* root;
public:
/** Initialize your data structure here. */
Trie() {
root = new TrieNode();
}
/** Inserts a word into the trie. */
int insert(string word) {
bool is_new = false;
TrieNode* cur = root;
for(int i=word.size()-1;i>=0;i--) // 倒着插入单词
{
if(cur->next[word[i]-'a'])
{
cur = cur->next[word[i]-'a'];
}else
{
cur->next[word[i]-'a'] = new TrieNode();
cur = cur->next[word[i]-'a'];
is_new = true;
}
}
return is_new?word.size()+1:0;
}
};
class Solution {
public:
int minimumLengthEncoding(vector<string>& words) {
if(words.size()<=0)
return 0;
sort(words.begin(),words.end(),cmp);//按照长短排下序
int res = 0;
Trie* node = new Trie();
for(int i=0;i<words.size();i++)
{
res+=node->insert(words[i]);
}
return res;
}
bool static cmp(string a,string b)
{
return a.size()>b.size();
}
};