字典树
- 字典树
- 面试
- 例题
-
- 实现字典树
- [添加与搜索单词 - 数据结构设计](#添加与搜索单词 - 数据结构设计)
- [720. 词典中最长的单词](#720. 词典中最长的单词)
- [212. 单词搜索 II](#212. 单词搜索 II)
- [648. 单词替换](#648. 单词替换)
字典树
节点定义:
cpp
struct TrieNode {
// 核心 1:路标数组
// 大小为 26,代表 a-z。
// 如果 children[0] 不为空,说明有通往 'a' 的路。
TrieNode* children[26];
// 核心 2:终点标记
// true 表示从根走到这里,组成了一个完整的单词。
bool isEnd;
TrieNode() {
isEnd = false;
// 初始化:刚开始所有路都不通
for (int i = 0; i < 26; i++) children[i] = nullptr;
}
};
距离:现在有一个空的 Trie,我们要插入字符串 "apple"。
- 站在 Root 节点
- 看第一个字 'a':检查 root->children['a'-'a'] (即索引0) 是否存在?
不存在(nullptr)。
动作:new 一个新节点,把路修好。
移动:指针跳到这个新节点。 - 看第二个字 'p':检查当前节点的 children['p'-'a'] (索引15) 是否存在?
不存在。动作:new 一个新节点。移动:指针跳到新节点。 - ...重复直到 'e'...
- 在 'e' 对应的节点上,打个勾 isEnd = true。
插入逻辑
cpp
void insert(string word) {
TrieNode* node = root; // 1. 指针站在根部
for (char c : word) {
int index = c - 'a'; // 算出走哪条路 (0-25)
// 2. 路没修?那就修一条
if (node->children[index] == nullptr) {
node->children[index] = new TrieNode();
}
// 3. 往前走一步
node = node->children[index];
}
// 4. 走完所有字符,标记这里是终点
node->isEnd = true;
}
查找逻辑(最关键)
- Trie 里有 "apple",我要查 "app" 存在吗?
指针顺着 a -> p -> p 走得很顺畅。
循环结束了,指针停在了第二个 'p' 的节点上。
关键判断:此时检查该节点的 isEnd。
因为我们只插入了 "apple",所以在第二个 'p' 处,isEnd 是 false。
结论:返回 false。 "app" 只是前缀,不是完整单词。 - 场景 B:Trie 里有 "apple",我要查 "banana" 存在吗?
第一个字母 'b'。
检查 root 下的 children['b'-'a']。
发现是 nullptr(路不通)。
结论:直接返回 false。
cpp
bool search(string word) {
TrieNode* node = root;
for (char c : word) {
int index = c - 'a';
// 路断了,直接判定不存在
if (node->children[index] == nullptr) {
return false;
}
node = node->children[index];
}
// 走到最后了,不仅要路通,还得必须是"终点"
return node->isEnd;
}
面试
这是 Trie 碾压哈希表的地方。
如果面试官问:"我要设计一个搜索引擎的自动补全功能,用户输入 'te',我要知道有没有以 'te' 开头的词。"
你用哈希表做不到(除非遍历整个哈希表)。但 Trie 只需要把上面的 search 代码改 一行:
cpp
bool startsWith(string prefix) {
TrieNode* node = root;
for (char c : prefix) {
int index = c - 'a';
if (node->children[index] == nullptr) return false;
node = node->children[index];
}
// 区别就在这!
// 只要能走完路径,不管是不是终点,都说明前缀存在。
return true;
}
例题
实现字典树
cpp
class Trie {
private:
struct TrieNode{
TrieNode* childen[26];
bool isEnd;
TrieNode(){
isEnd = false;
for(int i = 0;i<26;i++){
childen[i] = nullptr;
}
}
};
TrieNode* root;
public:
Trie() {
root = new TrieNode();
}
void insert(string word) {
TrieNode* node = root;
for(char c : word){
int index = c-'a';
if(node->childen[index] == nullptr){
node->childen[index] = new TrieNode();
}
node = node->childen[index];
}
node->isEnd = true;
}
bool search(string word) {
TrieNode* node = root;
for(char c:word){
int index = c-'a';
if(node->childen[index] == nullptr){
return false;
}
node = node->childen[index];
}
return node->isEnd;
}
bool startsWith(string prefix) {
TrieNode* node = root;
for (char c : prefix) {
int index = c - 'a';
if (node->childen[index] == nullptr) {
return false;
}
node = node->childen[index];
}
return true; // 只要能走完路径,就说明前缀存在
}
};
添加与搜索单词 - 数据结构设计
和上一题一样先定义TrieNode节点,以及添加单词
不同的地方在于这题结合了dfs深度优先搜索
首先传入要搜索的词,当前搜索到 word 的第几个字符,当前处在 Trie 树的哪个节点
如果搜索到index == word.size()的时候,返回字母尾巴的isEnd(注意尾巴的isEnd都是true)
接下来定义一下此时的word 的第几个字符(c)
两个if判断如果是正常字符的话就看,此时节点的孩子中是不是nullptr,如果是的话就直接返回false
如果不是进入dfs递归循环
如果是'.'的话利用for循环遍历26个孩子,只要有一个不是nullptr就是可以继续
如果不是nullptr的话进入向下判断
cpp
class WordDictionary {
private:
struct TrieNode{
TrieNode* childen[26];
bool isEnd;
TrieNode(){
isEnd = false;
for(int i = 0;i<26;i++){
childen[i] = nullptr;
}
}
};
TrieNode* root;
bool dfs(string word,int index, TrieNode* node){
if(index == word.size()){
return node->isEnd;
}
char c = word[index];
if(c != '.'){
int i = c-'a';
if(node->childen[i] == nullptr) return false;
return dfs(word,index+1,node->childen[i]);
}else{
for(int i = 0; i < 26; i++){
if (node->childen[i] != nullptr){
if (dfs(word, index + 1, node->childen[i])){
return true;
}
}
}
return false;
}
}
public:
WordDictionary() {
root = new TrieNode();
}
void addWord(string word) {
TrieNode* node = root;
for(char c:word){
int index = c-'a';
if(node->childen[index] == nullptr){
node->childen[index] = new TrieNode();
}
node = node->childen[index];
}
node->isEnd = true;
}
bool search(string word) {
return dfs(word,0,root);
}
};
720. 词典中最长的单词
常规的创建字典树节点结构体,但是新加了一个string word(此时输出结果单词)方便后续的结果替换
另外创建结果字符串(刚开始是空的)
插入也是常规插入,但是多了一个记录此时插入的word
最重要的dfs遍历,首先传入根节点开始dfs的逻辑
在dfs里面,首先判断新进入的节点是不是根节点,因为根节点是空的,我们需要跳过他,去找真正的第一个节点
然后开始判断这里node中word的长度是不是大于result的长度,如果大于就直接将结果替换
或者他们长度相同的情况下,判断此时node的长度是不是比result小,如果小也替换result
遍历26个子节点,如果他们是的isEnd是True(代表之前出现过)(体现原题目中的逐步添加)则继续往下走
cpp
class Solution {
private:
struct TrieNode{
TrieNode* children[26];
bool isEnd;
string word;
TrieNode(){
isEnd = false;
word = "";
for(int i = 0;i<26;i++){
children[i] = nullptr;
}
}
};
TrieNode* root;
string result = "";
public:
void insert(const string& w){
TrieNode* node = root;
for(char c:w){
int index = c-'a';
if(node->children[index] == nullptr){
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->isEnd = true;
node->word = w;
}
void dfs(TrieNode* node){
if(node != root){
if(node->word.size() > result.length() || (node->word.length() == result.length() && node->word < result)){
result = node->word;
}
}
for(int i = 0;i<26;i++){
if(node->children[i]!=nullptr && node->children[i]->isEnd){
dfs(node->children[i]);
}
}
}
string longestWord(vector<string>& words) {
root = new TrieNode();
for(const string& w:words){
insert(w);
}
dfs(root);
return result;
}
};
212. 单词搜索 II
常规插入和建树,但是这里不需要isEnd
判断是不是出棋盘了,出了就返回
然后记录此时的字符和index
看看下面有没有路,没路就返回
有路的话记录下一个节点为nextNode
看看下一个节点里面的word是不是空的(建树的时候在最后一个节点哪里安排上word,不是最有一个节点都是空)
不是空就把结果取出来,并把结果消除(放出其他的路也能拿到这个结果,不同路同一个结果)
回溯操作,标记这个格子,防止回路死循环
上下左右以此递归
然后复原
cpp
class Solution {
private:
struct TrieNode{
TrieNode* children[26];
string word;
TrieNode(){
word = "";
for(int i = 0;i<26;i++){
children[i] = nullptr;
}
}
};
TrieNode* root;
vector<string> result;
void insert(const string& w){
TrieNode* node = root;
for(char c: w){
int index = c-'a';
if(node->children[index] == nullptr){
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->word = w;
}
void dfs(vector<vector<char>>& board,int i,int j,TrieNode* node){
if(i<0||i>=board.size()||j<0||j>=board[0].size()||board[i][j]=='#'){
return;
}
char c= board[i][j];
int index = c-'a';
if (node->children[index] == nullptr) {
return;
}
TrieNode* nextNode = node->children[index];
if(!nextNode->word.empty()){
result.push_back(nextNode->word);
nextNode->word = "";
}
board[i][j] = '#';
dfs(board,i-1,j,nextNode);
dfs(board,i+1,j,nextNode);
dfs(board,i,j+1,nextNode);
dfs(board,i,j-1,nextNode);
board[i][j] = c;
}
public:
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
root = new TrieNode();
for(const string& w : words){
insert(w);
}
int m = board.size();
int n = board[0].size();
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
dfs(board,i,j,root);
}
}
return result;
}
};
648. 单词替换
cpp
class Solution {
private:
struct TrieNode{
TrieNode* children[26];
bool isEnd;
TrieNode(){
isEnd = false;
for(int i =0;i<26;i++){
children[i] = nullptr;
}
}
};
TrieNode* root;
void insert(const string& word){
TrieNode* node = root;
for(char c : word){
int index = c-'a';
if(node->children[index] == nullptr){
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->isEnd = true;
}
string findRoot(const string& word){
TrieNode* node = root;
string prefix = "";
for(char c:word){
int index = c-'a';
if(node->children[index] == nullptr){
return word;
}
node = node->children[index];
prefix += c;
if(node->isEnd){
return prefix;
}
}
return word;
}
public:
string replaceWords(vector<string>& dictionary, string sentence) {
root = new TrieNode();
for(const string& w:dictionary){
insert(w);
}
stringstream ss(sentence);
string word;
string result = "";
while(ss>>word){
if(!result.empty()){
result += " ";
}
result += findRoot(word);
}
return result;
}
};