LeetCodeP208.实现Trie(前缀树)

前缀树定义

在这里插入图片描述
在这里插入图片描述

分析

先来看字典树树的大致结构

可以看到, 我们使用每条边表示一个字符, 每个结点使用一个唯一的序号标识

比如

  1. 1->2->5 表示的字符串是 aa
  2. 1->3->7 表示的字符串是 ba

通过图可以知道, 只要遵循上述约定, 假如有两个不相同的字符串有相同的前缀, 则它们可以共用一些结点, 但是这两个字符串的最后一个字符所在边指向的结点序号必定不同, 这个结论对n个不同字符串同样成立

题目要求

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false
  • 1 <= word.length, prefix.length <= 2000
  • wordprefix 仅由小写英文字母组成
  • insertsearchstartsWith 调用次数 总计 不超过 30000 次

题目分析

由于题目所要求的是建立小写英文字母的字典树, 故单个结点至多可以引出 26 条边

我们在字典树上插入字符串时, 比如插入字符串 bab, 但是字典树上只存在 ba 子串, 故需要在 6 号结点上新增一个指向 b 的边

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UL6IXUHY-1621752381574)(C:\Users\悠一木碧\AppData\Roaming\Typora\typora-user-images\image-20210523142212644.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u9Ft4Ra8-1621752381576)(C:\Users\悠一木碧\AppData\Roaming\Typora\typora-user-images\image-20210523142346138.png)]

故至此, 插入 bab 字符串结束

插入

为了表示在 6 号结点建立了 b 字符的边, 我们可以使用一个二维数组 nodesPath, 其中 nodesPath[i] 表示 i 号结点

其中 26 表示单个结点最多可以有26条边, 具体问题具体分析, 因为这个问题要求的是小写英文字母

int maxNodes = 100000;
int[][] nodesPath = new int[maxNodes][26];

我们可以以 nodesPath[0] 表示字典树的根节点, 根节点不表示字符, 所有指向根节点的边, 表示没有对应字符的边

// 表示根节点没有到 a 字母结点的边
nodesPath[0][0] = 0;
// 表示根节点到 b 字母结点的边指向的是索引为 1 的结点
nodesPath[0][1] = 1

故我们可以用字段来维护结点数, 这样增加一个结点的时候, 对结点数++, 同时结点获得了一个唯一的索引序号

遍历一个字符串 word 的所有字符, 当对应的边指向根节点时, 说明要增加边了

int currentNodeIndex = 0;
for (int i = 0; i < word.length(); i++) {
     int c = word.charAt(i) - 'a';
     if (nodesPath[currentNodeIndex][c] == 0) {
         ++nodeCount;
         nodesPath[currentNodeIndex][c] = nodeCount;
     }
     currentNodeIndex = nodesPath[currentNodeIndex][c];
}

查找

当我们在字典树上新增了我们所需的全部字符后, 还需要做一个操作, 用于标记当前字符串被插入了

还记得上面我们说的: 假如有两个不相同的字符串有相同的前缀, 则它们可以共用一些结点, 但是这两个字符串的最后一个字符所在边指向的结点序号必定不同 结论吗?

思考下面一个问题

insert("abcd");
search("abc") = false ? 

插入了 abcd 后, 字典树上必定存在 abc, 但是要求返回 search(“abc”) = false, startsWith(“abc”) = true;

故可以用一个一维数组 exists

int maxNodes = 100000;
int[] exists = new int[maxNodes];

当插入一个字符串后, 设置 exists[字符串最后一个字符的唯一标识] = true

这样, 即使找到了所有字符都在字典树上, 只要 exists[index] = false, 就说明这个字符串没有插入过, 也就是 search() = false;

而 startsWith()方法只需要字符存在即可, 返回 true

实现代码

    class Trie {
        /*
         每个结点到下一个结点的路径
         */
        private final int[][] nodesPath;

        /*
        标记每个单词是否插入过字典树
         */
        private final boolean[] exist;

        /*
        结点数量
         */
        private int nodeCount;
        
        /*
        当查询不到所需字符时, 返回的索引值
         */
        private final int INDEX_WHEN_CHAR_NOT_EXISTS = -1;

        /**
         * Initialize your data structure here.
         */
        public Trie() {
            nodeCount = 0;
            nodesPath = new int[100000][26];
            exist = new boolean[100000];
        }

        public void insert(String word) {
            int currentNodeIndex = 0;
            for (int i = 0; i < word.length(); i++) {
                int targetChar = word.charAt(i) - 'a';
                if (nodesPath[currentNodeIndex][targetChar] == 0) {
                    ++nodeCount;
                    nodesPath[currentNodeIndex][targetChar] = nodeCount;
                }
                currentNodeIndex = nodesPath[currentNodeIndex][targetChar];
            }
            exist[currentNodeIndex] = true;
        }

        public boolean search(String word) {
            int index = getIndexOfLastChar(word);
            /*
            尽管每个字符都在字典树上出现过, 但是可能是其他字符串的子串, 故为了区分, 采用返回 exist数组中的值
             */
            return INDEX_WHEN_CHAR_NOT_EXISTS != index && exist[index];
        }

        /**
         * 查找对应的子串是否出现在字典树中, 只需要每个字符依次出现在字典树中即可
         */
        public boolean startsWith(String prefix) {
            int index = getIndexOfLastChar(prefix);
            return INDEX_WHEN_CHAR_NOT_EXISTS != index;
        }

        private int getIndexOfLastChar(String word) {
            int currentNodeIndex = 0;
            for (int i = 0; i < word.length(); i++) {
                int c = word.charAt(i) - 'a';
                if (nodesPath[currentNodeIndex][c] == 0) {
                    return INDEX_WHEN_CHAR_NOT_EXISTS;
                }
                currentNodeIndex = nodesPath[currentNodeIndex][c];
            }
            return currentNodeIndex;
        }
    }

运行结果

在这里插入图片描述

总结

这份代码虽能实现问题所需, 但是效率有待提高

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是使用 Kotlin 实现 Trie 的代码: ```kotlin class Trie() { private val root = TrieNode() fun insert(word: String) { var cur = root for (c in word) { if (!cur.children.containsKey(c)) { cur.children[c] = TrieNode() } cur = cur.children[c]!! } cur.isEnd = true } fun search(word: String): Boolean { var cur = root for (c in word) { if (!cur.children.containsKey(c)) { return false } cur = cur.children[c]!! } return cur.isEnd } fun startsWith(prefix: String): Boolean { var cur = root for (c in prefix) { if (!cur.children.containsKey(c)) { return false } cur = cur.children[c]!! } return true } } class TrieNode { val children = mutableMapOf<Char, TrieNode>() var isEnd = false } ``` 在上面的代码中,我们定义了 `Trie` 类和 `TrieNode` 类,其中 `TrieNode` 表示前缀树的节点,包含一个表示是否为单词结尾的布尔值和一个 `children` 字典,用于存储子节点。`Trie` 类中包含 `insert`、`search` 和 `startsWith` 三个方法,分别用于插入、查找和判断是否存在前缀。在 `insert` 方法中,我们遍历字符串的每个字符,如果当前节点的子节点中没有该字符,则新建一个 `TrieNode` 并加入子节点中。在遍历结束后,将最后一个节点标记为单词结尾。在 `search` 和 `startsWith` 方法中,我们同样遍历字符串的每个字符,如果当前节点的子节点中没有该字符,则直接返回 false。如果遍历结束后,当前节点是单词结尾,则说明该单词存在,返回 true;否则说明该单词不存在,返回 false。在 `startsWith` 方法中,我们不需要判断当前节点是否为单词结尾,只需判断该前缀是否存在即可。 希望能对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值