字典树又称单词查找树,它是一种树形结构,是一种哈希树的变种,典型应用是用于统计,保存大量的字符串(但不仅限于字符串),统计以是否有以某字符串最为前缀的字符串,有的话有多少,某字符串出现了多少次等,所以经常被搜索引擎系统用于文本词频统计。
它与字典很相似,当你要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找.字典树不仅可以用来储存字母,也可以储存数字等其它数据。它的优势是,利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表还高,当数据足够庞大时,会发现她比传统的字符串统计要快很多。
它有三个基本性质:
(1)根节点不存储字符
(2)除根节点外每一个节点都只存储一个字符
(3)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串,每个节点的所有子节点包含的字符都不相同。
下边我将把我实现的代码和大家分享一下,代码几乎每行都有详细注释,大家一看就清除明了了,时间原因,就不再用多余的文字再追加详述了。
package Trie; import org.apache.commons.lang3.StringUtils; import com.google.common.base.CharMatcher; /** * 字典树类 * * @author chenleixing */ public class Trie { //各个节点的子树数目即字符串中的字符出现的最多种类 private final int SIZE = 26; //除根节点外其他所有子节点的数目 private int numNode; //树的深度即最长字符串的长度 private int depth; //字典树的根 private TrieNode root; /** * 初始化字典树 */ public Trie() { this.numNode=0; this.depth=0; this.root = new TrieNode(); } /** * 字典树节点类,为私有内部类 */ private class TrieNode { // 所有的儿子节点或者一级子节点 private TrieNode[] son; // 有多少字符串经过或到达这个节点,即节点字符出现的次数 private int numPass; // 有多少字符串通过这个节点并到此结束的数量 private int numEnd; // 是否有结束节点 private boolean isEnd; // 节点的值 private char value; /** * 初始化节点类 */ public TrieNode() { this.numPass=0; this.numEnd=0; this.son = new TrieNode[SIZE]; this.isEnd = false; } }
一般字典树常用存储单词类字符串,当然也可以存储数字或者其他字符只是一个耗费内存较多的问题,不管怎么最好在操作方法之前做个验证和判断,以更加的提高操作效率,代码健壮性更强,如插入,查找是否存在等,如果字符串中存在“非法”的字符,那么可以直接返回false来结束操作。代码如下边所示:
/** * 对操作的字符串进行一个非法的判断,是否为字母构成的字符串 */ private boolean isStrOfLetter(String str){ //null或者空白字符串,则插入失败 if (StringUtils.isBlank(str)){ return false; } //如果字符串中有非字母字符,则插入失败 if(!CharMatcher.JAVA_LETTER.matchesAllOf(str)){ return false; } return true; }
其中代码中我用到了common-lang工具包里的StringUtils工具类和Guava里的CharMatcher,当然大家可以用循环啊用正则表达式或者其他工具来实现相同的功能,这里就不再一一详述了,如果有想深入了解它们用法的话或jar包下载,可参考我的以下博文:commons-lang中常用方法,StringUtils方法全集介绍,JavaScript、Java正则表达式详解,打了兴奋剂的CharMatcher,Strings字符串判断工具。
/** * 插入方法,插入字符串,不区分大小写 */ public boolean insertStr(String str) { //插入的字符为非法字符,则插入失败 if(!isStrOfLetter(str)){ return false; } //插入字符串 str=str.toLowerCase();//不区分大小写,转为小写 char[] letters = str.toCharArray();//转成字符数组 TrieNode node=this.root;//先从父节点开始 for (char c:letters) { int pos = c - 'a';//得到应存son[]中的索引 if (node.son[pos] == null) {//此字符不存在 node.son[pos] = new TrieNode(); node.son[pos].value = c; node.son[pos].numPass=1; this.numNode++; } else {//此字符已经存入 node.son[pos].numPass++; } node = node.son[pos];//继续为下一下字符做准备 } node.isEnd = true;//标记:有字符串到了此节点已结束 node.numEnd++;//这个字符串重复次数 if(letters.length>this.depth){//记录树的深度 this.depth=letters.length; } return true;//插入成功 }
/** * 在字典树中查找是否存在某字符串为前缀开头的字符串(包括前缀字符串本身),不区分大小写 */ public boolean isContainPrefix(String str) { //查找的字符是否非法字符,则失败 if(!isStrOfLetter(str)){ return false; } //查找字符串 str=str.toLowerCase();//不区分大小写,转为小写 char[] letters = str.toCharArray();//转成字符数组 TrieNode node=this.root;//先从父节点开始 for (char c:letters) { int pos = c - 'a';//得到应存son[]中的索引 if (node.son[pos] != null) { node=node.son[pos];//此字符存在继续查找下一个字符 } else {//此字符不存在 return false; } } return true; }
/** * 在字典树中查找是否存在某字符串(不为前缀),不区分大小写 */ public boolean isContainStr(String str) { //查找的字符是否非法字符,则失败 if(!isStrOfLetter(str)){ return false; } //查找字符串 str=str.toLowerCase();//不区分大小写,转为小写 char[] letters = str.toCharArray();//转成字符数组 TrieNode node=this.root;//先从父节点开始 for (char c:letters) { int pos = c - 'a';//得到应存son[]中的索引 if (node.son[pos] != null) { node=node.son[pos];//此字符存在继续查找下一个字符 } else {//此字符不存在 return false; } } return node.isEnd; }
/** * 统计以指定字符串为前缀的字符串数量,不区分大小写 */ public int countPrefix(String str) { //统计的字符是否非法字符,则返回0 if(!isStrOfLetter(str)){ return 0; } //查找字符串 str=str.toLowerCase();//不区分大小写,转为小写 char[] letters = str.toCharArray();//转成字符数组 TrieNode node=this.root;//先从父节点开始 for (char c:letters) { int pos = c - 'a';//得到应存son[]中的索引 if (node.son[pos] == null) { return 0;//没有以此字符串为前缀开头 } else {//此字符存在,继续遍历 node=node.son[pos]; } } return node.numPass; }
/** * 统计以指定字符串为前缀的字符串数量,不区分大小写 */ public int countPrefix(String str) { //统计的字符是否非法字符,则返回0 if(!isStrOfLetter(str)){ return 0; } //查找字符串 str=str.toLowerCase();//不区分大小写,转为小写 char[] letters = str.toCharArray();//转成字符数组 TrieNode node=this.root;//先从父节点开始 for (char c:letters) { int pos = c - 'a';//得到应存son[]中的索引 if (node.son[pos] == null) { return 0;//没有以此字符串为前缀开头 } else {//此字符存在,继续遍历 node=node.son[pos]; } } return node.numPass; }
/** * 统计以指定字符串为前缀的字符串数量,不区分大小写 */ public int countPrefix(String str) { //统计的字符是否非法字符,则返回0 if(!isStrOfLetter(str)){ return 0; } //查找字符串 str=str.toLowerCase();//不区分大小写,转为小写 char[] letters = str.toCharArray();//转成字符数组 TrieNode node=this.root;//先从父节点开始 for (char c:letters) { int pos = c - 'a';//得到应存son[]中的索引 if (node.son[pos] == null) { return 0;//没有以此字符串为前缀开头 } else {//此字符存在,继续遍历 node=node.son[pos]; } } return node.numPass; }
这里是通过递归打印出所有的节点的值,如果想存入一个List或者追加StringBuilder或者StringBuffer中,需要创建一个全局变量或者方法里创建然后以参数形式传到递归方法中,这里不再进行详述,因为字典树的主要用途不在这里,此方法一般不需要。
/** * 返回根节点,根节点不存值 */ public TrieNode getRoot() { return this.root; }
/** * 返回字典树的深度 */ public int getDept() { return this.depth; }
/** * 返回字典树的所有子节点的数目(不包含子节点) */ public int getNumNode() { return this.numNode; }
package Trie; import org.junit.Test; public class TrieTest { /** * 测试字典树 * * @author chenleixing */ @Test public void testTrie(){ //创建一个字典树(其实可以在创建时指定字典树各节点的大小,大小根据存入字符种类的数量) Trie trie=new Trie(); //测试字符串(当然越庞大越能展现它的优势) String[] testStrs=new String[]{"chefsd","chen","hahi","ch","cxing","hahha","my","home"}; for(String s:testStrs){//向字典树中存入字符串 trie.insertStr(s); } //测试是否包含指定前缀的字符串 boolean isCont=trie.isContainPrefix("ch"); System.out.println(isCont);//输出true //测试包含指定前缀的字符串的数量 int countPrefix=trie.countPrefix("ch"); System.out.println(countPrefix);//输出3 //测试包含指定字符串的数量 int countStr=trie.countStr("ch"); System.out.println(countStr);//输出1 //测试包含指定前缀的字符串的数量 int countPre=trie.countPrefix("chee"); System.out.println(countPre);//输出0 //测试子节点的数量和树的深度 int numNode=trie.getNumNode();//为22 int dept=trie.getDept();//为6 System.out.println("字典树子节点的数量:"+numNode+" 树的深度:"+dept); } }
true 3 1 0 字典树子节点的数量:22 树的深度:6
over了!
转载请注明—作者:Java我人生(陈磊兴) 原文出处:http://blog.csdn.net/chenleixing/article/details/44708533