第一眼看到Trie树算法,首先明白的就是他一定是用树形结构实现的算法。后来实现完整个算法才知道其实他也是压缩树,类似于哈弗曼编码和CF-Tree,因为树中保留了公共的前缀,减少了不必要的重复存储空间。所以查询效率会高很多,如果你明白哈弗曼编码的实现过程,这个自然也是一样的道理。那Trie树与Huffman编码树有什么区别呢,Huffman是0或1的编码,而Trie则是文本查找树,节点上可以是一个字母字符,也可以是汉字等等,大体就是这个意思。好,下面说说算法的原理。
1、首先获取所有的文本数据,划分成逐条逐条的形式。
2、读入每行数据,对照当前比较字符值与当前节点的子节点比较,寻找到与之匹配的节点
3、如果找到对应的子节点,将子节点作为当前节点,并移除数据的此字符,继续步骤2。
4、如果未找到对应子节点,新建节点插入当前的节点中,并将新节点作为当前节点,继续步骤2。
5、操作的终止条件为数据中的字符已经全部移除比较完毕。
输入的字符数据Input.txt:
abc bcd bca bcc bbd abca树节点类TreeNode.java:
package Trie; import java.util.ArrayList; /** * * * * @author lyq * * */ public class TreeNode { //节点的值 String value; //节点孩子节点 ArrayList<TreeNode> childNodes; public TreeNode(String value) { this.value = value; this.childNodes = new ArrayList<TreeNode>(); } public ArrayList<TreeNode> getChildNodes() { return childNodes; } public void setChildNodes(ArrayList<TreeNode> childNodes) { this.childNodes = childNodes; } }算法工具类TrieTool.java:
package Trie; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; /** * * * * @author lyq * * */ public class TrieTool { // 测试数据文件地址 private String filePath; // 原始数据 private ArrayList<String[]> datas; public TrieTool(String filePath) { this.filePath = filePath; readDataFile(); } /** * * 从文件中读取数据 */ private void readDataFile() { File file = new File(filePath); ArrayList<String[]> dataArray = new ArrayList<String[]>(); try { BufferedReader in = new BufferedReader(new FileReader(file)); String str; String[] tempArray; while ((str = in.readLine()) != null) { tempArray = new String[str.length()]; for (int i = 0; i < str.length(); i++) { tempArray[i] = str.charAt(i) + ""; } dataArray.add(tempArray); } in.close(); } catch (IOException e) { e.getStackTrace(); } datas = dataArray; } /** * * 构造Trie树 * * * * @return */ public TreeNode constructTrieTree() { TreeNode rootNode = new TreeNode(null); ArrayList<String> tempStr; for (String[] array : datas) { tempStr = new ArrayList<String>(); for (String s : array) { tempStr.add(s); } // 逐个字符串的添加 addStrToTree(rootNode, tempStr); } return rootNode; } /** * * 添加字符串的内容到Trie树中 * * * * @param node * * @param strArray */ private void addStrToTree(TreeNode node, ArrayList<String> strArray) { boolean hasValue = false; TreeNode tempNode; TreeNode currentNode = null; // 子节点中遍历寻找与当前第一个字符对应的节点 for (TreeNode childNode : node.childNodes) { if (childNode.value.equals(strArray.get(0))) { hasValue = true; currentNode = childNode; break; } } // 如果没有找到对应节点,则将此节点作为新的节点 if (!hasValue) { // 遍历到了未曾存在的字符值的,则新键节点作为当前节点的子节点 tempNode = new TreeNode(strArray.get(0)); // node.childNodes.add(tempNode); insertNode(node.childNodes, tempNode); currentNode = tempNode; } strArray.remove(0); // 如果字符已经全部查找完毕,则跳出循环 if (strArray.size() == 0) { return; } else { addStrToTree(currentNode, strArray); } } /** * * 将新建的节点按照字母排序的顺序插入到孩子节点中 * * * * @param childNodes * * 孩子节点 * * @param node * * 新键的待插入的节点 */ private void insertNode(ArrayList<TreeNode> childNodes, TreeNode node) { String value = node.value; int insertIndex = 0; for (int i = 0; i < childNodes.size() - 1; i++) { if (childNodes.get(i).value.compareTo(value) <= 0 && childNodes.get(i + 1).value.compareTo(value) > 0) { insertIndex = i + 1; break; } } if (childNodes.size() == 0) { childNodes.add(node); } else if (childNodes.size() == 1) { // 只有1个的情况额外判断 if (childNodes.get(0).value.compareTo(value) > 0) { childNodes.add(0, node); } else { childNodes.add(node); } } else { childNodes.add(insertIndex, node); } } }测试类Client.java:
package Trie; /** * * Trie树算法 * * @author lyq * * */ public class Client { public static void main(String[] args) { String filePath = "C:\\Users\\lyq\\Desktop\\icon\\input.txt"; TrieTool tool = new TrieTool(filePath); tool.constructTrieTree(); } }算法的最终构造的树的形状大致如下(由于时间关系,我就没有写在控制台输出的程序了):
root
|
a b
| |---|
b b c
| | |----|-----|
c d a c d
|
a
这里所说的遗漏点就是在插入节点的时候,需要按照字母的排序插入,这是为了使得查找更加的高效。算法在构建树的时候每次都从根节点开始往下找,效率不够高,其实更好的办法是把输入数据进行字典序的排序,然后再当前节点做处理,要么继续往下添加,要么回溯到上一个节点。
算法的特点在最开始介绍的时候也已经提到过,利用了字符串的公共前缀减少了查询时间,最大限度的减少无谓的字符串比较,常用于做文本的词频统计。