LeetCode 208.实现Trie(前缀树) 题解

题目信息

LeetoCode地址: . - 力扣(LeetCode)

题目理解

题目已经清晰的告诉了我们要实现Trie,以及它的优点,那么这些优点解决了什么问题,为什么传统的方法不行?

现在让我们还原一下问题: 保存一些字符串,并判定新给出的字符串是否是这些字符串中的一员,或者是其中某一员的前缀。

举个例子,保存"app", "apple", "application"这三个字符串,并判断"app", "append"是否是这些字符串组成的集合里的元素,判断"app", "appl" "appc" 是否是这些字符串组成集合的某个元素的前缀。

最直观的思路,是将这三个字符串都保存到HashMap elementMap里,从而达到最好情况下常数级别时间复杂度的查询;

elementMap = {"app", "apple", "application"}

对于前缀,我们可以将所有元素的所有前缀都保存到另一个HashMap prefixMap里,从而也能达到最好情况下常数级别时间复杂度的查询;

prefixMap = {"a", "ap", "app", "appl", "apple", "appli", "applic", "applica", "applicat", "applicati", "applicatio", "application"};

看起来它能够正常的工作,问题解决了,对嘛?对,也不完全对。

首先,这两个HashMap占据了巨大的存储空间,每增加一个长度为n的字符串,我们都需要额外存储"1+2+...+n"=1/2*n^2 + 1/2*n个字符, 这个增长速度是很恐怖的,而且存储内容上有非常多的相似度。

其次,这种解决思路是单向的,即只能知道集合是否包含某个前缀,却不能知道某个前缀的字符串,譬如查询以"ap"开头的所有字符串。

要解决这些问题,我们必须高效利用每个字符,让其携带尽可能多的信息,比如是否有字符以该字符结尾,是否有其他字符在该字符后出现?

譬如第一个字符串"app", 有三个字符"a", "p", "p",易知对于第一个"a"来说,后续出现了"p", 而对于第一个"p"来说,后续出现了"p";而对于第二个"p",它是结尾字符。如果使用链表将这个字符串表示出来,并将结尾字符通过红色区分出来,则如下图所示:

类似的,"apple" 和"application"的图示如下:

可以清楚的看到,这三个链表的前三个节点是重复的,都是a, p, p, 后两个链表的前4的节点都是重复的,都是a,p,p,l。而l之后出现了分叉,在"apple"中,l指向了e,在"application"中,l指向了i,如果我们将三个链表合并,并允许节点可以指向多个其他节点的话,图示就如下所示:

注意图中有三个节点是红色的,意味着有字符串是以该节点结尾的。

现在,链表里的元素的都是a开头的,如果我们此时插入了一个字符串是"book",又会怎样呢?

需要引入一个代表根的root节点,该节点表明了当前我们集合里有以哪些首字符开端的字符串。加入"book"之后的链表如下图所示:

使用这个链表结构,我们就查询任意字符串是否存在于集合之中,以及前缀是否包含与集合之中。

比如我想查询"china", 首字符是"c",而我们发现,从root节点的下一个节点之中,只有"a","b", 所以我们可以断言,"china"不在我们的集合里。

相反的,如果我们想查询"apple",则可以从root一路走到红色e节点,即表明链表中存在"apple"字符串。

这个链表结构就是Trie前缀树。

递归写法Trie

最直观的想法是,每个节点就是一个Trie对象,该对象代表的是某一个字符,它有一个成员变量,保存了所有的下一个节点,由于下一个字符只可能是'a'到'z'这些字符,可以直接通过一个HashMap保存起来,在search的时候,从首字符开始,通过root节点开始不断的从hashMap中查询下一个字符对应的Trie对象,如果不是最后一个字符,则递归调用search方法,直到匹配最后一个字符。

插入的时间复杂度 O(n), n 为字符串长度

搜索的时间复杂度O(n), n 为字符串长度

额外空间复杂度: O(n * m), n为平均字符串长度,m为字符串个数。

class Trie {

    HashMap<String, Trie> charMap;
    Boolean finished;
    public Trie() {
        charMap = new HashMap<>();
        finished = false;
    }

    public void insert(String word) {
        if (word.length() == 1) {
            Trie orDefault = charMap.getOrDefault(word, new Trie());
            orDefault.finished = true;
            charMap.putIfAbsent(word, orDefault);
            return;
        }
        String firstChar = word.substring(0, 1);
        Trie orDefault = charMap.getOrDefault(firstChar, new Trie());
        charMap.putIfAbsent(firstChar, orDefault);
        orDefault.insert(word.substring(1));
    }

    public boolean search(String word) {
        if (word.length() == 1) {
            return charMap.get(word) != null && charMap.get(word).finished;
        }
        String firstChar = word.substring(0, 1);
        if (!charMap.containsKey(firstChar)) {
            return false;
        }
        return charMap.get(firstChar).search(word.substring(1));
    }

    public boolean startsWith(String prefix) {
        if (prefix.length() == 1) {
            return charMap.get(prefix) != null && charMap.get(prefix).charMap.size() > 0;
        }
        String firstChar = prefix.substring(0, 1);
        if (!charMap.containsKey(firstChar)) {
            return false;
        }
        return charMap.get(firstChar).startsWith(prefix.substring(1));
    }
}

数组循环Trie写法

由于可能的每一个字符只有a到z这26个,所以完全可以将HashMap替换成Array,而且在search时也无需递归,可以通过for循环进行字符的遍历。

插入的时间复杂度 O(n), n 为字符串长度

搜索的时间复杂度O(n), n 为字符串长度

额外空间复杂度: O(n * m), n为平均字符串长度,m为字符串个数。

相比于递归实现,少了方法栈帧的开销。

class Trie {
    class TreeNode {
        TreeNode[] nextNodeArray;
        Boolean finished;

        public TreeNode() {
            this.nextNodeArray = new TreeNode[26];
            this.finished = false;
        }
    }

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

    public void insert(String word) {
        TreeNode current = root;
        for (int i = 0; i<word.length(); i++) {
            char currentChar = word.charAt(i);
            TreeNode nextNode = current.nextNodeArray[currentChar - 'a'];
            if (nextNode == null) {
                nextNode = new TreeNode();
                current.nextNodeArray[currentChar - 'a'] = nextNode;
            }
            current = nextNode;
        }
        current.finished = true;
    }

    public boolean search(String word) {
        TreeNode current = root;
        for (int i = 0; i< word.length(); i++) {
            char currentChar = word.charAt(i);
            TreeNode nextNode = current.nextNodeArray[currentChar - 'a'];
            if (nextNode == null) {
                return false;
            }
            current = nextNode;
        }
        return current.finished;
    }

    public boolean startsWith(String prefix) {
        TreeNode current = root;
        for (int i = 0; i< prefix.length(); i++) {
            char currentChar = prefix.charAt(i);
            TreeNode nextNode = current.nextNodeArray[currentChar - 'a'];
            if (nextNode == null) {
                return false;
            }
            current = nextNode;
        }
        return true;
    }

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值