算法数据结构-Trie树

数据结构与算法:前缀树(字典树、Trie树) 前缀树 阅读详情

  Trie树,又称为前缀树(因为某节点的后代存在共同的前缀,比如pan是panda的前缀),字典树,顾名思义它本事也是属于树这个数据结构体系中的一员,当然它也有很多变种,如后缀树,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree等等。我们通常使用Trie来处理字符串匹配的,使用它可以解决在一组字符串集合中快速查找某个字符串的问题。它为何能这么高效?它的查询的时间复杂度是多少?缺点是什么?

我们结合一个项目中的需求来讲解描述下Trie树的作用,并且结合例子来自我感受下它的效率。

需求:抖音视频平台,虎牙斗鱼在线直播平台,起点纵横小说平台。这些平台都会有用户评论的功能(抖音中的评论,虎牙和斗鱼的在线弹幕,起点纵横的帖子评论)。既然存在用户评论的概念,那么对于用户评论的语句就要进行检测,对于一些敏感词汇就要进行处理(有将敏感词汇变更为*显示,也有提示用户需要修改评论,否则不能进行发表)。不论做何种后续的操作,那么如何能快速检测这些敏感词是关键。例如当用户存在这么一句评论:“i want fuck you”,那么其中包含了敏感词汇“fuck”。那么我们如何能快速的呢。一般我们收集这些敏感词汇组成一个字典,然后将语句和敏感词典进行匹配发现敏感词汇。

我们就使用上述的Trie树来进行敏感词典(假设当前的敏感词典为fuck,bitch,bullshit,suck)的构建,这个词语在Trie树中是这么体现的呢,如下图所示:

从上图可以看出Trie树的一些特点:

  1. 根节点不包含任何字符,除了根节点任何一个节点只会包含一个字符
  2. 从根节点开始到某一节点中间所有经过的节点对应的字符连接起来就是该节点对应的字符串
  3. 一般使用不同颜色来区分节点类型,例如上述以黄色节点对应字符串是存储类型字符串用于后续对比,而非黄色节点对应为非存储类型字符串的或只是中间字符串
  4. 每个节点下的子节对应的字符都不相同

1.1:Trie树的插入非常的简单,将写入的单词的每个字符依次的写入到Trie树中,单词的每个字符都可以看作是树中的每一层。每个字母插入前先看字母对应的节点是否存在,存在则共享该节点,不存在则创建对应的节点。例如若我们将bitch这个单词写入到Tire树中(假设词典中已存在fuck,bullshit,suck)。第一步将bitch的首字母b插入到根节点后面,因为b节点已经存在,所以公用此节点;第二步写入i字母,在查看b节点的子发现只有u节点无i节点,所以创建i节点,链接到b节点之后;第三步重复第二步直到单词的尾节点,并标明此节点颜色为黄色(实际上这个颜色只是标志位的一种体现,我也可以使用其它颜色或者其它表现形式,标志位为了避免前缀词对词典的影响,例如pan是panda的前缀,那么若panda在Trie中存在,那么对应pan肯定也是在Trie中存在,而我们期盼的是panda属于词典而pan不属于,这个时候就是该标志位的作用时候了)。

1.2:那么Trie树的查询效率怎么样呢,Trie树的查询是和被检索的字符串的长度有关,例如我需要查询bullshit这个单词是否在词典中存在就会按照根路径向下检索匹配,直到找到尾节点为黄色并且包含的字母为t的时候,就代表bullshit在字典中是存在的,那么对应的时间复杂度为O(n)

1.3:Trie树的节点的删除较为复杂,通常分为2种情况:

  1. 若删除的字符串在Tire的存在是前缀词的存在,这种删除比较简单,只需要找到尾部节点将其节点的标志位转变下即可
  2. 若删除的字符串在Tire的存在是非前缀词的存在,这种删除较为复杂,需要从根节点开始向下遍历(按照字符串的路径向下遍历)删除,若当前节点下无其它节点(不在路径类的)是,可将该节点对应的分支全部移除,若存在则该节点属于公共节点不可被移除,直到尾部节点被移除。

2:Java代码对于Trie树基本实现:

2.1:基础代码实现


import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;

/**
 * java实现Trie树数据结构
 * 此树为不安全
 * @author fangyuan
 */
public class TrieTree implements Serializable, Iterable<String> {

    private static final long serialVersionUID = 8386482531322992189L;

    /**
     * 当前Trie树中包含元素个数
     */
    private int size;

    /**
     * 当前Trie树中包含节点数量
     */
    private long length;

    /**
     * 定义根节点
     */
    private TrieTreeNode root;

    /**
     * 用于存储当前树中元素
     */
    private List<String> eles;

    //定义节点类型
    /**
     * 标示该节点字符串应该在trie树中
     */
    static final int yellow = 1;

    /**
     * 普通节点
     */
    static final int white = 0;

    /**
     * 每一层树默认分支数
     */
    static final int minTreeLevel = 2;

    /**
     * 构建函数 初始化trie树
     */
    public TrieTree(){
        this.size=0;
        this.length=1;
        this.root = new TrieTreeNode('/',white);
        this.eles = new ArrayList<>(8);
    }



    /**
     * 定义Trie节点
     */
    class TrieTreeNode {

        //当前节点对应的字母
        private char ele;

        //定义该节点类型
        private int type;

        //当前节点所包含的字节点 后续使用其它结构对其进行优化
        private List<TrieTreeNode> nexts;

        public  TrieTreeNode(char ele,int type){
            this.ele = ele;
            this.type=type;
            this.nexts = new ArrayList<>(minTreeLevel);
        }

    }

    /**
     * 新增一个元素
     * @param str 被写入的元素字符串
     */
    public void addNewEle(String str){

        char ele;
        TrieTreeNode current = this.root;
        List<TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toCharArray();
        //将chars以节点的形式写入到Trie树中
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];
            //若词中包含空格直接去除
            if(ele ==' ') {
                continue;
            }
            //当前节点type
           int nodeType = i==chars.length-1?yellow:white;

            if(trieTreeNodes.size() == 0){
                //直接新建一个新节点
                TrieTreeNode trieTreeNode =  new TrieTreeNode(ele,nodeType);
                trieTreeNodes.add(trieTreeNode);
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                this.length++;
            }else{
                boolean flag = true;
                //遍历此层节点是否已包含了该字母
                for (int j=0;j<trieTreeNodes.size();j++) {
                    TrieTreeNode trieTreeNode  = trieTreeNodes.get(j);
                    if(trieTreeNode.ele == ele){
                        //说明当前节点已经存在 处理下前缀词
                        trieTreeNode.type =nodeType;
                        current = trieTreeNode;
                        trieTreeNodes = trieTreeNode.nexts;
                        flag = false;
                        break;
                    }
                }

                //若当前节点不包含则重新创建
                if(flag){
                    TrieTreeNode trieTreeNode =  new TrieTreeNode(ele,nodeType);
                    trieTreeNodes.add(trieTreeNode);
                    current = trieTreeNode;
                    trieTreeNodes = trieTreeNode.nexts;
                    this.length++;
                }
            }
        }
        //创建完成
        this.size++;

        eles.add(str);
    }

    /**
     * 删除一个元素
     * @param str 被删除的元素字符串
     */
    public void remove(String str){

        char ele;
        TrieTreeNode current = this.root;
        List<TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toCharArray();

        boolean flag = true;

        LinkedHashMap<List<TrieTreeNode>,TrieTreeNode> delQueue = new LinkedHashMap<>(chars.length);

        //遍历trie树
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];

            //若词中包含空格直接去除
            if (ele == ' ') {
                continue;
            }

            //遍历此层节点是否已包含了该字母
            for (int j=0;j<trieTreeNodes.size();j++) {
                TrieTreeNode trieTreeNode  = trieTreeNodes.get(j);
                if(trieTreeNode.ele == ele){
                    //若当前字符为该单词的最后一个字母 若该单词为一个前缀词只需要变更节点属性即可
                    if(i==chars.length-1 && trieTreeNode.type == yellow && trieTreeNode.nexts.size()>0){
                        //变更节点类型
                        trieTreeNode.type=white;
                        this.size--;
                        this.eles.remove(str);
                        return;
                    }
                    //记录当前节点到队列中 为后续删除做准备
                    delQueue.put(trieTreeNodes,trieTreeNode);
                    current = trieTreeNode;
                    trieTreeNodes = trieTreeNode.nexts;
                    break;
                }

                if(j==trieTreeNodes.size()-1){
                    //当前字符串在Trie中不存在 直接跳出
                    return;
                }
            }
        }

        //移除节点
        for(Map.Entry<List<TrieTreeNode>, TrieTreeNode> entry: delQueue.entrySet()){
            List<TrieTreeNode> k = entry.getKey();
            TrieTreeNode v = entry.getValue();
            if(v.nexts.size()<=1){
                if(flag) {
                    k.remove(v);
                    flag = false;
                }
                this.length--;
            }
        }
        //参数递减
        this.size--;
        this.eles.remove(str);
    }

    public int size(){
        return this.size;
    }

    /**
     * 查询的字符串是否在trie字典中存在
     * @param str 被查询的元素字符串
     * @return
     */
    public  boolean search(String str) {

        char ele;
        TrieTreeNode current = this.root;
        List<TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toCharArray();
        boolean flag = false;

        //遍历trie树
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];

            //若词中包含空格直接去除
            if(ele ==' ') {
                continue;
            }
            //遍历此层节点是否已包含了该字母
            for (int j=0;j<trieTreeNodes.size();j++) {
                TrieTreeNode trieTreeNode  = trieTreeNodes.get(j);
                if(trieTreeNode.ele == ele){
                    //若当前字符为该单词的最好一个字母 为避免前缀词的影响 需再次确认下 尾节点的类型
                    if(i==chars.length-1 && trieTreeNode.type == yellow){
                        return true;
                    }
                    current = trieTreeNode;
                    trieTreeNodes = trieTreeNode.nexts;
                    break;
                }

                if(j==trieTreeNodes.size()-1){
                    flag = true;
                }

            }
            if(flag){
                break;
            }
        }
        return false;
    }

    @Override
    public Iterator<String> iterator() {
        return this.eles.iterator();
    }

    /**
     * 实际调用list中接口处理
     * @param action
     */
    @Override
    public void forEach(Consumer<? super String> action) {
        this.eles.forEach(action);
    }

    public static void main(String[] args) {

        TrieTree trieTree = new TrieTree();

        String[] test = {"fuck","bitch","bullshit","bull","suck"};

        for(int i=0;i<test.length;i++){

            trieTree.addNewEle(test[i]);
        }

        //测试迭代功能
        trieTree.forEach(str->{
            System.out.println("-----初始元素集合------"+str);
        });

        //检测单词
        System.out.println("-------bitch------>"+trieTree.search("bitch"));

        System.out.println("------suck------->"+trieTree.search("suck"));

        System.out.println("------bull------->"+trieTree.search("bull"));


        trieTree.remove("bitch");

        trieTree.remove("suck");

        trieTree.remove("bull");

        //测试迭代功能
        trieTree.forEach(str->{
            System.out.println("-----被删除后的元素集合------"+str);
        });

        System.out.println("-------bitch------>"+trieTree.search("bitch"));

        System.out.println("------suck------->"+trieTree.search("suck"));

        System.out.println("------bull------->"+trieTree.search("bull"));

    }
}

最终测试的结果为:

2.2:优化版trie树

package com.fc.javacode.dataStructure.tree;

import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;

/**
 *
 *相对于TrieTree 优化子节点的寻址 使得整个Trie树的时间复杂度为O(n) n为树的深度
 * @author fangyuan
 */
public class TrieHashTree  implements Serializable, Iterable<String> {


    /**
     * 当前Trie树中包含元素个数
     */
    private int size;

    /**
     * 当前Trie树中包含节点数量
     */
    private long length;

    /**
     * 定义根节点
     */
    private TrieHashTree.TrieTreeNode root;

    /**
     * 用于存储当前树中元素
     */
    private List<String> eles;

    //定义节点类型
    /**
     * 标示该节点字符串应该在trie树中
     */
    static final int yellow = 1;

    /**
     * 普通节点
     */
    static final int white = 0;

    /**
     * 每一层树默认分支数
     */
    static final int minTreeLevel = 2;

    /**
     * 构建函数 初始化trie树
     */
    public TrieHashTree(){
        this.size=0;
        this.length=1;
        this.root = new TrieHashTree.TrieTreeNode('/',white);
        this.eles = new ArrayList<>(8);
    }


    /**
     * 定义Trie节点
     */
    class TrieTreeNode {

        //当前节点对应的字母
        private char ele;

        //定义该节点类型
        private int type;

        //当前节点所包含的字节点 后续使用其它结构对其进行优化
        private Map<Character,TrieTreeNode> nexts;

        public  TrieTreeNode(char ele,int type){
            this.ele = ele;
            this.type=type;
            this.nexts = new HashMap<>(minTreeLevel);
        }

    }

    /**
     * 新增一个元素
     * @param str 被写入的元素字符串
     */
    public void addNewEle(String str){

        char ele;
        TrieHashTree.TrieTreeNode current = this.root;
        Map<Character,TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toCharArray();
        //将chars以节点的形式写入到Trie树中
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];
            //若词中包含空格直接去除
            if(ele ==' ') {
                continue;
            }
            //当前节点type
            int nodeType = i==chars.length-1?yellow:white;

            //遍历此层节点是否已包含了该字母
            TrieTreeNode trieTreeNode ;
            if((trieTreeNode = trieTreeNodes.get(ele))!=null){
                //说明当前节点已经存在 处理下前缀词
                trieTreeNode.type =nodeType;
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                continue;
            }else{
                trieTreeNode =  new TrieHashTree.TrieTreeNode(ele,nodeType);
                trieTreeNodes.put(ele,trieTreeNode);
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                this.length++;
            }
        }
        //创建完成
        this.size++;
        eles.add(str);
    }



    /**
     * 查询的字符串是否在trie字典中存在
     * @param str 被查询的元素字符串
     * @return
     */
    public  boolean search(String str) {

        char ele;
        TrieHashTree.TrieTreeNode current = this.root;
        Map<Character,TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toCharArray();
        //将chars以节点的形式写入到Trie树中
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];
            //若词中包含空格直接去除
            if(ele ==' ') {
                continue;
            }

            //遍历此层节点是否已包含了该字母
            TrieTreeNode trieTreeNode ;
            if((trieTreeNode = trieTreeNodes.get(ele))!=null){
                //若当前字符为该单词的最好一个字母 为避免前缀词的影响 需再次确认下 尾节点的类型
                if(i==chars.length-1 && trieTreeNode.type == yellow){
                    return true;
                }
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                continue;
            }else{
                break;
            }

        }
        return false;
    }

    /**
     * 删除一个元素
     * @param str 被删除的元素字符串
     */
    public void remove(String str){

        char ele;
        TrieHashTree.TrieTreeNode current = this.root;
        Map<Character,TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toCharArray();

        LinkedHashMap<Map, Character> delQueue = new LinkedHashMap<>(chars.length);

        //遍历trie树
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];

            //若词中包含空格直接去除
            if (ele == ' ') {
                continue;
            }


            //遍历此层节点是否已包含了该字母
            TrieTreeNode trieTreeNode ;
            if((trieTreeNode = trieTreeNodes.get(ele))!=null){
                //若当前字符为该单词的最后一个字母 若该单词为一个前缀词只需要变更节点属性即可
                if(i==chars.length-1 && trieTreeNode.type == yellow && trieTreeNode.nexts.size()>0){
                    //变更节点类型
                    trieTreeNode.type=white;
                    this.size--;
                    this.eles.remove(str);
                    return;
                }
                //记录当前节点到队列中 为后续删除做准备
                delQueue.put(trieTreeNodes,ele);
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                continue;
            }else{
                return;
            }

        }

        boolean flag = true;

        //移除节点
        for(Map.Entry<Map, Character> entry: delQueue.entrySet()){
            Map k = entry.getKey();
            Character v = entry.getValue();
            if(k.size()<=1){
                if(flag) {
                    k.remove(v);
                    flag = false;
                }
                this.length--;
            }
        }
        //参数递减
        this.size--;
        this.eles.remove(str);
    }
    

    @Override
    public Iterator<String> iterator() {
        return this.eles.iterator();
    }

    /**
     * 实际调用list中接口处理
     * @param action
     */
    @Override
    public void forEach(Consumer<? super String> action) {
        this.eles.forEach(action);
    }


    public static void main(String[] args) {

        TrieHashTree trieTree = new TrieHashTree();

        String[] test = {"fuck","bitch","bullshit","bull","suck","婊子"};

        for(int i=0;i<test.length;i++){

            trieTree.addNewEle(test[i]);
        }

        //测试迭代功能
        trieTree.forEach(str->{
            System.out.println("-----初始元素集合------"+str);
        });

        //检测单词
        System.out.println("-------bitch------>"+trieTree.search("bitch"));

        System.out.println("------suck------->"+trieTree.search("suck"));

        System.out.println("------bull------->"+trieTree.search("bull"));


        trieTree.remove("bitch");

        trieTree.remove("suck");

        trieTree.remove("bull");

        //测试迭代功能
        trieTree.forEach(str->{
            System.out.println("-----被删除后的元素集合------"+str);
        });

        System.out.println("-------bitch------>"+trieTree.search("bitch"));

        System.out.println("------suck------->"+trieTree.search("suck"));

        System.out.println("------bull------->"+trieTree.search("bull"));

    }

}

最终结果:

上述代码中若该Trie树作用只是考虑英文单词的检测,其实更简便的方式,因为每个节点存储的子节点最多为26个,所以使用一个长度为26个字母的数组进行存储,并且每个存储的char字母-a就得到对应的在数组中对应的存储的位置了,这样也就避免了节点使用list结构需要进行的迭代,使用hashmap存储时存在hash冲突的情况了。但因为要考虑到汉字的因素,所以就不能只使用数组了。

3:实现最终的需求,用以检测一段文字中是否包含敏感词,若包含使用*号替换(此处要结合AC算法(Aho-Corasick)),也支持获取所有敏感词

package com.fc.javacode.dataStructure.tree;

import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;

/**
 * 使用AC算法结合Trie来做到高效的多文本匹配
 *
 */
public class TrieTreeAC implements Serializable, Iterable<String> {

    /**
     * 当前Trie树中包含元素个数
     */
    private int size;

    /**
     * 当前Trie树中包含节点数量
     */
    private long length;

    /**
     * 目前最大level深度 为了应对多层词结构(例如存储AB,ABC都是词典中一员需要将两者都处理)
     */
    private int maxLevel;

    /**
     * 定义根节点
     */
    private TrieTreeAC.TrieTreeNode root;

    /**
     * 用于存储当前树中元素
     */
    private List<String> eles;

    String xh = "";

    //定义节点类型
    /**
     * 标示该节点字符串应该在trie树中
     */
    static final int yellow = 1;

    /**
     * 普通节点
     */
    static final int white = 0;

    /**
     * 每一层树默认分支数
     */
    static final int minTreeLevel = 2;


    /**
     * 构建函数 初始化trie树
     */
    public TrieTreeAC(){
        this.size=0;
        this.length=1;
        this.root = new TrieTreeAC.TrieTreeNode('/',white,0);
        this.eles = new ArrayList<>(8);
    }


    /**
     * 定义Trie节点
     */
    class TrieTreeNode {

        //当前节点对应的字母
        private char ele;

        //定义该节点类型
        private int type;

        //当前节点所包含的字节点 后续使用其它结构对其进行优化
        private Map<Character, TrieTreeAC.TrieTreeNode> nexts;

        //当前节点对应level深度
        private int level;

        //当查询失败时跳到的节点
        private TrieTreeNode failTo;

        public  TrieTreeNode(char ele,int type,int level){
            this.ele = ele;
            this.type=type;
            this.level=level;
            this.nexts = new HashMap<>(minTreeLevel);
        }

    }

    /**
     * 记录字符串中
     */
    class Position{

        int startCursor;

        int endCursor;

        public  Position(int startCursor,int endCursor){
            this.startCursor = startCursor;
            this.endCursor = endCursor;
        }
    }

    /**
     * 初始化
     * @param entitys
     */
    public  void init(List<String> entitys){
        this.buildTrieTree(entitys);
        this.buildFailTo();
    }

    /**
     * 根据词典创建一个新的字典树
     */
    private void buildTrieTree(List<String> entitys){

        //将数据写入到Trie树
        entitys.forEach(str->{
            this.addNewEle(str);
        });

    }

    /**
     * 为每个节点构建匹配失败时的跳转-失败结点(即匹配失败时,跳转到哪个结点继续匹配)
     * 其思想为:第一层子节点的fail节点直接指定是根节点
     * 其它子节点的fail节点 是从其父节点指定的fail节点的子节点中寻找该节点中对应的字符,若存在则找到fail节点了,若不存在继续向上寻找,直到找相应的节点或最终的失败节点为root节点时结束 
     * 
     */
    private void buildFailTo() {
        //这里使用算法类似于BFS算法(核心是一样的,从根节点向外部一层一层处理 ,每一层都要为每个节点找到对应的fail节点)
        Queue<TrieTreeNode> queue = new LinkedList<TrieTreeNode>();

        queue.add(this.root);

        while(!queue.isEmpty()){

            TrieTreeNode  currentNode =  queue.poll();

            TrieTreeNode failTo = currentNode.failTo;

            //处理当前节点的子节点
            for (Map.Entry<Character, TrieTreeNode> entry :currentNode.nexts.entrySet()) {

                Character childEle = entry.getKey();
                TrieTreeNode childNode = entry.getValue();

                queue.add(childNode);
                //循环一直找到该节点的回退点
                while(true){
                    if(failTo == null){
                        //如果当前节点无回退点 直接以根节点作为回退点
                        childNode.failTo=this.root;
                        break;
                    }
                    //判断fail节点是其父节点的失败点中查找相应路径
                    if(failTo.nexts.containsKey(childEle)){
                        childNode.failTo = failTo.nexts.get(childEle);
                        break;
                    }else{
                        //继续在当前节点的失败节点中查找相应的路径
                        failTo = failTo.failTo;
                    }
                }
            }
        }

        //根路径fail指向root节点
        this.root.failTo = this.root;
    }

    /**
     * 将一段文字中敏感字符进行替换为*号格式
     * @param str 需要被替换的字符
     */
    public String replace(String str){
        List<Position> positions = getPositions(str);
        //获取对应位置上字符串
        StringBuilder sb = new StringBuilder(str);
        positions.forEach(position -> {
            sb.replace(position.startCursor,position.endCursor+1,this.xh.substring(0,position.endCursor-position.startCursor+1));
        });
        return sb.toString();
    }

    /**
     *
     * @param str
     * @return
     */
    public List<String> filter(String str){

        List<Position> positions = getPositions(str);

        List<String> rs = new ArrayList<>(positions.size());

        positions.forEach(position -> {
            rs.add(str.substring(position.startCursor,position.endCursor+1));
        });
        return rs;
    }

    /**
     *
     * @param str
     * @return
     */
    private List<Position> getPositions(String str){

        char ele;
        char[] chars = str.toLowerCase().toCharArray();
        //定义起点游标 遍历字符串起点游标
        int startCursor = 0;
        //定义end游标
        int endCurson = 0;

        List<Position> positions = new ArrayList<>(8);
        TrieTreeAC.TrieTreeNode current = this.root;

        //开始进行替换
        for(int i=0;i<chars.length;i++){

            endCurson = i;
            ele = chars[i];
            //遍历此层节点是否已包含了该字母
            TrieTreeAC.TrieTreeNode trieTreeNode ;

            if((trieTreeNode = current.nexts.get(ele))!=null){
                //处理AC中goto
                if( trieTreeNode.type == yellow ){
                    if(trieTreeNode.nexts.size()>0 &&trieTreeNode.level<this.maxLevel){
                        //虽然找到end节点 避免前缀词的影响继续向下迭代
                        //记录对应位置 继续向下寻找
                        Position position = new Position(startCursor,endCurson);
                        positions.add(position);
                        current = trieTreeNode;
                    }else{
                        //说明已找到词典 记录对应字典的位置
                        Position position = new Position(startCursor,endCurson);
                        positions.add(position);
                        //开始下一轮的词典的寻找
                        startCursor = i;
                        current = this.root;
                    }
                }else{
                    //继续下一个字符的匹配
                    current = trieTreeNode;
                    continue;
                }
            }else{
                //匹配失败 尝试进行跳转到失败节点进行下一次匹配
                //重置startCursor位置
                if(current.failTo.equals(this.root)){
                    //当前节点fialTo的节点为root根节点
                    //开始下一轮的词典的寻找

                    //重置位置
                    if(i <chars.length-1 && current.level>0){
                        startCursor = i;
                        i = i-1;
                    }else{
                        startCursor = i+1;
                    }
                }else{
                    //计算当前对应start位置 并更新
                    startCursor +=(i+1-startCursor - current.failTo.level);
                }
                current = current.failTo;
            }
        }
        return positions;
    }

    /**
     * 新增一个元素
     * @param str 被写入的元素字符串
     */
    public void addNewEle(String str){

        char ele;
        TrieTreeAC.TrieTreeNode current = this.root;
        Map<Character, TrieTreeAC.TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toLowerCase().toCharArray();
        //将chars以节点的形式写入到Trie树中
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];
            //若词中包含空格直接去除
            if(ele ==' ') {
                continue;
            }
            //当前节点type
            int nodeType = i==chars.length-1?yellow:white;
            //若当前类型为end节点 用以计算最大深度
            if(nodeType == yellow && maxLevel<i+1){
                //构建*库
                for(int n=0;n<(i+1)-maxLevel;n++){
                    this.xh+="*";
                }
                maxLevel = i+1 ;
            }
            //遍历此层节点是否已包含了该字母
            TrieTreeAC.TrieTreeNode trieTreeNode ;
            if((trieTreeNode = trieTreeNodes.get(ele))!=null){
                //说明当前节点已经存在 处理下前缀词
                trieTreeNode.type =nodeType;
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                continue;
            }else{
                trieTreeNode =  new TrieTreeAC.TrieTreeNode(ele,nodeType,i+1);
                trieTreeNodes.put(ele,trieTreeNode);
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                this.length++;
            }
        }
        //创建完成
        this.size++;
        eles.add(str);
    }


    /**
     * 查询的字符串是否在trie字典中存在
     * @param str 被查询的元素字符串
     * @return
     */
    public  boolean search(String str) {

        char ele;
        TrieTreeAC.TrieTreeNode current = this.root;
        Map<Character, TrieTreeAC.TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toLowerCase().toCharArray();
        //将chars以节点的形式写入到Trie树中
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];
            //若词中包含空格直接去除
            if(ele ==' ') {
                continue;
            }

            //遍历此层节点是否已包含了该字母
            TrieTreeAC.TrieTreeNode trieTreeNode ;
            if((trieTreeNode = trieTreeNodes.get(ele))!=null){
                //若当前字符为该单词的最好一个字母 为避免前缀词的影响 需再次确认下 尾节点的类型
                if(i==chars.length-1 && trieTreeNode.type == yellow){
                    return true;
                }
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                continue;
            }else{
                break;
            }
        }
        return false;
    }

    /**
     * 删除一个元素
     * @param str 被删除的元素字符串
     */
    public void remove(String str){

        char ele;
        TrieTreeAC.TrieTreeNode current = this.root;
        Map<Character, TrieTreeAC.TrieTreeNode> trieTreeNodes = this.root.nexts;
        //将当前词解析为char字符数组
        char[] chars = str.toLowerCase().toCharArray();

        LinkedHashMap<Map, Character> delQueue = new LinkedHashMap<>(chars.length);

        //遍历trie树
        for (int i = 0; i < chars.length; i++) {

            ele = chars[i];

            //若词中包含空格直接去除
            if (ele == ' ') {
                continue;
            }

            //遍历此层节点是否已包含了该字母
            TrieTreeAC.TrieTreeNode trieTreeNode ;
            if((trieTreeNode = trieTreeNodes.get(ele))!=null){
                //若当前字符为该单词的最后一个字母 若该单词为一个前缀词只需要变更节点属性即可
                if(i==chars.length-1 && trieTreeNode.type == yellow && trieTreeNode.nexts.size()>0){
                    //变更节点类型
                    trieTreeNode.type=white;
                    this.size--;
                    this.eles.remove(str);
                    return;
                }
                //记录当前节点到队列中 为后续删除做准备
                delQueue.put(trieTreeNodes,ele);
                current = trieTreeNode;
                trieTreeNodes = trieTreeNode.nexts;
                continue;
            }else{
                return;
            }
        }

        boolean flag = true;
        //移除节点
        for(Map.Entry<Map, Character> entry: delQueue.entrySet()){
            Map k = entry.getKey();
            Character v = entry.getValue();
            if(k.size()<=1){
                if(flag) {
                    k.remove(v);
                    flag = false;
                }
                this.length--;
            }
        }
        //参数递减
        this.size--;
        this.eles.remove(str);
    }

    @Override
    public Iterator<String> iterator() {
        return this.eles.iterator();
    }

    /**
     * 实际调用list中接口处理
     * @param action
     */
    @Override
    public void forEach(Consumer<? super String> action) {
        this.eles.forEach(action);
    }


    public static void main(String[] args) {

        TrieTreeAC trieTreeAC = new TrieTreeAC();

        List<String> test = new ArrayList<>();

        test.add("fuck");
        test.add("bitch");
        test.add("bullshit");
        test.add("bull");
        test.add("suck");
        test.add("婊子");
        test.add("我草");
        test.add("草");
        test.add("我草你");

        trieTreeAC.init(test);
        //
        String testStr = "你好你好啊,我bullshitbitc哈哈卧槽我草你呀啊呀呀发疯";
        System.out.println("=======原语句为=======>"+testStr);
        //测试替换功能
        String rs = trieTreeAC.replace(testStr);

        trieTreeAC.filter(testStr).forEach(str->{
            System.out.println(str);
        });

        System.out.println("=======处理过后的语句为=======>"+rs);


        //测试迭代功能
        trieTreeAC.forEach(str->{
            System.out.println("-----初始元素集合------"+str);
        });

        //检测单词
        System.out.println("-------bitch------>"+trieTreeAC.search("bitch"));

        System.out.println("------suck------->"+trieTreeAC.search("suck"));

        System.out.println("------bull------->"+trieTreeAC.search("bull"));


        trieTreeAC.remove("bitch");

        trieTreeAC.remove("suck");

        trieTreeAC.remove("bull");

        //测试迭代功能
        trieTreeAC.forEach(str->{
            System.out.println("-----被删除后的元素集合------"+str);
        });

        System.out.println("-------bitch------>"+trieTreeAC.search("bitch"));

        System.out.println("------suck------->"+trieTreeAC.search("suck"));

        System.out.println("------bull------->"+trieTreeAC.search("bull"));

    }

}

最终的结果满足要求:

源码地址

数据结构——22. KMP算法和Trie树 数组),当在某个节点匹配失败时,可以通过失败指针跳转到另一状态,从而实现在一个文本串中同时查找多个模式串,且时间复杂度为线性的。如果学有余力,可以了解一下这个概念,它会让你对 Trie 树和 KMP 的理解更加深刻。这个“跳转到哪里”的信息,就存储在一个预先计算好的辅助数组中,这个数组通常被称为。它在 Trie 树的结构上,为每个节点增加了一个“失败指针”(类似 KMP 的。从根节点开始,遍历单词的每个字符。KMP 算法是一种高效的字符串匹配算法,它的核心在于。从根节点开始,顺着单词的字符路径往下走。 阅读详情

相关推荐

看图轻松理解数据结构与算法系列(Trie树)

前言 推出一个新系列,《看图轻松理解数据结构和算法》,主要使用图片来描述常见的数据结构和算法,轻松阅读并理解掌握。本系列包括各种堆、各种队列、各种列表、各种树、各种图、各种排序等等几十篇的样子。 Trie树 Trie树,是一种搜索树,也称字典树或单词查找树,此外也称前缀树,因为某节点的后代存在共同的前缀。它的key都为字符串,能做到高效查询和插入,时间复杂度为O(k),k为字符串长度,缺点是如果大...

seaboat——a free boat on the sea.(公众号:远洋号) 900

敏感词过滤

敏感词过滤 主要内容: 前缀树 名称:Trie、字典树、查找树 特点:查找效率高,消耗内存大 应用:字符串检索、词频统计、字符串排序等 敏感词过滤器 定义前缀树 获取敏感词,根据敏感词,初始化前缀树 编写过滤敏感词的方法 实现功能: 传入字符串,过滤其中的敏感词字段,返回过滤后的字段和当前存在的敏感词集合 前缀树示例 1、假设有2个敏感词 ace bd,画出根节点,从敏感词中分析出第一层 2、继续分析后续字母,每一层对应敏感词第几个字母 3、在敏感词底层字母处做一个标记,遍历到中

weixin_46186045的博客 1662

JAVA:实现Trie树数据结构算法(附完整源码)

JAVA:实现Trie树数据结构算法(附完整源码)

希望我的博客,能帮上你解决学习中工作中所遇到的问题 495

lambda表达式

lambda表达式 文 | 少鹏 (cherry) 1 初识lambda表达式 List&amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;String&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; list = Arrays.asList(&amp;amp;amp;amp;amp;amp;amp;amp;quot;one&amp;amp;amp;amp;amp;amp;amp

cherry的博客 1191

Ubuntu下Ollama局域网共享大模型服务:5分钟搞定配置与防火墙设置

本文提供了一份在Ubuntu系统上快速配置Ollama实现局域网共享大模型服务的实战指南。通过修改服务配置使其监听所有网络接口,并设置UFW防火墙规则,用户可在5分钟内让同一Wi-Fi下的其他设备(如笔记本、手机)便捷调用主机上的AI模型能力,实现团队或多设备间的AI资源共享。

ik6789012的博客 822

SolidWorks草图绘制实战:如何用图片参考快速搞定智能小车底板设计

本文详细介绍了在SolidWorks中利用图片参考进行草图绘制的实战方法,以智能小车底板设计为例,帮助设计师快速将概念转化为精准三维模型。通过插入、对齐参考图片并高效描图,结合参数化约束与尺寸驱动,大幅提升设计效率和准确性,尤其适合新手入门与老手优化工作流。

rust6ferris的博客 218

Java 中常用的函数式接口Consumer

文章目录写在前面1、 Consumer 源码解析1.1、@FunctionalInterface注解1.2、void accept(T t)1.3、default Consumer andThen(Consumer<? super T> after)2、 Consumer 使用实例2.1、forEach(Consumer<? super T> action)2.2、格式化打印信息 实例 写在前面 java.util.function.Consumer 接口,一般用它来消费一个数据,

这里的分享,都是干货 3059

树莓派c语言串口通讯程序

在网上搜了很多关于树莓派串口通讯的例子,但是都是用python写的,虽然python很有名,而且最近也在学习这门语言, 但是还是想用c语言实现一下,因为需要用到整套系统里,其他内容都是用c语言写的。但是网上没有找打现成的。 还好,之前做过天嵌2440开发板的串口程序(参照天嵌工程师提供的代码),发现串口程序其实是通用的, 驱动层屏蔽了硬件细节,个人猜测,几乎所有移植好的系统的串口,都可以用一

realank的专栏 1万+

【图解算法】Trie树

Trie树,字符串查找树

就爱摆烂 5160

2019年电赛D题《简易电路特性测试仪》全过程

2019年全国大学生电子设计大赛D题《简易电路特性测试仪》硬件部分及故障分析

zzhao2580的博客 5万+

开源PACS系统Orthanc(DICOM服务器)配置

PACS(Picture Archiving and Communication System)是医学影像的数字化存储、检索和传输系统,用于管理和存储医学图像和相关信息,如放射学、核医学、超声、病理学等各种医学影像。PACS系统通过数字化技术取代了传统的胶片方式,使医疗机构能够更高效地存储、传输和查看医学图像,从而提高了医疗图像的管理和工作流程。

qq_42231043的博客 6249

数据结构:Trie(前缀树/字典树)

Trie 又称字典树、前缀树和单词查找树,是一颗非典型的多叉树模型,即每个结点的分支数量可能为多个。Trie这个名字取自“retrieval”,检索,读音和 try 相同。

m0_63997099的博客 3950

CTF 命令执行绕过总结

tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示。或:rce-xor-or.php & rce-xor-or.py。# ls -t >shell 将输出输入到shell文件中。异或:rce-xor.php & rce-xor.py。%09(url传递)(cat%09flag.php)sh /flag 2>%261 //报错出文件内容。2、1使用空变量$*和$@,$x,${x}绕过。7、异或无符号(过滤0-9a-zA-Z)more:一页一页的显示档案内容。

jnszstmei的博客 1959

数据结构与算法学习笔记----Trie树

@ author: 明月清了个风。

weixin_60278491的博客 1208

VC Spyglass CDC验证避坑指南:如何正确配置同步复位信号(实验二重点解析)

本文深入解析VC Spyglass CDC验证中同步复位信号的正确配置方法,通过实验二案例详细展示错误配置的典型表现及修正过程。文章提供同步复位的最佳实践方案,包括约束文件修改指南、验证流程优化建议和高级调试技巧,帮助工程师有效避免CDC验证中的常见陷阱。

rock5的博客 678

Yocto开发讲解系列 - 总目录

Yocto开发讲解系列总纲Yocto开发专栏前言

fulinux的博客 3万+

算法基础_数据结构【KMP + Trie 树 + 并查集】

本篇博客是《算法基础_数据结构》的算法题型总结+模板习题练习。主要算法涉及:KMP、Trie树、并查集的算法。

序属秋秋秋的博客 1362

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

数据结构:kmp算法,Trie树,以及并查集的干货详解---小白也能看懂

kmp算法,Trie, 并查集的详解及其模板

2401_86619696的博客 1828

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

算法与数据结构-Trie树

Trie 树,也叫“字典树”。顾名思义,它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。当然,这样一个问题可以有多种解决方法,比如散列表、红黑树,或者我们前面几节讲到的一些字符串匹配算法,但是,Trie 树在这个问题的解决上,有它特有的优点。不仅如此,Trie 树能解决的问题也不限于此,我们一会儿慢慢分析。现在,我们先来看下,Trie 树到底长什么样子。我举个简单的例子来说明一下。

码农界的菜鸟 577

算法-数据结构-实现 Trie (前缀树)

算法-数据结构-实现 Trie (前缀树) 1 题目概述 1.1 题目出处 https://leetcode-cn.com/problems/implement-trie-prefix-tree/ 1.2 题目描述 实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。 示例: Trie trie = new Trie(); trie.insert(“apple”); trie.search(“apple”); // 返回 true trie.sea

迷路剑客个人博客 321

【数据结构与算法】深刨Trie树(字典树)

Trie树

qq_66314292的博客 807

【算法·数据结构】trie树(字典树)

有关trie树的概念 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里。为了提高查找和定位的速度,通常都要画出与单词列表所对应的单词查找树,其特点如下: 根节点不包含字母,除根节点外每一个节点都仅包含一个大写英文字母; 从根节点到某一节点,路径上经过的字母依次连起来所构成的字母序列,称为该节点对应的单词。单词列表中的每个词,都是该单词查找树某个节点所对应的单词; 在满足上述条件下,...

Ronaldo7_ZYB的博客 739

数据结构与算法10:递归树、Trie树、B+树

对于很多业务场景需要使用“二分思想”的情况下,基本上都可以拆解为二叉树这种数据结构来处理,比如归并排序、快速排序、二分查找、跳表等。归并排序的递归树是一棵满二叉树,在二分的过程中的时间复杂度是O(logn)。Trie树也叫“字典树”,它是一种专门处理字符串匹配的数据结构。B树是⼀种多叉平衡查找树,而且⾮叶⼦节点和叶⼦节点都会存储数据;B+树是只有叶⼦节点才会存储数据。

码农兴哥的技术笔记 1011

JAVA:实现Trie树数据结构算法(附带源码)

JAVA:实现Trie树数据结构算法(附带源码)

欢迎来到我的博客!这里是一个专注于计算机技术的分享平台,涵盖编程开发、算法研究、系统架构、软件工程等多个领域。无论你是初学者还是资深开发者,都能在这里找到有价值的内容。 1067

数据结构与算法—Trie树

Trie,又经常叫前缀树,字典树等等。它有很多变种,如后缀树,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree。当然很多名字的意义其实有交叉。 Trie树是一种非常重要的数据结构,它在信息检索,字符串匹配等领域有广泛的应用,同时,它也是很多算法和复杂数据结构的基础,如后缀树,AC自动机等。 典型应用是用于统计和排序大量的字符串(但不...

weixin_30396699的博客 196

【数据结构算法】Trie树

数据结构-Trie树 Trie树是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。 class Trie: # word_end = -1 def __init__(self): """ Initialize your data structure here.

weixin_43409812的博客 648
上一篇: Java实现LRU算法
下一篇: 算法数据结构-树(1)
F_Hello_World
博客等级 码龄11年 38粉丝 58原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值