Trie树,又称字典树,单词查找树。它来源于retrieval(检索)中取中间四个字符构成的。用于存储大量的字符串以便支持快速模式匹配。主要应用在信息检索领域。
Trie有三种结构:标准Trie(standard trie),压缩Trie,后缀Trie(suffix trie)。
1、标准Trie
标准Trie树的结构:所有含有公共前缀的字符串将挂在树中同一个结点下。实际上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 = 第一个指针,随机定位)。时间复杂度为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*(n2))
package trie; enum NodeKind{LN,BN}; /** * Trie node * @author Ning * */ class TrieNode { char key; TrieNode[] points = null; NodeKind kind = null; } /** * Branch node */ class BranchNode extends TrieNode { public BranchNode(char k) { super.key = k; super.kind = NodeKind.BN; super.points = new TrieNode[27]; // TODO Auto-generated constructor stub } } /** * Leaf node */ class LeafNode extends TrieNode { public LeafNode(char key) { super.key = key; super.kind = NodeKind.LN; // TODO Auto-generated constructor stub } } public class StandarTrie { //Create root node TrieNode root = new BranchNode(' '); //向字典树中插入一个单词 public void insert(String words) { TrieNode curNode = root; // add '#' as an end symbol words = words + "#"; char[] chars = words.toCharArray(); for(int i = 0;i < chars.length;i++) { if(chars[i] == '#') { curNode.points[26] = new LeafNode('#'); } else { int pSize = chars[i] - 'a'; // 如果不存在,创建一个新的分支节点 if(curNode.points[pSize] == null) { curNode.points[pSize] = new BranchNode(chars[i]); } curNode = curNode.points[pSize]; } } } //检查一个单词是否在字典树中 public boolean fullMatch(String words) { TrieNode curNode = root; char[] chars = words.toCharArray(); for (int i = 0; i < chars.length; i++) { int pSize = chars[i] - 'a'; System.out.println(chars[i]+"->"); if(curNode.points[pSize] == null) return false; curNode = curNode.points[pSize]; } if(curNode.points[26] != null && curNode.points[26].key == '#') return true; return false; } //前序遍历 public void preorderTraverse(TrieNode curNode) { if(curNode != null) { System.out.println(curNode.key); if(curNode.kind == NodeKind.BN) { for(TrieNode node : curNode.points) { preorderTraverse(node); } } else { System.out.println(); } } } //获取root public TrieNode getRoot() { return this.root; } }
后面的两种字典树,后续继续更新!