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.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种情况:
- 若删除的字符串在Tire的存在是前缀词的存在,这种删除比较简单,只需要找到尾部节点将其节点的标志位转变下即可
- 若删除的字符串在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"));
}
}
最终的结果满足要求: