说来也奇怪,最近碰到的很多问题都需要用字典树来解决,索性就来研究一番。在这篇博客中,我会通过一些实例来讲解一下字典树的一些基本使用。例如:创建、添加、查找、按字典序排序、按数值大小进行排序(对于一些数值序列的排序)等等。
关于字典的实际应用实例,请参见本人的另一篇博客:《算法:两种对拼音进行智能切分的方法》
本文链接:http://blog.csdn.net/lemon_tree12138/article/details/49177509 -- 编程小笙
--转载请注明出处
为了便于以下对字典树的说明,我这里的节点Node可能会有一些对于读者而并不必要的成员。例如,fre, visited, minLength, prefixCount等等。这里读者可以根据自己的需求自行增减。
String name; // 结点的字符名称 int fre; // 单词的词频 boolean end; // 是否是单词结尾 boolean root; // 是否是根结点 Node[] children; // 子节点信息 boolean visited; // 是否已经遍历过了 int minLength; // 通过该节点的最小的数字长度 int prefixCount = 0; // 有多少单词通过这个节点,即节点字符出现的次数 Node parent; // 当前节点的父节点
对于创建一棵空的字典树,其实相对来说是比较容易的。因为,我们不需要对树进行一些元素新增或是移除。我们只是对字典树中的一些必要的成员进行了一些初始化的工作。下面是代码部分:
Node root; int depth; public TrieTree(String name) { root = new Node(name); root.setFre(0); depth = 0; root.setEnd(false); root.setRoot(true); }
对于在字典树的新增元素的关键地方,应该就是我们要在何是停止,即新增完成的条件是什么?
我们元素新增完成的条件是,我们在对新增的元素(如:"12345")进行遍历,直到遍历到字符串的末尾。
在我们对新增的字符串str进行插入字典树的过程中,比如说已经遍历到位置i,如果str.chartAt(i)在字典树中已经存在,则我们可以直接pass,继续遍历i+1的位置;如果str.chartAt(i)在字典树中是不存在的,那么我们就必须新增此节点,再将新增的此节点挂载到上一个点的后面,然后继续遍历i+1位置上元素str.chartAt(i+1)。然后在插入的字符串最后的位置上设置此节点为结束节点(即在此位置可以构成单词,或是完整添加的数字)。具体代码如下:
public void insert(String number) { Node node = root; char[] numberCells = number.toCharArray(); for (int i = 0; i < numberCells.length; i++) { int num = Integer.parseInt(String.valueOf(numberCells[i])); if (node.getChildren()[num] != null) { if (numberCells.length < node.getChildren()[num].getMinLength()) { node.getChildren()[num].setMinLength(numberCells.length); } if (i == numberCells.length - 1) { Node endNode = node.getChildren()[num]; endNode.setFre(endNode.getFre() + 1); endNode.setEnd(true); } node.getChildren()[num].prefixCountIncrement(); } else { Node newNode = new Node(numberCells[i] + ""); newNode.setParent(node); if (i == numberCells.length - 1) { newNode.setFre(1); newNode.setEnd(true); newNode.setRoot(false); } newNode.setMinLength(numberCells.length); node.getChildren()[num] = newNode; depth = Math.max(i + 1, depth); } node = node.getChildren()[num]; } }
此功能的应用点在于,词频统计。我们在每次新增一个元素时都会在原来的基本上,对词频进行自增处理。如果新增的词在之前的字典树中是不存在的,就设置初始值为1,如果原本有这个节点,就在原来的词频上+1.在上一步(插入一个新的节点元素)中可以看到具体操作。那么这里介绍一下查询词频的操作。代码如下:
public int searchFre(String number) { int fre = -1; Node node = root; char[] numberCells = number.toCharArray(); for (int i = 0; i < numberCells.length; i++) { int num = Integer.parseInt(String.valueOf(numberCells[i])); if (node.getChildren()[num] != null) { node = node.getChildren()[num]; fre = node.getFre(); } else { fre = -1; break; } } return fre; }
对于前缀统计的操作,我们也需要在插入的过程中进行统计。这是因为,如果我们在插入的时候不进行统计,那么我们就必须在每次查询一个前缀的时候,去遍历前缀结束节点以下的所有子节点。这样势必会增加时间上的复杂度,是一种不理想的方式。不过,因为有时,我们并不会只是要求计算有多少以prefix为前缀的串。所以,可能遍历是在所难免。还是要看需求吧。以下代码是查询过程:
public int countPrefix(String prefix) { if (prefix == null || prefix.length() == 0) { return -1; } Node node = root; char[] letters = prefix.toCharArray(); for (int i = 0; i < prefix.length(); i++) { if (node.getChildren()[Integer.parseInt(String.valueOf(letters[i]))] == null) { return 0; } else { node = node.getChildren()[Integer.parseInt(String.valueOf(letters[i]))]; } } return node.getPrefixCount(); }
对于树深度的问题,对于其实际的应用点,我目前还未知晓。只是在写其他功能的时候想到了,就附带了吧。
这个深度,也是要在新增节点的时候去实时更新的。这样可以减小查询时的时间复杂度。查询代码如下:
public int depth() { return depth; }It's too easy, isn't it?
就以我们的数字字典树为例。因为我们在构造树的过程就是一个以字典序为基础的过程,所以我们的遍历就可以直接对树进行顺序遍历就Ok。对于字典树而言,顺序遍历的过程,其实就是对树的深度遍历。如果大家还记得深度遍历的过程,相信大家可以很容易地写出此代码。我的编码过程如下(使用了递归):
public void dictOrder(Node node, String prefix) { if (node != null) { if (node.isEnd()) { System.out.println(prefix + node.getName()); } for (Node children : node.getChildren()) { if (children == null) { continue; } dictOrder(children, prefix + (node.isRoot() ? "" : node.getName())); } } }
在第6步中,我们看到对树深度优先搜索的过程就是对树进行按字典序排序。那么可能你也会问另一个问题,那么是广度优先搜索又会是怎么样的结果呢?广度优先搜索的另一个叫法,我们可以说是对树的分层遍历。既然是对树进行分层,那么就是说"123"要排在"1234"的前面。而在第6步中我们也说到了,字典树本身就是以字典序为基础进行新增。也就是"123"必然是在"124"的前面。Ok,基于这样的分析,我们可以得到一个很容易理解的结论:对字典树进行广度优先搜索的过程就是对字典树进行按数值大小进行排序。具体的实现代码如下:
/** * 对数字字典树按实际数值大小排序(即分层打印) * TrieTree */ public void sortNumberOrder(Node node, String prefix) { Queue<Node> queuing = new LinkedList<Node>(); queuing.offer(node); while (!queuing.isEmpty()) { Node currentNode = queuing.poll(); if (currentNode.isEnd()) { System.out.println(getNodePath(currentNode)); } Node[] children = currentNode.getChildren(); for (Node sonNode : children) { if (sonNode != null) { queuing.offer(sonNode); } } } } /** * 获得某一节点的上层节点,即前缀字符串 * @param node * @return */ public String getNodePath(Node node) { StringBuffer path = new StringBuffer(); Node currentNode = node; while (currentNode.getParent() != null) { path.append(currentNode.getName()); currentNode = currentNode.getParent(); } return path.reverse().toString(); }
http://download.csdn.net/detail/u013761665/9186431