一、Tire树概念
字典树应用场景:单词检索、统计和排序字符串,字符串前缀搜索等
基本性质:
- 根节点不包含字符,除根节点外的每一个节点都只包含一个字符
- 从根节点到某一结点,路径上经过的字符连起来,为该节点对应的字符串
- 每个节点的所有子节点(只针对孩子节点,不包括孙子)包含的字符都不相同
数据结构核心:利用字符串的公共前缀减少查询时间,最大限度减少无意义的字符比较。是典型的空间换时间,若处理的数据中有很多公共的前缀,则效率较高;若没有很多公共前缀,则内存占用量较大
例如我们用字典树存储pool、prize、preview、prepare、produce、progress这些词
构造节点数据结构:
struct TireNode {
TireNode(char ch, int freq)
:ch(ch)
, freq(freq)
{}
char ch; // 当前节点存储的字符
int freq; // 当前单词有几个
map<char, TireNode*> children; // 当前节点所有的子节点以及子节点对应的字符
};
用map,而不用数组的原因:
- 用户可以从map快速(O(log2N))得知map是否有某个字符,而不用遍历整个数组才能知道是否存在当前需要搜索的字符
- map底层是红黑树,存字符是有序的
二、add接口
拿插入hello举例
- 如果孩子节点中有h字符的节点,则移动cur指向当前节点,遍历下一个字符;
- 若没有,则创建节点存储h字符,cur指向当前节点,遍历下一个字符
void add(const string& word) {
TireNode* cur = root_;
for (char c : word) {
if (cur->child_map[c] == nullptr) {
// 当前字符的节点没有创建
TireNode* node = new TireNode(c, 0);
cur->child_map[c] = node;
cur = node;
}
else {
// 当前字符已存在
cur = cur->child_map[c];
}
}
// cur指向word单词最后一个节点
cur->freq++;
}
三、query接口
int query(const string& word) const{
TireNode* cur = root_;
for (char c : word) {
if (cur->child_map[c] == nullptr) {
return 0;
}
else {
cur = cur->child_map[c];
}
}
return cur->freq;
}
四、前序遍历接口
将字符串按照字典序排序,我们直接用字典树存储,进行前序遍历即可。因为对于每个节点的孩子节点,我们是用map存储的,所以先序遍历是按字典序输出的
void pre_order() const{
string str;
vector<string> word_list;
pre_order(root_, str, word_list);
for (string word : word_list) {
cout << word << endl;
}
cout << endl;
}
// 字典顺序输出接口
void pre_order(TireNode* node, string str, vector<string>& word_list) const{
// str记录当前节点以前的字符串
if (node != root_) {
str.push_back(node->ch);
if (node->freq > 0) {
word_list.push_back(str);
}
}
// 递归处理孩子节点
for (auto pair : node->child_map) {
pre_order(pair.second, str, word_list);
}
}
// 逆字典顺序输出接口
void pre_order(TireNode* node, string str, vector<string>& word_list) const{
// 递归处理孩子节点
for (auto pair : node->child_map) {
pre_order(pair.second, str + node->ch, word_list);
}
// str记录当前节点以前的字符串
if (node != root_) {
str.push_back(node->ch);
if (node->freq > 0) {
word_list.push_back(str);
}
}
}
五、前缀查询接口
vector<string> query_prefix(const string& prefix) const{
vector<string> word_list;
TireNode* cur = root_;
for (char c : prefix) {
if (cur->child_map[c] == nullptr) {
// 不存在此前缀
return word_list;
}
else {
cur = cur->child_map[c];
}
}
// 由于前序遍历接口中的str存储的是当前节点以前的字符串,不包括当前字符
// cur指向前缀最后一个字符节点,故传入的str不包括前缀最后一个字符
pre_order(cur, prefix.substr(0, prefix.size()-1), word_list);
return word_list;
}
六、删除字符串的接口
我们在删除某个单词的时候,有以下三种典型场景:
- 删除po:此时我们也不能删除po这两个节点,这会影响到pool这个单词,我们应该将节点o的freq置为0
- 删除pool:同理我们不能删除pool四个节点,删除ol两个而节点即可
- 删除prepare:此时我们不能删除prepare七个节点,这会影响到preview,我们应该做的是删除pare四个节点
void remove(const string& str) {
TireNode* cur = root_;
TireNode* del_start = root_;
char del_ch = str[0];
for (char c : str) {
if (cur->child_map[c] == nullptr) {
// 不存在此字符串,什么都不做
return ;
}
else {
if (cur->freq > 0 || cur->child_map.size() > 1) {
// 出现中途存储了单词或字典树分叉
del_ch = c; // 用于节点的map删除的元素
del_start = cur; // 从del_strat开始删除
}
cur = cur->child_map[c];
}
}
// 此时cur指向str最后一个字符的节点
if (!cur->child_map.empty()) {
// 后面还有字符,则直接置0
cur->freq = 0;
}
else {
// 后面没有字符了
// 删除prepare或pool,从del_start往后开始删除
TireNode* del = del_start->child_map[del_ch];
del_start->child_map.erase(del_ch);
queue <TireNode*> q;
q.push(del);
while (!q.empty()) {
TireNode* del = q.front();
q.pop();
for (auto& pair : del->child_map) {
q.push(pair.second);
}
delete del;
}
}
}
七、TireTree完整代码
class TireTree {
public:
TireTree() {
root_ = new TireNode('\0', 0); // 根节点不存字符
}
~TireTree() {
queue<TireNode*> q;
q.push(root_);
while (!q.empty()) {
TireNode* node = q.front();
q.pop();
for (auto& pair : node->child_map) {
q.push(pair.second);
}
delete node;
}
cout << endl;
}
// 添加单词
void add(const string& word) {
TireNode* cur = root_;
for (char c : word) {
if (cur->child_map[c] == nullptr) {
// 当前字符的节点没有创建
TireNode* node = new TireNode(c, 0);
cur->child_map[c] = node;
cur = node;
}
else {
// 当前字符已存在
cur = cur->child_map[c];
}
}
// cur指向word单词最后一个节点
cur->freq++;
}
int query(const string& word) const{
TireNode* cur = root_;
for (char c : word) {
if (cur->child_map[c] == nullptr) {
return 0;
}
else {
cur = cur->child_map[c];
}
}
return cur->freq;
}
void pre_order() const{
string str;
vector<string> word_list;
pre_order(root_, str, word_list);
for (string word : word_list) {
cout << word << endl;
}
cout << endl;
}
vector<string> query_prefix(const string& prefix) const{
vector<string> word_list;
TireNode* cur = root_;
for (char c : prefix) {
if (cur->child_map[c] == nullptr) {
// 不存在此前缀
return word_list;
}
else {
cur = cur->child_map[c];
}
}
pre_order(cur, prefix.substr(0, prefix.size()-1), word_list);
return word_list;
}
void remove(const string& str) {
TireNode* cur = root_;
TireNode* del_start = root_;
char del_ch = str[0];
for (char c : str) {
if (cur->child_map[c] == nullptr) {
// 不存在此字符串,什么都不做
return ;
}
else {
if (cur->freq > 0 || cur->child_map.size() > 1) {
// 出现中途存储了单词或字典树分叉
del_ch = c; // 用于节点的map删除的元素
del_start = cur; // 从del_strat开始删除
}
cur = cur->child_map[c];
}
}
// 此时cur指向str最后一个字符的节点
if (!cur->child_map.empty()) {
// 后面还有字符,则直接置0
cur->freq = 0;
}
else {
// 后面没有字符了
// 删除prepare或pool,从del_start往后开始删除
TireNode* del = del_start->child_map[del_ch];
del_start->child_map.erase(del_ch);
queue <TireNode*> q;
q.push(del);
while (!q.empty()) {
TireNode* del = q.front();
q.pop();
for (auto& pair : del->child_map) {
q.push(pair.second);
}
delete del;
}
}
}
private:
struct TireNode {
TireNode(char ch, int freq)
:ch(ch)
, freq(freq)
{}
char ch;
int freq;
map<char, TireNode*> child_map;
};
TireNode* root_;
private:
void pre_order(TireNode* node, string str, vector<string>& word_list) const{
// str记录当前节点以前的字符串(不包括当前节点)
if (node != root_) {
str.push_back(node->ch);
if (node->freq > 0) {
word_list.push_back(str);
}
}
// 递归处理孩子节点
for (auto pair : node->child_map) {
pre_order(pair.second, str, word_list);
}
}
};