为什么需要用到字典树算法:
当我们需要对一些需要的过滤的文本内容进行筛选时,最简单的方法就是逐个查找,需要过滤一个关键字时,也许不是很麻烦,但是当我们需要过滤很多关键字,并且过滤的文本很大时,逐个查找就很浪费时间和内存空间。
如何理解字典树算法:
字典树算法就是利用字符串的公共前缀来减少查询时间,能够最大限度的减少字符串的比较次数。
首先:我先讲一下字典树算法是如何实现的:
这个树就是字典树:它表示我们需要过滤的字: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("淫秽"));
}
}
//(字典树)前缀树节点
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);
}
上述代码,我在字典树的基础上做了改进:
字典树算法改进:
一般来说:我们需要过滤掉“坏蛋”这个词语,但是当有人用“坏 蛋”或者“坏※蛋”等时就不会被过滤掉,
因此我增添了除掉空格和非东亚文字的内容;来达到正确过滤的目的;
判断是不是东亚文字:东亚文字的范围(0x2E80-0x9FFF)
//判断是否是一个符号
private boolean isSymbol(char c) {
int ic = (int) c;
// 0x2E80-0x9FFF 东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
}