Trie字典树

一、Tire树概念

字典树应用场景:单词检索、统计和排序字符串,字符串前缀搜索等

基本性质:

  1. 根节点不包含字符,除根节点外的每一个节点都只包含一个字符
  2. 从根节点到某一结点,路径上经过的字符连起来,为该节点对应的字符串
  3. 每个节点的所有子节点(只针对孩子节点,不包括孙子)包含的字符都不相同

数据结构核心:利用字符串的公共前缀减少查询时间,最大限度减少无意义的字符比较。是典型的空间换时间,若处理的数据中有很多公共的前缀,则效率较高;若没有很多公共前缀,则内存占用量较大

例如我们用字典树存储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举例

  1. 如果孩子节点中有h字符的节点,则移动cur指向当前节点,遍历下一个字符;
  2. 若没有,则创建节点存储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;
}

六、删除字符串的接口

在这里插入图片描述

我们在删除某个单词的时候,有以下三种典型场景:

  1. 删除po:此时我们也不能删除po这两个节点,这会影响到pool这个单词,我们应该将节点o的freq置为0
  2. 删除pool:同理我们不能删除pool四个节点,删除ol两个而节点即可
  3. 删除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);
        }
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bugcoder-9905

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值