JAVA字典树:以及算法的改进;

版权声明:只做原创,告别转载。 https://blog.csdn.net/pingsha_luoyan/article/details/80077409

为什么需要用到字典树算法:

当我们需要对一些需要的过滤的文本内容进行筛选时,最简单的方法就是逐个查找,需要过滤一个关键字时,也许不是很麻烦,但是当我们需要过滤很多关键字,并且过滤的文本很大时,逐个查找就很浪费时间和内存空间。

如何理解字典树算法:

字典树算法就是利用字符串的公共前缀来减少查询时间,能够最大限度的减少字符串的比较次数。

   首先:我先讲一下字典树算法是如何实现的:

这个树就是字典树:它表示我们需要过滤的字:abcd , abd ,bcd ,efg ,hi,b;

并且被红点标记过的就是需要过滤的关键字,运行到标记处时,就可以过滤该字段;

                                                

假如最开始需要查询的文本为:abdehiefgchasbaasjk;

我们需要定义3个指针:指针1.指针2,指针3;

            (指针1指向字典树的根节点,指针2,和指针3都是指向查询文本的头,当指针2指向文本的末尾时,算法结束);

最开始时,指针1指向字典树的根节点,指针3所指的字符与指针1所指的节点的子节点比较,假若相同,则指针3后移一位,指针1后移到相同的子节点继续比较,如果该节点是被标记的,则跳出循环,(可以选择将指针2所指的)指针1回到根节点,指针2指向指针3所指的位置后移一位,指针3指向指针2;

假若不同,那么指针2直接后移一位,指针3也后移一位;循环,知道指针2 指向末尾时结束;

举例:

如果不明白文字叙述的,可以根据表格来理解:

初始过滤文本abdehiefgchasbaasjk;需要过滤的字:abcd , abd ,bcd ,efg ,hi,b;

第一次:因为ab是过滤字的关键字,所以删除或者替代

指针 初始状态次 第一次 第二次 第三次      
指针1 根节点 a b

根节点

因为b是有标记的节点,

所以跳出循环,将ab删除或者替代

     
指针2 a a a d      
指针3 a b d d      

第二次:因为d不是过滤字的关键字,根节点下没有子节点与d匹配,所以保留,指针23后移;

指针 初始状态次 第一次          
指针1 根节点 根节点          
指针2 d e          
指针3 d e          

第三次:根节点下有e节点,但是e节点下没有h节点,所以eh不是需要过滤字;

指针 初始状态次 第一次 第二次 第三次 第四次 第五次 第六次
指针1 根节点 e 根节点        
指针2 e e h        
指针3 e h h        

根据表格就很容易理解了。

最后,用java来实现字典树算法:

package com.nowcoder.service;

import com.nowcoder.controller.HomeController;
import org.apache.commons.lang.CharUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;


import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

@Service
public class SensitiveService implements InitializingBean {
    private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
    @Override
    public void afterPropertiesSet() throws Exception {
        //读取关键字文件
        try{
            InputStream is =
                    Thread.currentThread().getContextClassLoader().getResourceAsStream("SensitiveWords.txt");
            InputStreamReader read = new InputStreamReader(is);
            BufferedReader bufferedReader = new BufferedReader(read);
            String lineTxt;
            while ((lineTxt = bufferedReader.readLine())!=null){
                //去掉空格之后加入到前缀树中;
                addWord(lineTxt.trim());
            }
            read.close();
        }catch (Exception e){
            logger.error("读取敏感词文件失败:"+e.getMessage());
        }

    }
    //增加过滤字的关键字
    // 例如: abcd
    private void addWord(String linrTxt){
        //将树指向根节点
        TrieNode tempNode = rootNode;
        //遍历前缀树
        for(int i=0;i<linrTxt.length();++i){
            //依次查找过滤字
            //例如:abcd过滤字,第一次找的是a,
            Character c = linrTxt.charAt(i);
            //例如:第一次找a;看看有没有a的节点;
            TrieNode node = tempNode.getSubNode(c);
            //如果没有a的节点,增添一个a的节点;
            if(node==null){
                node = new TrieNode();
                tempNode.addSubNode(c,node);
            }
            //有a的情况下,直接进入下一个节点;
            tempNode = node;
            //判断过滤字的关键字是否到结尾,结尾就设置为true;
            if(i==linrTxt.length()-1){
                tempNode.setkeywordEnd(true);
            }
        }
    }
    //(字典树)前缀树节点//创建一个字典树
    private  class  TrieNode{
        //是不是关键词的结尾;
        private  boolean end = false;
        //当前节点下的所有子节点;
        private Map<Character , TrieNode> subNodes =new HashMap<Character, TrieNode>();
        //往字典树中加入过滤字;
        public void addSubNode(Character key ,TrieNode node){
            subNodes.put(key,node);
        }
        //获得当前节点的下一个节点;
        TrieNode getSubNode(Character key){
            return subNodes.get(key);
        }
        //判断是不是字典树的尾节点
        boolean  isKeywordEnd(){
            return end;
        }
        void setkeywordEnd(boolean end){
            this.end=end;
        }
    }
    //前缀树的根节点
    private  TrieNode rootNode = new TrieNode();

    //判断是否是一个符号
    private boolean isSymbol(char c) {
        int ic = (int) c;
        // 0x2E80-0x9FFF 东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
    }



    //过滤敏感词关键字部分
    public  String filiter(String text){
        //判断文本是否为空
        if(StringUtils.isBlank(text)){
            return text;
        }
        //过滤完成后的结果集;
        StringBuilder result = new StringBuilder();
        //过滤字的替代
        String replacement = "*";
        //三个指针实现字典树算法
        //1,初始状态指向前缀树的根节点的指针
        TrieNode tempNode = rootNode;
        //2,指向需要查询的文本的初始位置的指针:
        //只要指针指向末尾结束时,则整个文本过滤完成
        int begin = 0;
        //3,初始位置协同的指针
        int position = 0;
        //后面都用指针1,指针2,指针3,来代替指针
        //判断指针3有没有检索完整个指针
        while(position<text.length()){
            //当前位置取出
            char c = text.charAt(position);
            //遇到空格直接跳过
            if (isSymbol(c)) {
                if (tempNode == rootNode) {
                    result.append(c);
                    ++begin;
                }
                ++position;
                continue;
            }
            //根节点下面有没有过滤字的关键字
            tempNode = tempNode.getSubNode(c);
            //没有发现敏感词关键字
            if(tempNode==null){
                //将指针2指向的文本放入到过滤完成后的结果集中;
                result.append(text.charAt(begin));
                //一次过滤完成后;
                //指针3指向下一个要检测的位置
                position=begin+1;
                //指针2同步到指针3所在的位置
                begin=position;
                //指针1指向前缀树的根节点
                tempNode = rootNode;
            }else if (tempNode.isKeywordEnd()){
                //发现敏感词关键字
                result.append(replacement);
                begin=position+1;
                position=begin;
                tempNode=rootNode;
            }else{
                ++position;
            }
        }
        result.append(text.substring(begin));
        return result.toString();
    }

    public  static void main(String [] argv){
        SensitiveService sensitiveService = new SensitiveService();
        sensitiveService.addWord("淫秽");
        System.out.print(sensitiveService.filiter("淫秽"));
    }
} position=begin+1;
                //指针2同步到指针3所在的位置
                begin=position;
                //指针1指向前缀树的根节点
                tempNode = rootNode;
            }else if (tempNode.isKeywordEnd()){
                //发现敏感词关键字
                result.append(replacement);
                begin=position+1;
                position=begin;
                tempNode=rootNode;
            }else{
                ++position;
            }
        }
        result.append(text.substring(begin));
        return result.toString();
    }

    public  static void main(String [] argv){
        SensitiveService sensitiveService = new SensitiveService();
        sensitiveService.addWord("淫秽");
        System.out.print(sensitiveService.filiter("淫秽"));
    }
}
 //(字典树)前缀树节点
    private  class  TrieNode{
        //是不是关键词的结尾;
        private  boolean end = false;
        //当前节点下的所有子节点;
        private Map<Character , TrieNode> subNodes =new HashMap<Character, TrieNode>();
        //往字典树中加入过滤字;
        public void addSubNode(Character key ,TrieNode node){
            subNodes.put(key,node);
        }
        //获得当前节点的下一个节点;
        TrieNode getSubNode(Character key){
            return subNodes.get(key);
        }
        //判断是不是字典树的尾节点
        boolean  isKeywordEnd(){
            return end;
        }
        void setkeywordEnd(boolean end){
            this.end=end;
        }
    }
    //前缀树的根节点
    private  TrieNode rootNode = new TrieNode();

    //判断是否是一个符号
    private boolean isSymbol(char c) {
        int ic = (int) c;
        // 0x2E80-0x9FFF 东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
    }

//判断是否是一个符号
    private boolean isSymbol(char c) {
        int ic = (int) c;
        // 0x2E80-0x9FFF 东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
    }

上述代码,我在字典树的基础上做了改进:

字典树算法改进:

    一般来说:我们需要过滤掉“坏蛋”这个词语,但是当有人用“坏 蛋”或者“坏※蛋”等时就不会被过滤掉,

因此我增添了除掉空格和非东亚文字的内容;来达到正确过滤的目的;

判断是不是东亚文字:东亚文字的范围(0x2E80-0x9FFF)

//判断是否是一个符号
    private boolean isSymbol(char c) {
        int ic = (int) c;
        // 0x2E80-0x9FFF 东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
    }

猜你喜欢

转载自blog.csdn.net/pingsha_luoyan/article/details/80077409