算法:用Java实现trie树,并用于实现敏感词过滤的功能

trie树,可能大家都比较陌生。trie树,又叫字典树,是一种树形结构,一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

trie树有3个基本性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同

我们今天准备用Java来实现trie树,然后利用其高效的搜索功能,搜索句子里面的敏感词,并将敏感词替换为*。trie树的搜索原理如下:

  1. 从根结点开始一次搜索
  2. 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索
  3. 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索
  4. 迭代上述过程……
  5. 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找
  6. 而敏感词比对的话,如果比对过程中某一个字母在子树中不存在,那么第一个字母开头的敏感词就不存在,就从第一个字母的下一个字母为首继续找
  7. 如果句子结束而trie树还没查找完毕,也算是没找到
  8. 如果某个字母已经找到了trie树的叶结点了,那么就找到了一个敏感词,就把第一个找的字母和最后一个字母全部替换为*,完成过滤功能

此外一个问题,那就是一个敏感词是另一个敏感词的前缀的问题。比如说有三个敏感词:hello、hell、he。假设这三个敏感词加入到trie树,trie树的最终效果就是只增加了hello这一个单词,也就只会过滤hello,hell和he就无法过滤了。

我的实现原理,就是每一个结点新增一个布尔类型的属性,是否是结束字母(isEndLetter)。也就是在敏感词加入到trie树的过程中,加到了最后一个字母,那最后一个字母的结点,该布尔属性设置为true,不管是在没有的树枝上新增结点,还是在已有的树枝上走,反正最后一个字母的结点(创建的或者是找到的),都设置该布尔类型为true。当敏感词过滤的时候,设置一个变量为敏感词查找的检查点checkpoint,如果在某一个树枝上找到了某个结点为true,则记录下来,后面又找到就再更新记录(保证是最长的敏感词)。如果找到树的叶子结点,找到了全树枝单词,那么全树枝单词就是敏感词。如果没有找到叶子结点下一个单词就不对了,那么就以上一个检查点(当前checkpoint)找到的敏感词为准。

本次Java实现trie树,根节点为 ‘/’ ,测试的敏感词为:hello、hell、he、hero、see、sun、right(注意,敏感词需要不包含 * 号,否则会抛出非法参数异常)。加上每个结点的isEndLetter的布尔值,画成trie树,就是下面的样子:
在这里插入图片描述
接下来上代码,我用Java编写了TrieTree类,可以实现添加敏感词,以及根据添加的敏感词过滤句子,返回过滤后的句子,其中敏感词的字母都会被替换成*号:

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @Author: LiYang
 * @Date: 2020/2/19 20:43
 * @Description: trie树(字典树)做敏感词过滤
 */
public class TrieTree {

    /**
     * trie树的结点类(TrieTree的静态内部类)
     */
    private static class TrieNode {

        //当前结点的字母
        private char letter;

        //当前节点字母是否可以作为结束字母
        private boolean isEndLetter = false;

        //当前结点的下一结点集合
        private Set<TrieNode> next;

        /**
         * 空构造方法
         */
        public TrieNode() {

        }

        /**
         * 入参letter的构造方法
         * @param letter
         */
        public TrieNode(char letter) {
            this.letter = letter;

            //为每一个结点创建一个空的next集合
            this.next = new HashSet<>();
        }

        /**
         * 重写equals方法,结点字母相等,结点即相等
         * @param obj
         * @return
         */
        @Override
        public boolean equals(Object obj) {
            //如果不是同一类,则绝对不等
            if (!(obj instanceof TrieNode)) {

                //返回不等
                return false;
            }

            //字母相等,则结点相等,否则不等
            return this.letter == ((TrieNode)obj).letter;
        }

        /**
         * 重写了equals方法,就得跟着重写hashCode方法
         * @return
         */
        @Override
        public int hashCode() {
            //以letter的数值代表hashcode
            return this.letter;
        }

        /**
         * 查找某字母的下级字母结点
         * @param letter 查找的下一个字母
         * @return 下一个字母的结点
         */
        public TrieNode findNext(char letter) {
            //遍历查找
            for (TrieNode node : next) {

                //如果找到了
                if (node.letter == letter) {

                    //返回该结点
                    return node;
                }
            }

            //没找到,返回null
            return null;
        }

    }


    /************** 接下来是TrieTree类的属性和方法 **************/

    //trie树的根结点,默认为'/'
    private TrieNode root = new TrieNode('/');

    //trie树的公共后缀(句子尾部的处理需要)
    private static final String PUBLIC_SUFFIX = "********";

    /**
     * 空构造方法
     */
    public TrieTree() {

    }

    /**
     * 带初始敏感词数组的构造方法
     * @param words
     */
    public TrieTree(String[] words) {
        //遍历敏感词数组
        for (int i = 0; i < words.length; i++) {

            //加入初始敏感词
            addWord(words[i]);
        }
    }

    /**
     * 加入需要过滤的敏感词(敏感词不能带*号)
     * @param word 待加入的敏感词
     */
    public void addWord(String word) {
        //验证敏感词里面是否带有*号(否则程序可能会出错)
        if (word.contains("*")) {

            //抛出非法参数异常
            throw new IllegalArgumentException("敏感词中不能含有*号");
        }

        //将敏感词转化为字符数组
        char[] charArray = word.toCharArray();

        //当前trie树结点为根结点
        TrieNode currentNode = root;

        //遍历敏感词每一个字母
        for (int i = 0; i < charArray.length; i++) {

            //在当前trie树结点的下一个字母结点集合里,寻找当前字母结点
            TrieNode nextNode = currentNode.findNext(charArray[i]);

            //如果当前字母存在
            if (nextNode != null) {

                //找到的当前字母结点作为当前trie树结点
                //接下来在当前字母结点的下一个字母集合里找下一个字母
                currentNode = nextNode;

            //如果当前字母不存在
            } else {

                //为当前字母创造trie树结点
                TrieNode trieNode = new TrieNode(charArray[i]);

                //将当前字母创造的trie树结点,加入到当前结点的下一个结点集合里
                currentNode.next.add(trieNode);

                //以当前字母创造的trie树结点作为当前结点,继续找下一个字母
                //有就继续看下一个字母,没有就创建,直到创建完整敏感词树枝
                currentNode = trieNode;
            }
        }

        //将最后一个字母,设置为结束字母
        currentNode.isEndLetter = true;
    }

    /**
     * 过滤敏感词(用当前TrieTree类的敏感词库来过滤)
     * @param sentence 待过滤的原始句子
     * @return 敏感词字母被替换为*的句子
     */
    public String filtrate(String sentence) {
        //要处理的句子,统一加上公共后缀(句子尾部的处理需要)
        sentence = sentence + PUBLIC_SUFFIX;

        //将原始句子转化为字符数组
        char[] charArray = sentence.toCharArray();

        //过滤后的句子的字符列表
        List<Character> filtrated = new ArrayList<>();

        //遍历原始句子字符数组,从每一个字母开始向右同步走一遍trie树
        for (int i = 0; i < charArray.length; i++) {

            //字母和trie树的比对指标,就是同步比对到第几个字母了
            int index = i;

            //结束字母比对检查点,记录找到的从起始字母
            //到所有结束字母中最大长度的结束字母的下标
            int checkpoint = -1;

            //当前比对的trie树结点,用它的下一字母集合比对,初始化为root
            TrieNode currentNode = root;

            //持续比对
            while (true) {

                //如果比对的字母已经超过了原始句子的字符数组边界
                if (index > charArray.length - 1) {

                    //停止比对
                    break;
                }

                //当前trie树结点的下一字母集合的元素个数
                int nextSize = currentNode.next.size();

                //将当前字母在当前trie树结点的下一字母集合中寻找
                //是否存在当前字母的trie树结点
                currentNode = currentNode.findNext(charArray[index]);

                //如果在trie树上找到了
                if (currentNode != null) {

                    //如果是结束字母
                    if (currentNode.isEndLetter) {

                        //更新结束字母检查点
                        checkpoint = index;
                    }

                    //指标+1,也就是继续拿下一个字母
                    //和下一个trie树结点继续比对
                    index ++;

                //如果trie树上没找到,且还没找到trie树的尽头,即叶结点
                } else if (nextSize > 0) {

                    //如果检查点不为初始值-1,也就是存在前缀敏感词
                    if (checkpoint != -1) {

                        //首先我们得到前缀敏感词的长度
                        int prefixWordLength = checkpoint - i + 1;

                        //遍历敏感词长度的次数
                        for (int j = 0; j < prefixWordLength; j++) {

                            //加入等长的*符号
                            filtrated.add('*');
                        }

                        //将原始句子的遍历指标置为前缀
                        //敏感词最后一个字母的位置
                        i = checkpoint;

                    //如果检查点为i,也就是不存在前缀敏感词
                    } else {

                        //说明以原始句子字母开头的所有词中不存在敏感词
                        //将该字母原样装入到过滤后的句子的字符列表
                        filtrated.add(charArray[i]);
                    }

                    //结束比对
                    break;

                //如果trie树上没找到,且找到了trie树的尽头,即叶结点
                //那就是找到了以原始句子字母开头的敏感词
                //我们的处理方式是用 "*" 代替敏感词的字母
                } else {

                    //首先,我们得到敏感词的长度
                    int wordLength = index - i;

                    //遍历敏感词长度的次数
                    for (int j = 0; j < wordLength; j++) {

                        //加入等长的*符号
                        filtrated.add('*');
                    }

                    //将原始句子的遍历指标置为
                    //敏感词最后一个字母的位置
                    i = index - 1;

                    //结束比对
                    break;
                }
            }
        }
        //至此,过滤后的句子的字符列表已创建完毕

        //创建StringBuffer对象
        StringBuffer sbuf = new StringBuffer();

        //遍历过滤后的句子字符列表
        for (Character character : filtrated) {

            //将过滤后句子的字符拼接
            sbuf.append(character);
        }

        //返回原始句子过滤后的句子(截取尾部公共后缀前的句子)
        return sbuf.substring(0, sbuf.length() - PUBLIC_SUFFIX.length());
    }


    /**
     * 测试trie树的敏感词过滤功能
     * @param args
     */
    public static void main(String[] args) {
        //敏感词字符串数组
        String[] words = {"hello", "hell", "he", "hero", "see", "sun"};

        //额外加入的敏感词
        String extraWord = "right";

        //创建TrieTree实例,初始化加入words的六个敏感词
        TrieTree trieTree = new TrieTree(words);

        //继续加入第七个敏感词
        trieTree.addWord(extraWord);

        //过滤前的句子
        String sentence = "hellofsxhelllgfeheldxghergasgherohgseevsunfright";

        //打印过滤前的句子
        System.out.println("过滤前:" + sentence);

        //调用TrieTree示例的方法,将句子进行敏感词过滤
        String filtrated = trieTree.filtrate(sentence);

        //打印过滤后的句子,用作比对
        System.out.println("过滤后:" + filtrated);
    }

}

运行新的TrieTree类的main方法,控制台打印如下,测试通过:

过滤前:hellofsxhelllgfeheldxghergasgherohgseevsunfright
过滤后:*****fsx****lgfe**ldxg**rgasg****hg***v***f*****
发布了67 篇原创文章 · 获赞 14 · 访问量 3万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章