题目描述:
实现一个 Trie (前缀树),包含 insert
, search
, 和 startsWith
这三个操作。
示例:
Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // 返回 true trie.search("app"); // 返回 false trie.startsWith("app"); // 返回 true trie.insert("app"); trie.search("app"); // 返回 true
说明:
- 你可以假设所有的输入都是由小写字母
a-z
构成的。 - 保证所有输入均为非空字符串。
解题思路:
Trie树,又称字典树、前缀树,是一种树形结构,是哈希树的变种,是一种用于快速检索的多叉树结构。
典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
Trie树也有它的缺点,Trie树的内存消耗非常大。
假设有and,as,at,cn,com这些关键词,那么如何构建Trie树呢?
从上面可以发现一些Trie树的特性:
- 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
- 从根节点到某一节点的路径上的字符连接起来,就是该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
应用
字典树的查询时间复杂度是O(logL),L是字符串的长度。所以效率还是比较高的。
hash表:
通过hash函数把所有的单词分别hash成key值,查询的时候直接通过hash函数即可,都知道hash表的效率是非常高的,为O(1)。对于单词查询,如果我们hash函数选取的好,计算量少,且冲突少,那单词查询速度肯定是非常快的。那如果hash函数的计算量相对大呢,且冲突率高呢?这些都是要考虑的因素。
另外hash表不支持动态查询,什么叫动态查询,当我们要查询单词apple时,hash表必须等待用户把单词apple输入完毕才能hash查询。当你输入到appl时肯定不可能hash吧。
字典树(tries树):
对于单词查询这种,还是用字典树比较好,但也是有前提的,空间大小允许,字典树的空间相比较hash还是比较浪费的,毕竟hash可以用bit数组。
字典树支持动态查询,比如apple,当用户输入到appl时,字典树此刻的查询位置可以就到达l这个位置,那么我在输入e时光查询e就可以了(更何况如果我们直接用字母的ASCII作下标肯定会更快)!字典树并不用等待你完全输入完毕后才查询。
先遍历字母序在前面的比如abdh,然后abdi。减少了没必要的strcmp,这个很好理解。
abdh和abdi的最长公共前缀是abd,遍历字典树到字母d时,此时这些单词的公共前缀是abd。
我们使用辞典或者是搜索引擎的时候,输入appl,后面会自动显示一堆前缀是appl的东东吧。
那么有可能是通过字典树实现的,前面也说了字典树可以找到公共前缀,我们只需要把剩余的后缀遍历显示出来即可。
代码:
class Trie {
public:
/** Initialize your data structure here. */
Trie(void){
_root = new TrieNode();
}
~Trie(void){
clear(_root);
}
/** Inserts a word into the trie. */
void insert(string word) { // 插入单词
if(word.size() == 0) return;
TrieNode* index = _root;
for(int i = 0; i < word.size(); ++i){
if(index->next[word[i] - 'a'] == NULL)
index->next[word[i] - 'a'] = new TrieNode;
index = index->next[word[i] - 'a'];
}
++index->val;
}
/** Returns if the word is in the trie. */
bool search(string word) { // 查找单词
if(word.size() == 0) return false;;
TrieNode* index = _root;
for(int i = 0; i < word.size(); ++i){
if(index->next[word[i] - 'a'] == NULL)
return false;
index = index->next[word[i] - 'a'];
}
return index->val == 0 ? false : true;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) { // 查找前缀
if(prefix.size() == 0) return false;
TrieNode* index = _root;
for(int i = 0; i < prefix.size(); ++i){
if(index->next[prefix[i] - 'a'] == NULL)
return false;
index = index->next[prefix[i] - 'a'];
}
return true;
}
private:
class TrieNode{ // 定义trie节点
public:
const static int MAX_CHILD = 26;
int val;
TrieNode* next[MAX_CHILD];
TrieNode():val(0){
for(int i = 0; i < MAX_CHILD; ++i)
next[i] = NULL;
}
};
TrieNode* _root;
void clear(TrieNode* root){ // 析构过程
if(root == NULL)
return;
for(int i = 0; i < root->MAX_CHILD; ++i)
clear(root->next[i]);
delete root;
root = NULL;
}
};
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* bool param_2 = obj.search(word);
* bool param_3 = obj.startsWith(prefix);
*/
参考文章:https://blog.csdn.net/jinzhao1993/article/details/52966761 、 https://blog.csdn.net/bqw18744018044/article/details/82502435#%E5%9B%9B%E3%80%81Trie%E7%BB%93%E7%82%B9%E7%9A%84%E5%AE%9A%E4%B9%89