基本概念
-
Trie 树
又称单词查找树、前缀树,是一种树形结构。典型应用是用于统计、排序和保存大量的字符串(但不仅限于字符串)。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,比哈希表更快。
-
基本性质
①.根节点不包含字符,除根节点外每个节点都只包含一个字符
②.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
③.每个节点的所有子节点包含的字符都不相同
-
基本操作
①.插入:把一个单词插入到字典树
②.查询前缀:判断某个单词是否为一个单词的前缀
③.查询单词:判断某个单词是否已经存在
基本原理
-
字典树的本质
Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。
-
构建原理
Trie 树的插入操作就是将单词的每个字母逐一插入Trie树。插入前先判断字母对应的节点是否存在,存在则移动到下一层继续插入,不存在则创建对应的节点。
实现方法
// TrieNode 节点类,由 a-z 小写字母构成的字典树
class TrieNode
{
private:
int count;//包含子节点数量,可以用于判断是否叶子节点
bool isEnd;//标记是否单词结尾
vector<TrieNode*> children;//存储子节点指针
public:
// 构造
TrieNode():count(0),isEnd(false),children(26,NULL) {}
// 析构
~TrieNode()
{
for(int i = 0;i < 26;i++ )
{
if( children[i] ) delete children[i];
}
}
// 对外系列接口
int size() { return count ;} // 返回子节点数量
TrieNode* insertNode(char c) // 插入一个子节点,并返回其指针
{
if( c < 'a' || c > 'z' ) return NULL;
if( children[c - 'a'] == NULL)
{
children[c - 'a'] = new TrieNode();
count++;
}
return children[c - 'a'] ;
}
TrieNode* getNode( char c )//返回指定子节点
{
if( c < 'a' || c > 'z' ) return NULL;
return children[c - 'a'] ;
}
bool idWordEnd(){ return isEnd;}//返回是否单词结尾
void setEnd() { isEnd = true ;}//标记本节点为单词结尾
};
// Trie 类,封装操作接口
class Trie {
private:
TrieNode * root;//根节点
public:
// 构造
Trie() : root( new TrieNode() ){}
// 析构
~Trie()
{
delete root;
}
// 插入一个单词
void insert(string word) {
TrieNode * p = root;
for(int i = 0;i < word.size();i++ )
{
p = p->insertNode(word[i]);
}
p->setEnd() ;
}
//逆序插入一个单词
void insertReverse(string word) {
TrieNode * p = root;
for(int i = word.size() -1;i >-1;i-- )
{
p = p->insertNode(word[i]);
}
p->setEnd() ;
}
//根据单词返回节点
TrieNode *getNode(string word)
{
TrieNode * p = root;
for(int i = 0;i < word.size();i++ )
{
p = p->getNode(word[i]) ;
if( p == NULL ) return NULL;
}
return p;
}
// 判断指定单词是否存在
bool search(string word) {
TrieNode * p = getNode(word);
if( p )
return p->idWordEnd();
return false;
}
//判断指定前缀是否存在
bool startsWith(string prefix) {
TrieNode * p = getNode(prefix);
return p != NULL;
}
};
字典树应用
你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"I reset the computer. It still didn’t boot!“已经变成了"iresetthecomputeritstilldidntboot”。在处理标点符号和大小写之前,你得先把它断成词语。当然了,你有一本厚厚的词典dictionary,不过,有些词没在词典里。假设文章用sentence表示,设计一个算法,把文章断开,要求未识别的字符最少,返回未识别的字符数。
示例:
输入:
dictionary = [“looked”,“just”,“like”,“her”,“brother”]
sentence = “jesslookedjustliketimherbrother”
输出: 7
解释: 断句后为"jess looked just like tim her brother",共7个未识别字符。来源:力扣(LeetCode)
-
题目分析
①.动态规划
定义 dp[i] 表示考虑截止到位置 i 时最少的未识别的字符数量。
为方便初始化,在字符串开头增加一个不可识别字符 “#”,则dp[0] = 1。
若存在一个位置 j 把前 i 个字符构成的子串 [0,i] 分为两部分,并且子串 [j,i] 是字典里的单词,如下图所示:
dp[i] 可以转换成 dp[j-1],遍历找到所有的 j ,然后dp[i] 取所有 j 位置的最小值即可,所以状态转移方程为dp[i] = min(dp[i],dp[j-1]);
若不存在一个位置 j,则 dp[i] = dp[i-1] + 1。
②.字典树
用 j 在范围 [0,i] 遍历所有子串 [j,i] 时,每次都从头到尾截取子串,存在大量的重复判断,可以使用字典树进行优化:
从 j = i 开始倒叙遍历,若 [j,i] 不是字典是中的前缀,则直接中断循环即可,若 [j,i] 是字典是中的前缀,再判断是否是字典中的单词。 -
代码示例
class TrieNode { private: int count;//包含子节点数量,可以用于判断是否叶子节点 bool isEnd;//标记是否单词结尾 vector<TrieNode*> children;//存储子节点指针 public: // 构造 TrieNode():count(0),isEnd(false),children(26,NULL) {} // 析构 ~TrieNode() { for(int i = 0;i < 26;i++ ) { if( children[i] ) delete children[i]; } } // 对外系列接口 int size() { return count ;} // 返回子节点数量 TrieNode* insertNode(char c) // 插入一个子节点,并返回其指针 { if( c < 'a' || c > 'z' ) return NULL; if( children[c - 'a'] == NULL) { children[c - 'a'] = new TrieNode(); count++; } return children[c - 'a'] ; } TrieNode* getNode( char c )//返回指定子节点 { if( c < 'a' || c > 'z' ) return NULL; return children[c - 'a'] ; } bool idWordEnd(){ return isEnd;}//返回是否单词结尾 void setEnd() { isEnd = true ;}//标记本节点为单词结尾 }; // Trie 类,封装操作接口 class Trie { private: TrieNode * root;//根节点 public: // 构造 Trie() : root( new TrieNode() ){} // 析构 ~Trie() { delete root; } // 插入一个单词 void insert(string word) { TrieNode * p = root; for(int i = 0;i < word.size();i++ ) { p = p->insertNode(word[i]); } p->setEnd() ; } //逆序插入一个单词 void insertReverse(string word) { TrieNode * p = root; for(int i = word.size() -1;i >-1;i-- ) { p = p->insertNode(word[i]); } p->setEnd() ; } //根据单词返回节点 TrieNode *getNode(string word) { TrieNode * p = root; for(int i = 0;i < word.size();i++ ) { p = p->getNode(word[i]) ; if( p == NULL ) return NULL; } return p; } // 判断指定单词是否存在 bool search(string word) { TrieNode * p = getNode(word); if( p ) return p->idWordEnd(); return false; } //判断指定前缀是否存在 bool startsWith(string prefix) { TrieNode * p = getNode(prefix); return p != NULL; } // }; class Solution { public: int respace(vector<string>& dictionary, string sentence) { Trie * trie = new Trie(); for(int i = 0;i < dictionary.size();i++ ) { string word = dictionary[i]; trie->insertReverse(word); } sentence = '#'+sentence; vector<int> dp(sentence.size(),0); dp[0] = 1; for( int i = 1;i < sentence.size();i++) { dp[i] = dp[i-1]+1; string temp = ""; for(int j = i;j > -1;j--) { temp += sentence[j] ; TrieNode * p = trie->getNode(temp); if( p ) //是后缀 { if( p->idWordEnd() ) dp[i] = min(dp[i],dp[j-1]); } else { break; } } } return dp[sentence.size()-1] -1 ; } };