一棵用List来存储子结点的字典树——当然,也可以用哈希表等形式存储。
这篇笔记记录了构建思路,文末是源码
一、构建思路
- Step1 设计结点——数据结构
- Step2 实现相应的操作方法——增删改查
Step1 设计结点
我们需要该结点能够存储以下数据:
- 该结点代表的单个字符->>字节
- 该结点是否为词语结尾(叶子结点)->>布尔
- 该结点的子结点串->>链表结构
public class Trie {
private Character ch;//字符
private Boolean isLeaf=false;//是否是词结点
private List<Trie> children=new ArrayList<>();//子结点
//此处省略构造方法、getter&setter、toString方法
}
Step2 实现增删改查
1. 查
思路:
- 有序遍历查询词的每一个字符;
- 同时逐层遍历子结点;
- 当且仅当该层存在该字符,遍历该字符的子结点以继续查询;
- 当且仅当该词的最后一个字符被找到且为叶子节点,方认为该词存在;
- 否则认为该词不存在;
public Boolean searchWord(String text){
Boolean found=false;//是否匹配到该字
Boolean isLeaf=false;//该字是否是词结点
List<Trie> offspring=this.children;
for(int i=0;i<text.length();i++){
found=false;
for(Trie t:offspring){
if (t.getCh()==text.charAt(i)){
found=true;
isLeaf=t.isLeaf;
offspring=t.children;
break;
}
}
if (!found)
return false;
}
return (found&&isLeaf);
}
2. 增
思路:
- 逐字遍历插入词,同时逐层遍历检索树
- 当且仅当该字不存在,则添加该字及其以后诸字结点
- 该词的最后一个字结点必须为叶子结点
PS 增删改这里可能会遇到一个知识点可以注意一下:
- 关键概念:深拷贝(deep copy)与浅拷贝(shallow copy)
- 年少的我曾以为这种坑爹概念只有py有,所以卡在这里好久总想不通这个增删改应该怎么实现QAQ
public Boolean insertWord(String text){
Boolean found=false;//是否匹配到该字
Boolean isLeaf=false;//该字是否是词结点
Trie root=this;
for (int i=0;i<text.length();i++){
found=false;
for(Trie t:root.children){
if (t.getCh()==text.charAt(i)){
found=true;
isLeaf=t.isLeaf;
//ATTENTION!这里root=t操作是引用对象的浅拷贝,即root与t将指向同一数据
//正是利用这一点,我们得以通过操作root从而操作this这棵原始子树的子结点
root=t;
break;
}
}
if (!found){
root.children.add(addSubChild(text.substring(i)));
return true;
}
}
if (found&&!isLeaf){
root.isLeaf=true;
}
return false;
}
private Trie addSubChild(String subText){
Trie subLastChild=new Trie(subText.charAt(subText.length()-1),true);
Trie subChild;
for (int i=subText.length()-2;i>-1;i--){
subChild=new Trie(subText.charAt(i));
subChild.addChild(subLastChild);
subLastChild=subChild;
}
return subLastChild;
}
3.删
思路:
- 逐字遍历插入词,同时逐层遍历检索树
- 当且仅当该字符在该层存在则继续该字符下一层的遍历
- 遍历过程中用辅助对象(deleteTrie, deleteVal)标记这条搜索路径上距离最终的目标叶子最近的有孩子的结点或叶子结点
- 若遍历结束,目标叶子存在则进行剪枝操作
- 最后的剪枝操作分为两种情况:
- 若目标叶子有子结点,则取消目标叶子的叶子特性即可;
- 若目标叶子没有子结点,则在这条从根节点到目标节点的搜索之路上,自之前deleteTrie所标记的结点之后的结点,一直到目标叶子,都是该剪掉废枝桠——直接删掉deleteTrie下一层且在搜搜路径上的那个结点即可。
public Boolean deleteWord(String text){
Boolean found=false;//是否匹配到该字
int deleteVal=-1;//需要删除的字符层
Trie deleteTrie=this;
Trie root=this;
for (int i=0;i<text.length();i++){
found=false;
for(Trie t:root.children){
if (t.getCh()==text.charAt(i)){
found=true;
root=t;
if((root.isLeaf||root.children.size()>1)&&(i!=text.length()-1)){
deleteVal=i;
deleteTrie=root;
}
break;
}
}
if (!found){
return false;
}
}
if(found){
if (root.children.size()>0){
root.isLeaf=false;
}
else {
for (int r=0;r<deleteTrie.children.size();r++){
if (deleteTrie.children.get(r).ch==text.charAt(deleteVal+1)){
deleteTrie.children.remove(r);
break;
}
}
}
return true;
}
return false;
}
//感觉这个代码块还能优化一下,等之后有时间再继续叭
4.改
思路:
- 把原有的词语修改为新的词语——这个操作成立的前提是,原词必须存在
- 理想的最优路径应该是:先搞个匹配算法(看看新旧词开始不一样的是第几个字符);然后,便利到该字符的前一个结点,删了原词语的余下子串,插入新词的余下子串,这样的遍历长度应该是(preLength+posLength-divLength)。(preLength、posLength、divLength分别代表原词长度、新词长度、匹配算法找到的新旧词从第一个字开始完全一致的长度)
- 代码最少的路径则是:利用之前写过的增删查三个方法,直接删了原词,添加新词即可,这样的遍历长度则是(preLength+posLength)。
- 不过,由于笔者建立这颗检索树的目的是为了中文分词,而中文的词语一般比较短(不像英文单词动则十几个字母),所以感觉这两个算法相差的divLength可能对于整个后续整个工程而言,量级比较小的亚子,所以就偷偷懒选择后一个算法啦~
public Boolean updateWord(String preText,String posText){
if (this.searchWord(preText)){ //原词必须存在
this.deleteWord(preText);//删了原词
this.insertWord(posText);//添加新词
return true;
}
return false;
}
二、源码
import java.util.ArrayList;
import java.util.List;
public class Trie {
private Character ch;//字符
private Boolean isLeaf=false;//是否是词结点
private List<Trie> children=new ArrayList<>();//子结点
protected Trie(){}
public Trie(Character ch){
this.ch=ch;
}
public Trie(Character ch, Boolean isLeaf){
this.ch=ch;
this.isLeaf=isLeaf;
}
public List<Trie> getChildren() {
return children;
}
public void setChildren(List<Trie> children) {
this.children = children;
}
public Character getCh() {
return ch;
}
public void setCh(Character ch) {
this.ch = ch;
}
public Boolean getLeaf() {
return isLeaf;
}
public void setIsLeaf(Boolean isLeaf) {
this.isLeaf = isLeaf;
}
/**
* 添加子结点
* @param child
* @return
*/
public Trie addChild(Trie child){
//遍历子结点,查看该孩子是否已存在
for (int i=0;i<this.children.size();i++){
Trie oldChild=this.children.get(i);
if (oldChild.getCh().equals(ch)){
this.children.set(i,oldChild);
return oldChild;
}
}
//如果该孩子不存在,则添加该孩子
this.children.add(child);
return child;
}
/**
* 搜索词语
* @param text
* @return
*/
public Boolean searchWord(String text){
Boolean found=false;//是否匹配到该字
Boolean isLeaf=false;//该字是否是词结点
List<Trie> offspring=this.children;
for(int i=0;i<text.length();i++){
found=false;
for(Trie t:offspring){
if (t.getCh()==text.charAt(i)){
found=true;
isLeaf=t.isLeaf;
offspring=t.children;
break;
}
}
if (!found)
return false;
}
return (found&&isLeaf);
}
/**
* 添加词语
* @param text
* @return
*/
public Boolean insertWord(String text){
Boolean found=false;//是否匹配到该字
Boolean isLeaf=false;//该字是否是词结点
Trie root=this;
for (int i=0;i<text.length();i++){
found=false;
for(Trie t:root.children){
if (t.getCh()==text.charAt(i)){
found=true;
isLeaf=t.isLeaf;
root=t;
break;
}
}
if (!found){
root.children.add(addSubChild(text.substring(i)));
return true;
}
}
if (found&&!isLeaf){
root.isLeaf=true;
}
return false;
}
/**
* 删除词语
* @param text
* @return
*/
public Boolean deleteWord(String text){
Boolean found=false;//是否匹配到该字
int deleteVal=-1;//需要删除的字符层
Trie deleteTrie=this;
Trie root=this;
for (int i=0;i<text.length();i++){
found=false;
for(Trie t:root.children){
if (t.getCh()==text.charAt(i)){
found=true;
root=t;
if((root.isLeaf||root.children.size()>1)&&(i!=text.length()-1)){
deleteVal=i;
deleteTrie=root;
}
break;
}
}
if (!found){
return false;
}
}
if(found){
if (root.children.size()>0){
root.isLeaf=false;
}
else {
for (int r=0;r<deleteTrie.children.size();r++){
if (deleteTrie.children.get(r).ch==text.charAt(deleteVal+1)){
deleteTrie.children.remove(r);
break;
}
}
}
return true;
}
return false;
}
/**
* 修改词语
* @param preText
* @param posText
* @return
*/
public Boolean updateWord(String preText,String posText){
if (this.searchWord(preText)){//原词必须存在
this.deleteWord(preText);
this.insertWord(posText);
return true;
}
return false;
}
/**
* 添加词语方法的添加子串操作
* @param subText
* @return
*/
private Trie addSubChild(String subText){
Trie subLastChild=new Trie(subText.charAt(subText.length()-1),true);
Trie subChild;
for (int i=subText.length()-2;i>-1;i--){
subChild=new Trie(subText.charAt(i));
subChild.addChild(subLastChild);
subLastChild=subChild;
}
return subLastChild;
}
/**
* IDE自动生成的ToString,便于测试
* @return
*/
@Override
public String toString() {
return "Trie{" +
"ch=" + ch +
", isLeaf=" + isLeaf +
", children=" + children +
'}';
}
}