【数据结构】Trie(字典树 前缀树)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_42322103/article/details/97235357

文章目录

概述

trie是一种多叉树,它专门为字符串设计的。

如果对于一个英语字典来说,有n个条目。以查询这个字典中的某个条目来看,我使用映射结构(底层是树结构),查询的时间复杂度是O(logn)
使用tire的话,查询每个条目的时间复杂度,和字典中一共有多少条目无关!时间复杂度为O(w)w为查询单词的长度。这个是非常有优势。

tire的结构类似于这样
在这里插入图片描述
trie没有将字符串作为一个整体,而是将其拆开,每遍历到一个叶子节点就形成了一个英语单词,每个节点最多有26个指向下个节点的指针。

如果考虑到具体的业务场景,比如说网址,还需要有特殊的字符,这个每个节点最多指向下个节点的指针的数量可能是大于26的。所以为了tire的灵活性,一般不会限定tire有每个节点多少个指向下个节点的指针。

当然,有的单词还没到叶子节点就结束了,类似于这种 所以要有一个表示isWord
在这里插入图片描述

实现

import java.util.TreeMap;

//默认trie的节点为字符 进行演示
public class Trie {

    //trie节点的类设计
    private class Node{

        public boolean isWord;
        public TreeMap<Character, Node> next;  //映射

        public Node(boolean isWord){
            this.isWord = isWord;
            next = new TreeMap<>();
        }

        public Node(){
            this(false);
        }
    }

    private Node root;  //根节点
    private int size;  //字符数量

    public Trie(){
        root = new Node();
        size = 0;
    }

    // 获得Trie中存储的单词数量
    public int getSize(){
        return size;
    }

    // 向Trie中添加一个新的单词word
    public void add(String word){

        Node cur = root;
        for(int i = 0 ; i < word.length() ; i ++){
            char c = word.charAt(i);
            if(cur.next.get(c) == null)
                cur.next.put(c, new Node());
            cur = cur.next.get(c);
        }
        //遍历到最后一个字符
        if(!cur.isWord){
            cur.isWord = true;
            size ++;
        }
    }

    // 查询单词word是否在Trie中
    //与添加的逻辑 基本上一致
    public boolean contains(String word){

        Node cur = root;
        for(int i = 0 ; i < word.length() ; i ++){
            char c = word.charAt(i);
            if(cur.next.get(c) == null)
                return false;
            cur = cur.next.get(c);
        }
        return cur.isWord;
    }

    // 查询是否在Trie中有单词以prefix为前缀
    public boolean isPrefix(String prefix){

        Node cur = root;
        for(int i = 0 ; i < prefix.length() ; i ++){
            char c = prefix.charAt(i);
            if(cur.next.get(c) == null)
                return false;
            cur = cur.next.get(c);
        }

        return true;
    }


    //删除操作
    // 1. 如果单词是另一个单词的前缀,只需要把该word的最后一个节点的isWord的改成false
    // 2. 如果单词的所有字母的都没有多个分支,删除整个单词
    // 3,如果单词的除了最后一个字母,其他的字母有多个分支,
    public boolean remove(String word) {
        Node multiChildNode = null;
        int multiChildNodeIndex = -1;
        Node current = root;
        for (int i = 0; i < word.length(); i++) {
            Node child = current.next.get(word.charAt(i));
            //如果Trie中没有这个单词
            if (child == null) {
                return false;
            }
            //当前节点的子节点大于1个
            if (child.next.size() > 1) {
                multiChildNodeIndex = i;
                multiChildNode = child;
            }
            current = child;
        }

        //如果单词后面还有子节点
        if (current.next.size() > 0) {
            if (current.isWord) {
                current.isWord = false;
                size--;
                return true;
            }
            //不存在该单词,该单词只是前缀
            return false;
        }

        //如果单词的所有字母的都没有多个分支,删除整个单词
        if (multiChildNodeIndex == -1) {
            root.next.remove(word.charAt(0));
            size--;
            return true;
        }

        //如果单词的除了最后一个字母,其他的字母有分支
        if (multiChildNodeIndex != word.length() - 1) {
            multiChildNode.next.remove(word.charAt(multiChildNodeIndex + 1));
            size--;
            return true;
        }
        return false;
    }
}

上面实现的Trie中,使用TreeMap来保存节点的所有的子节点,也可以使用HashMap来保存所有的子节点,效率更高:

public Node() {
    next = new HashMap<>();
}

当然 我们也能用一个定长的数组来存储所有的子节点,效率比HashMap更高,因为不需要使用hash函数:

public Node(boolean isWord){
    this.isWord = isWord;
    next = new Node[26];//只能存储26个小写字母
}

trie查询效率非常高,但是对空间的消耗还是挺大的,这也是典型的空间换时间。

可以使用 压缩字典树(Compressed Trie) ,但是维护相对来说复杂一些。

前面也说了,如果我们不止存储英文单词,还有其他特殊字符,那么维护子节点的集合可能会更多。可以对Trie字典树做些限制,比如每个节点只能有3个子节点,左边的节点是小于父节点的,中间的节点是等于父节点的,右边的子节点是大于父节点的,这就是三分搜索Trie字典树(Ternary Search Trie)。

还有一种字符串模式识别的结构叫做后缀树。还有子串查询的相关问题,比如说KMP, Boyer-Moore,Rabin-Karp等。还有文件压缩相关和模式匹配的问题。更宏观的来看,我们写的程序代码也都是字符串,解析成可运行的程序,用到的是编译原理

字符串是非常非常重要的,学习相关的数据结构与算法就是修炼内功。

猜你喜欢

转载自blog.csdn.net/qq_42322103/article/details/97235357
今日推荐