一、前缀树介绍
(注:本节内容来源于网络)
定义:
所有含有公共前缀的字符串将挂在树中同一个结点下。实际上trie简明的存储了存在于串集合中的所有公共前缀。
假如有这样一个字符串集合X{bear,bell,bid,bull,buy,sell,stock,stop}。它的标准Trie树如下图:
如上图(蓝色圆形结点为内部结点,红色方形结点为外部结点),我们可以很清楚的看到字符串集合X构造的Trie树结构。其中从根结点到红色方框叶子节点所经历的所有字符组成的串就是字符串集合X中的一个串。
注意这里有一个问题: 如果X集合中有一个串是另一个串的前缀呢? 比如,X集合中加入串bi。那么上图的Trie树在绿色箭头所指的内部结点i 就应该也标记成红色方形结点。这样话,一棵树的枝干上将出现两个连续的叶子结点(这是不合常理的)。
也就是说字符串集合X中不存在一个串是另外一个串的前缀 。如何满足这个要求呢?我们可以在X中的每个串后面加入一个特殊字符$(这个字符将不会出现在字母表中)。这样,集合X{bear$、bell$、…. bi$、bid$}一定会满足这个要求。
总结:一个存储长度为n,来自大小为d的字母表中s个串的集合X的标准trie具有性质如下:
(1) 树中每个内部结点至多有d个子结点。
(2) 树有s个外部结点。
(3) 树的高度等于X中最长串的长度。
(4) 树中的结点数为O(n)。
标准 Trie树的查找:
对于英文单词的查找,我们完全可以在内部结点中建立26个元素组成的指针数组。如果要查找a,只需要在内部节点的指针数组中找第0个指针即可(b=第1个指针,随机定位)。时间复杂度为O(1)。
查找过程:假如我们要在上面那棵Trie中查找字符串bull (b-u-l-l)。
(1) 在root结点中查找第(‘b’-‘a’=1)号孩子指针,发现该指针不为空,则定位到第1号孩子结点处——b结点。
(2) 在b结点中查找第(‘u’-‘a’=20)号孩子指针,发现该指针不为空,则定位到第20号孩子结点处——u结点。
(3) … 一直查找到叶子结点出现特殊字符‘$’位置,表示找到了bull字符串
如果在查找过程中终止于内部结点,则表示没有找到待查找字符串。
效率:
对于有n个英文字母的串来说,在内部结点中定位指针所需要花费O(d)时间,d为字母表的大小,英文为26。由于在上面的算法中内部结点指针定位使用了数组随机存储方式,因此时间复杂度降为了O(1)。但是如果是中文字,下面在实际应用中会提到。因此我们在这里还是用O(d)。 查找成功的时候恰好走了一条从根结点到叶子结点的路径。因此时间复杂度为O(d*n)。
但是,当查找集合X中所有字符串两两都不共享前缀时,trie中出现最坏情况。除根之外,所有内部结点都自由一个子结点。此时的查找时间复杂度蜕化为O(d*(n^2))
二、前缀树的java实现
在算法中使用了java的map集合对象来构造树结构,使用map有如下优缺点:
优点:实现简单,操作简单
缺点:比较浪费内存空间。因为map对象在初始化时默认会分配16个entry空间,map对象有一个加载因子(load factor),加载因子的默认值是0.75,其意思是说当分配给map对象的内存空间占用率达到3/4时,map对象的内存空间会翻倍。例如分配给map对象的内存空间为16个entry,当内存空间占用达到12个entry时,map对象的内存空间就会变为32个entry。因此,map的这种机制在某些情况下会严重占用内存空间。可以增加加载因子来节省内存空间,但是这样会导致map的hash空间变小,那么hash时冲突就会增加,最后会影响到搜索的效率。因此,要根据具体的情况来选择合适的加载因子,以平衡空间利用率和效率。
打算做一个比较通用的字典树程序包,初步打算实现三种比较常用的trie树:前缀树、压缩树、后缀树。
设计类图如下所示:
(目前只初步实现了前缀树)
其代码如下所示:
[java]
view plain
copy
print
?
- package org.trie;
- public interface Trie {
- public void build(String dictName);
- public boolean search(String word);
- public boolean insert(String word);
- public boolean delete(String word);
- public void save(String path);
- public void read(String path);
- }
[java]
view plain
copy
print
?
- package org.trie.prefix;
- import java.util.Map;
- public class Node {
- private char c; //
- private int type; //
- private Map children; //
- public Node() {
- this.c = ‘ ‘;
- this.type = –1;
- this.children = null;
- }
- public char getC() {
- return c;
- }
- public void setC(char c) {
- this.c = c;
- }
- public int getType() {
- return type;
- }
- public void setType(int type) {
- this.type = type;
- }
- public void addChild(char c, Node child) {
- children.put(c + “”, child);
- }
- public Map getChildren() {
- return children;
- }
- public void setChildren(Map children) {
- this.children = children;
- }
- public boolean contains(char c) {
- if (children.containsKey(c + “”)) {
- return true;
- }
- return false;
- }
- public Node next(char c) {
- return children.get(c + “”);
- }
- public int sizeOfChildren() {
- return children.size();
- }
- public void removeChild(char c) {
- children.remove(c + “”);
- }
- }
children; // public Node() { this.c = ‘ ‘; this.type = -1; this.children = null; } public char getC() { return c; } public void setC(char c) { this.c = c; } public int getType() { return type; } public void setType(int type) { this.type = type; } public void addChild(char c, Node child) { children.put(c + “”, child); } public Map getChildren() { return children; } public void setChildren(Map children) { this.children = children; } public boolean contains(char c) { if (children.containsKey(c + “”)) { return true; } return false; } public Node next(char c) { return children.get(c + “”); } public int sizeOfChildren() { return children.size(); } public void removeChild(char c) { children.remove(c + “”); } }
[java]
view plain
copy
print
?
- package org.trie.prefix;
- import java.util.HashMap;
- public class BranchNode extends Node {
- public BranchNode(char c) {
- super.setC(c);
- super.setType(1);
- super.setChildren(new HashMap());
- }
- }
()); } }
[java]
view plain
copy
print
?
- package org.trie.prefix;
- public class LeafNode extends Node {
- public LeafNode() {
- super.setC(‘
);
- super.setType(1);
- }
- }
); super.setType(1); } }
[java]
view plain
copy
print
?
- package org.trie.prefix;
- import java.io.BufferedReader;
- import java.io.File;
- import java.io.FileReader;
- import org.trie.Trie;
- public class PrefixTrie implements Trie {
- private Node root;
- public PrefixTrie() {
- root = new BranchNode(‘ ‘);
- }
- public void build(String dictName) {
- BufferedReader reader = null;
- int i = 0;
- try {
- reader = new BufferedReader(new FileReader(new File(dictName)));
- String word = reader.readLine();
- while (word != null && !word.trim().equals(“”)) {
- i++;
- word += “$”;
- insert(word);
- word = reader.readLine();
- }
- System.out.println(“total words: “ + i);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (reader != null) {
- try {
- reader.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- }
- public boolean insert(String word) {
- if (search(word)) {
- System.out.println(“The word has already been added into the trie!”);
- return false;
- } else {
- char[] chars = word.toCharArray();
- Node currentNode = root;
- char c;
- for (int i = 0; i < chars.length; i++) {
- c = chars[i];
- if (c == ‘
) {
- Node leaf = new LeafNode();
- currentNode.addChild(‘
, leaf);
- }else{
- if (currentNode.contains(c)) {
- currentNode = currentNode.next(c);
- } else {
- Node branch = new BranchNode(c);
- currentNode.addChild(c, branch);
- currentNode = branch;
- }
- }
- }
- return true;
- }
- }
- public boolean delete(String word) {
- if(!search(word)){
- System.out.println(“No such word exists in the trie!”);
- return false;
- }else {
- char[] chars = word.toCharArray();
- Node formerNode = root;
- Node currentNode = root;
- char c;
- for (int i = 0; i < chars.length; i++) {
- c = chars[i];
- if (currentNode.sizeOfChildren() == 1) {
- formerNode.removeChild(currentNode.getC());
- }else if (c == ‘
) {
- currentNode.removeChild(c);
- }else {
- formerNode = currentNode;
- currentNode = currentNode.next(c);
- }
- }
- return true;
- }
- }
- public boolean search(String word) {
- char[] chars = word.toCharArray();
- Node currentNode = root;
- char c;
- for (int i = 0; i < chars.length; i++) {
- c = chars[i];
- if (c == ‘
&& currentNode.contains(c)) {
- return true;
- }else {
- if (currentNode.contains(c)) {
- currentNode = currentNode.next(c);
- } else {
- return false;
- }
- }
- }
- return false;
- }
- public void read(String path) {
- }
- public void save(String path) {
- }
- }
) { Node leaf = new LeafNode(); currentNode.addChild(‘, leaf); }else{ if (currentNode.contains(c)) { currentNode = currentNode.next(c); } else { Node branch = new BranchNode(c); currentNode.addChild(c, branch); currentNode = branch; } } } return true; } } public boolean delete(String word) { if(!search(word)){ System.out.println(“No such word exists in the trie!”); return false; }else { char[] chars = word.toCharArray(); Node formerNode = root; Node currentNode = root; char c; for (int i = 0; i < chars.length; i++) { c = chars[i]; if (currentNode.sizeOfChildren() == 1) { formerNode.removeChild(currentNode.getC()); }else if (c == ‘) { currentNode.removeChild(c); }else { formerNode = currentNode; currentNode = currentNode.next(c); } } return true; } } public boolean search(String word) { char[] chars = word.toCharArray(); Node currentNode = root; char c; for (int i = 0; i < chars.length; i++) { c = chars[i]; if (c == ‘&& currentNode.contains(c)) { return true; }else { if (currentNode.contains(c)) { currentNode = currentNode.next(c); } else { return false; } } } return false; } public void read(String path) { } public void save(String path) { } }
[java]
view plain
copy
print
?
- package org.trie.test;
- import java.io.File;
- import org.trie.Trie;
- import org.trie.prefix.PrefixTrie;
- public class PrefixTrieTest {
- public static void main(String[] args) {
- String path = new File(“”).getAbsolutePath() + File.separator + “dict//CoreDict.txt”;
- Trie trie = new PrefixTrie();
- trie.build(path);
- System.out.println(trie.search(“中华人民共和国$”));
- System.out.println(trie.delete(“中国$”));
- System.out.println(trie.search(“中国人$”));
- System.out.println(trie.search(“中$”));
- System.out.println(trie.search(“中国$”));
- }
- }
测试结果是正确的,但是可能没测试完全。
接口中有两个方法save和read暂时还没有想到怎么去实现,还有待研究。
三、算法可以改进的地方
初步实现的算法将每个单词的每个字符都存在了一个结点中,这无疑会非常浪费存储空间。
改进的方法:
可以将没有分支的单词后缀作为字符串存储在叶节点中,另如一个单词abcdefg$,如果在字符‘c’处分支后,后面没有其它单词与它拥有共同的前缀,那么就可以将“defg$”作为一个符串存在叶节点中。当插入一个单词abcdemn$时,可以将公共的de两个字符分出来分别存在分支结点中。
当然如果像这样改进,程序的数据结构及算法要做出相应的改变。