首先在文章的开头声明一下哈,本文只是介绍一种Java蛮力键树的实现,并没有什么高深的数据结构,所以数据量不超过百万字符的可以参考,数据量太大的另请高明吧。另外,后面的键树代码实际上不仅适用于中文存储和查找,只要是字符串形式的数据都可以存储。比如:“锄禾日当午”、“a+你好啊234#jfjf”这样形式的数据都可以放进去(韩文柬埔寨文怎么混搭都可以,只要编码方式别搞混)。
键树是一种非常简单的数据结构,相信学过的人都知道,没学过的人一看就明白:
图1 一棵键树
好了,既然它这么简单,那我就不介绍了,想要完整阅读键树定义的读者可以随便百度,下面开始设计分析。传统键树拥有两种可选的存储结构,分别是双链树和多重链表(又称为Trie树)。多重链表表示法适用于键树中结点的度较大的情况,因此本文在实现中文键树时考虑使用多重链表结构。键树将一条完整的信息串分割成一层一层的结点结构,对应到中文,即每一个结点上存储了一个汉字信息。典型的键树通常采用数组来实现结点后代的存储,这是源于英文字母只有固定26个。鉴于中文无法同样考虑,在实现时采用ArrayList来实现后代的存储。这既保证了查询速度,又避免数组越界的问题。因此,一个结点的结构就是:
class TrieNode {
public String value;
public ArrayList<TrieNode> ptr = null;
public TrieNode(String value) {
this.value=value;
ptr =new ArrayList<TrieNode>();
}
}
向这棵键树中插入新节点是很简单的,比如插入的新内容是一个"apple"的单词,那么将这个字符串依次拆开,逐个在键树中向下寻找(在ArrayList中遍历比较),并最终决定放不放就行(放就add,不放就下一层或者结束),具体实现就是后文中的insert(String key)方法。同样的道理,查找也很简单。
好了,键树就实现完成了,很简单。这里加了一个内容:因为正常人实现这种数据结构都会想要提供模糊查询的功能,比如我查找:"ap",就希望这棵树能给我一个"apple",满足你。实现这个功能的基本功就在于最简单的树的先序遍历,不过由于这是贱树,所以又不太一样。
因为前面说了百万以上不要看本文,所以我这里的先序遍历用了递归(百万以下就不要叫会栈溢出,随心所欲的插就行)。原理很简单,往遍历方法里传一个树结点,比如前面查询了"ap",那么"ap"的p结点就传了进来。然后用一个StringBuffer来装进后面的"ple"。如果还有类似于"application"这样的单词,就倒回去,再在StringBuffer里装一遍"plication"。有多个关键词的就会将每次查询的StringBuffer装进一个ArrayList<String>,最后这个集合searchResult就存储了模糊查询的结果。
ArrayList<String> searchResult=new ArrayList<String>();
StringBuffer tempWord=new StringBuffer();
int start=0;
private void traverseTree(TrieNode p){
if(!(p.ptr.isEmpty())){
for(TrieNode tn:p.ptr){
tempWord.append(tn.value);
start++;
traverseTree(tn);
start--;
tempWord.delete(start,tempWord.length());
}
}else{
searchResult.add(tempWord.toString());
}
}
最后说一下性能和改进:
性能:
20万字符(约60000条古诗)模糊查询平均耗时为1毫秒。插入的时间非常短,短到我忘了测(以上性能什么概念呢,就是如果你要做一个简易的搜索提示框的话,后台用这个键树来实现是非常合适的,搜索提示的反应零卡顿非常快。那如果是点一个按钮然后查询那种功能就更不在话下了)。
改进(这里的改进如果完成的话,那么和市面上一线的搜索引擎相应功能比,也就输在没有商标):
(1)对于要满足中拼双搜的搜索框提示功能,需要维护中文、拼音两棵键树(中文拼音转化可使用pinyin4j开源库,处理时注意拼音时涉及多音字),在设计算法时会复杂很多(复杂4倍左右)。
(2)为了提升中文键树的效率,可以考虑按照偏旁拆分中文来组织键树结点结构(就像按照偏旁部首查字典一样),将会使键树的效率提升非常多。实现这样的算法需要中文偏旁api的支持,至于有不有这样的api我就不知道了。
代码贴:中文键树的蛮力实现(可处理任意字符串)
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import com.yhk.filewriter.MyReader;
/*
* 无数据结构设计下的蛮力中文键树
*/
class TrieNode {
public String value;
public ArrayList<TrieNode> ptr = null;
public TrieNode(String value) {
this.value=value;
ptr =new ArrayList<TrieNode>();
}
}
public class TrieTree_1 {
private static TrieNode root = null;
ArrayList<String> searchResult=new ArrayList<String>();
StringBuffer tempWord=new StringBuffer();
int start=0;
public TrieTree_1() {
root = new TrieNode(null);
}
public void insert(String key) {
TrieNode p = root;
String tempWord;
boolean contains;
TrieNode tempNode;
for (int i = 0; i < key.length(); i++) {
tempWord=String.valueOf(key.charAt(i));
contains=false;
for(TrieNode tn:p.ptr){
if(tn.value.equals(tempWord)){
p=tn;
contains=true;
break;
}
}
if(!contains){
tempNode=new TrieNode(tempWord);
p.ptr.add(tempNode);
p=tempNode;
}
}
}
public ArrayList<String> search(String key) { //模糊查询就是这个方法,打个比方比如key是"ap",那么ArrayList里就有{"apple","application"}
TrieNode p = root;
String temp;
boolean contains=false;
for (int i = 0; i < key.length(); i++) {
temp=String.valueOf(key.charAt(i));
contains=false;
for(TrieNode tn:p.ptr){
if(tn.value.equals(temp)){
p=tn;
contains=true;
break;
}
}
if(contains){
continue;
}else{
break;
}
}
if(contains){
if(!(p.ptr.isEmpty())){
//查找到关键字
searchResult.clear();
tempWord.delete(0, tempWord.length());
tempWord.append(key);
start=key.length();
traverseTree(p);
}else{
//已经查找到键树的底部
return null;
}
}else{
//没有查找到相应关键字
return null;
}
return searchResult;
}
private void traverseTree(TrieNode p){
if(!(p.ptr.isEmpty())){
for(TrieNode tn:p.ptr){
tempWord.append(tn.value);
start++;
traverseTree(tn);
start--;
tempWord.delete(start,tempWord.length());
}
}else{
searchResult.add(tempWord.toString());
}
}
}