前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >漫画:什么是“前缀树”?

漫画:什么是“前缀树”?

作者头像
小灰
发布2023-09-26 08:29:39
2130
发布2023-09-26 08:29:39
举报
文章被收录于专栏:程序员小灰程序员小灰

————— 第二天 —————

如上图所示,我们在百度输入框输入ap两个字母,下拉菜单就会自动列举出包含该前缀的所有单词,比如api、app、apple等等。

小灰的想法,是要建立一个很大的哈希表,哈希表中的key,是所有单词包含的前缀。

举个例子,有两个单词app和apple,它们的前缀包括a、ap、app、appl、apple,把这些前缀都作为key存储到哈希表中,每一个key对应的value,就是具有这个前缀的单词:

比如app和apple都具有前缀a,那么a所对应的value就是app,apple;再比如appl是apple的前缀,那么appl这个key对应的value就是apple。

————————————

我们利用apple,app,api,banana,bus这5个单词来举例子,构建出一颗前缀树,这颗前缀树就像下图这样:

在这棵树中,每一个节点最多可以拥有26个孩子节点,对应着从a到z这26个英文字母。

在我们给的例子中,apple,app,api这三个单词是字母a开头,banana,bus这两个单词是字母b开头,所以根节点拥有两个孩子,分别对应着以字母a开头的单词和以字母b开头的单词。

apple,app,api这三个单词,第二个字母都是p,所以a孩子节点只拥有一个孩子节点,对应着第二个字母是p的单词。

banana,bus这两个单词,第二个字母分别是a和u,所以b孩子节点拥有两个孩子节点,分别对应着第二个字母是a的单词(banana),以及第二个字母是u的单词(bus)。

以此类推,所有单词的所有字母,共同构成了这个前缀树的所有节点。

假如我们输入查询关键字“ap”,进行前缀查询,前缀树将会如何工作呢?

首先,前缀树会根据关键字中的第一个字母“a”,检查根节点是否有a对应的孩子节点,发现存在该孩子节点:

接下来,根据关键字中的第二个字母“p”,检查a孩子节点是否拥有对应字母p的孩子节点,发现存在该孩子节点:

这样一来,前缀树就判断出当前字典中存在以“ap”为前缀的单词。

假如我们输入查询关键字“bus”,进行精确查询,前缀树将会如何工作呢?

首先,前缀树会根据关键字中的第一个字母“b”,检查根节点是否有b对应的孩子节点,发现存在该孩子节点:

接下来,根据关键字中的第二个字母“u”,检查b孩子节点是否拥有对应字母u的孩子节点,发现存在该孩子节点:

左后,根据关键字中的第三个字母“s”,检查u孩子节点是否拥有对应字母s的孩子节点,发现存在该孩子节点,并且该节点的结束标志位为真

这样一来,前缀树就判断出当前字典中存在精确匹配“bus”的单词。

假如我们要插入一个新单词“buy”,前缀树如何工作呢?

首先,前缀树会根据关键字中的第一个字母“b”,检查根节点是否有b对应的孩子节点,发现存在该孩子节点:

接下来,根据关键字中的第二个字母“u”,检查b孩子节点是否拥有对应字母u的孩子节点,发现存在该孩子节点:

然后,根据关键字中的第三个字母“y”,检查u孩子节点是否拥有对应字母y的孩子节点,发现并没有这个孩子节点:

最后,创建字母y对应的新孩子节点。由于字母y是单词buy当中的最后一个字母,所以该节点的结束标志位置为“真”:

这样一来,前缀树成功插入了“buy”这个新单词。

代码语言:javascript
复制
class TrieNode {
    // 存储当前节点的子节点
    private TrieNode[] children;
    // 标记该节点是否是一个单词的结束
    private boolean isEndOfWord;

    public TrieNode() {
        // 初始化子节点数组,通常为26个字母
        children = new TrieNode[26];
        isEndOfWord = false;
    }

    // 添加一个单词到Trie树中
    public void insert(String word) {
        TrieNode current = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';
            if (current.children[index] == null) {
                current.children[index] = new TrieNode();
            }
            current = current.children[index];
        }
        current.isEndOfWord = true;
    }

    // 从Trie树删除一个单词
    public boolean delete(String word) {
        return delete(this, word, 0);
    }

    private boolean delete(TrieNode current, String word, int index) {
        if (index == word.length()) {
            if (!current.isEndOfWord) {
                return false; // 单词不存在于树中
            }
            current.isEndOfWord = false;
            return canDelete(current); // 返回是否可以删除该节点
        }

        char ch = word.charAt(index);
        int childIndex = ch - 'a';
        TrieNode child = current.children[childIndex];

        if (child == null) {
            return false; // 单词不存在于树中
        }

        boolean canDeleteChild = delete(child, word, index + 1);

        if (canDeleteChild) {
            current.children[childIndex] = null;
            return canDelete(current);
        }

        return false;
    }

    // 判断节点是否能被删除
    private boolean canDelete(TrieNode node) {
        if(node.isEndOfWord){
           return false;
        }
        for (TrieNode child : node.children) {
            if (child != null) {
                return false;
            }
        }
        return true;
    }

    // 检查一个单词是否存在于Trie树中
    public boolean search(String word) {
        TrieNode current = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';
            if (current.children[index] == null) {
                return false;
            }
            current = current.children[index];
        }
        return current.isEndOfWord;
    }

    // 检查是否有以给定前缀开头的单词存在
    public boolean startsWith(String prefix) {
        TrieNode current = this;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix.charAt(i);
            int index = ch - 'a';
            if (current.children[index] == null) {
                return false;
            }
            current = current.children[index];
        }
        return true;
    }
}

public class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // 插入一个单词到Trie树中
    public void insert(String word) {
        root.insert(word);
    }

    // 从Trie树删除一个单词
    public boolean delete(String word) {
        return root.delete(word);
    }

    // 检查一个单词是否存在于Trie树中
    public boolean search(String word) {
        return root.search(word);
    }

    // 检查是否有以给定前缀开头的单词存在
    public boolean startsWith(String prefix) {
        return root.startsWith(prefix);
    }

    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insert("apple");
        System.out.println(trie.search("apple"));   // 输出 true
        System.out.println(trie.search("app"));     // 输出 false
        System.out.println(trie.startsWith("app"));       // 输出 true
        trie.insert("app");
        System.out.println(trie.search("app"));     // 输出 true
        trie.delete("apple");
        System.out.println(trie.search("apple"));     // 输出 false
    }
}

在该代码中,TrieNode为前缀树的节点类,Trie类负责前缀树的整体操作。

至于前缀树的删除操作,比插入和查找操作要复杂一些,涉及到递归,大家可以阅读代码中的delete方法进行理解。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-09-25 09:15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员小灰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档