`

【串和序列处理 3】Trie Tree 串集合查找

阅读更多

Trie 树, 又称字典树,单词查找树。它来源于retrieval(检索)中取中间四个字符构成(读音同try)。用于存储大量的字符串以便支持快速模式匹配。主要应用在信息检索领域。

 

Trie 有三种结构: 标准trie (standard trie)、压缩trie、后缀trie(suffix trie) 最后一种将在《字符串处理4:后缀树》中详细讲,这里只将前两种。

 

 

1. 标准Trie (standard 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=第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))

       

 

标准 Trie树的Java代码实现:

 

package net.hr.algorithm.stroper;

import java.util.ArrayList;

enum NodeKind{LN,BN};
/**
 * Trie结点
 */
class TrieNode{

	char key;
	TrieNode[] points=null;
	NodeKind kind=null;
}
/**
 * Trie叶子结点
 */
class LeafNode extends TrieNode{
	
	LeafNode(char k){
		super.key=k;
		super.kind=NodeKind.LN;
	}
}
/**
 * Trie内部结点
 */
class BranchNode extends TrieNode{
	
	BranchNode(char k){
		super.key=k;
		super.kind=NodeKind.BN;
		super.points=new TrieNode[27];
	}
}
/**
 * Trie树
 * @author heartraid
 */
public class StandardTrie {

	private TrieNode root=new BranchNode(' ');
	
	/**
	 * 想Tire中插入字符串
	 */
	public void insert(String word){
		
		//System.out.println("插入字符串:"+word);
		//从根结点出发
		TrieNode curNode=root;
		//为了满足字符串集合X中不存在一个串是另外一个串的前缀 
		word=word+"$";
		//获取每个字符
		char[] chars=word.toCharArray();
		//插入
		for(int i=0;i<chars.length;i++){
			//System.out.println("   插入"+chars[i]);	
			if(chars[i]=='$'){
				curNode.points[26]=new LeafNode('$');
			//	System.out.println("   插入完毕,使当前结点"+curNode.key+"的第26孩子指针指向字符:$");
			}
			else{
				int pSize=chars[i]-'a';
				if(curNode.points[pSize]==null){
					curNode.points[pSize]=new BranchNode(chars[i]);
				//	System.out.println("   使当前结点"+curNode.key+"的第"+pSize+"孩子指针指向字符: "+chars[i]);
					curNode=curNode.points[pSize];
				}
				else{
				//	System.out.println("   不插入,找到当前结点"+curNode.key+"的第"+pSize+"孩子指针已经指向字符: "+chars[i]);
					curNode=curNode.points[pSize];
				}
			}
		}
	}
	/**
	 * Trie的字符串全字匹配
	 */
	public boolean fullMatch(String word){
		//System.out.print("查找字符串:"+word+"\n查找路径:");
		//从根结点出发
		TrieNode curNode=root;
		//获取每个字符
		char[] chars=word.toCharArray();
		
		for(int i=0;i<chars.length;i++){
			if(curNode.key=='$'){
				System.out.println('&');
			//	System.out.println(" 【成功】");
				return true;
			}else{
				System.out.print(chars[i]+" -> ");	
				int pSize=chars[i]-'a';
				if(curNode.points[pSize]==null){
				//	System.out.println(" 【失败】");
					return false;
				}else{
					curNode=curNode.points[pSize];
				}
			}
		}
	//	System.out.println("  【失败】");
		return false;
	}
	

	/**
	 * 先根遍历Tire树
	 */
	private void preRootTraverse(TrieNode curNode){
		
		if(curNode!=null){
			System.out.print(curNode.key+" ");
			if(curNode.kind==NodeKind.BN)
				for(TrieNode childNode:curNode.points)
					preRootTraverse(childNode);
		}
		
	}
	/**
	 * 得到Trie根结点
	 */
	public TrieNode getRoot(){
		return this.root;
	}
	/**
	 * 测试
	 */
	public static void main(String[] args) {
		
		StandardTrie trie=new StandardTrie();
		trie.insert("bear");
		trie.insert("bell");
		trie.insert("bid");
		trie.insert("bull");
		trie.insert("buy");
		trie.insert("sell");
		trie.insert("stock");
		trie.insert("stop");
		
		trie.preRootTraverse(trie.getRoot());
		
		trie.fullMatch("stoops");
	}
}

 

中文词语的 标准 Trie树

      由于中文的字远比英文的26个字母多的多。因此对于trie树的内部结点,不可能用一个26的数组来存储指针。如果每个结点都开辟几万个中国字的指针空间。估计内存要爆了,就连磁盘也消耗很大。

 

      一般我们采取这样种措施:

     (1) 以词语中相同的第一个字为根组成一棵树。这样的话,一个中文词汇的集合就可以构成一片Trie森林。这篇森林都存储在磁盘上。森林的root中的字和root所在磁盘的位置都记录在一张以Unicode码值排序的有序字表中。字表可以存放在内存里。

    (2) 内部结点的指针用可变长数组存储。

 

     特点:由于中文词语很少操作4个字的,因此Trie树的高度不长。查找的时间主要耗费在内部结点指针的查找。因此将这项指向字的指针按照字的Unicode码值排序,然后加载进内存以后通过二分查找能够提高效率。

 

 

标准Trie树的应用和优缺点

     (1) 全字匹配:确定待查字串是否与集合的一个单词完全匹配。如上代码fullMatch()。

     (2) 前缀匹配:查找集合中与以s为前缀的所有串。

 

     注意:Trie树的结构并不适合用来查找子串。这一点和前面提到的PAT Tree以及后面专门要提到的Suffix Tree的作用有很大不同。

 

      优点: 查找效率比与集合中的每一个字符串做匹配的效率要高很多。在o(m)时间内搜索一个长度为m的字符串s是否在字典里。

      缺点:标准Trie的空间利用率不高,可能存在大量结点中只有一个子结点,这样的结点绝对是一种浪费。正是这个原因,才迅速推动了下面所讲的压缩trie的开发。

 

 

 

2. 压缩Trie (compressed trie)

 

      压缩Trie类似于标准Trie,但它能保证trie中的每个内部结点至少有两个子节点(根结点除外)。通过把单子结点链压缩进叶子节点来执行这个规则。

 

压缩Trie的定义

 

      冗余结点(redundant node):如果T的一个非根内部结点v只有一个子结点,那么我们称v是冗余的。

      冗余链(redundant link):如上标准Trie图中,内部结点e只有一个内部子结点l,而l也只有一个叶子结点。那么e-l-l就构成了一条冗余链。

      压缩(compressed):对于冗余链 v1- v2- v3- ... -vn,我们可以用单边v1-vn来替代。

 

      对上面标准Trie的图压缩之后,形成了Compressed Trie的字符表示图如下:

压缩Trie的性质和优势:

 

     与标准Trie比较,压缩Trie的结点数与串的个数成正比了,而不是与串的总长度成正比。一棵存储来自大小为d的字母表中的s个串的结合T的压缩trie具有如下性质:

 

     (1) T中的每个内部结点至少有两个子结点,至多有d个子结点。

     (2) T有s个外部结点。

     (3) T中的结点数为O(s)


     存储空间从标准Trie的O(n)降低到压缩后的O(s),其中n为集合T中总字符串长度,s为T中的字符串个数。

 

压缩Trie的压缩表示

 

     上面的图是压缩Trie的字符串表示。相比标准Trie而言,确实少了不少结点。但是细心的读者会发现,叶子结点中的字符数量增加了,比如结点ell,那么这种压缩空间的效率当然会打折扣了。那么有什么好办法呢,这里我们介绍一种压缩表示方法。即把所有结点中的字符串用三元组的形式表示如下图:

 

   

      其中三元组(i,j,k)表示S[i]的从第j个位置到第k个位置间的子串。比如(5,1,3,)表示S[5][1...3]="ell"。

 

      这种压缩表示的一个巨大的优点就是:无论结点需要存储多长的字串,全部都可以用一个三元组表示,而且三元组所占的空间是固定有限的。但是为了做到这一点,必须有一张辅助索引结构(如上图右侧s0—s7所示)。   

 

9
0
分享到:
评论
2 楼 ChenxofHit 2011-10-09  
[img][/img][url][/url][flash=200,200][/flash]
引用
1 楼 Heart.X.Raid 2011-08-28  
public boolean fullMatch(String word) {
		// System.out.print("查找字符串:"+word+"\n查找路径:");
		// 从根结点出发
		TrieNode curNode = root;
		// 获取每个字符
		char[] chars = word.toCharArray();

		for (int i = 0; i < chars.length; i++) {
			int pSize = chars[i] - 'a';
			if (curNode.points[pSize] == null) {
				System.out.println(" 【失败】");
				return false;
			} else {
				curNode = curNode.points[pSize];
				System.out.print(curNode.key + " -> ");
			}
		}
		if (curNode.points[26] != null) {
			System.out.print('$');
			System.out.println(" 【成功】");
			return true;
		} else {
			System.out.println(" 【失败】仅为Trie中的某个前缀");
			return false;
		}
	}  

Trie的字符串全字匹配方法的修改

相关推荐

    POJ2513-Colored Sticks【TrieTree+MergeSet+EulerPath】

    在该问题中,Trie树可能用于处理颜色字符串的集合,通过将每个字符串插入到Trie树中,可以快速地进行前缀匹配查询,例如查找具有相同前缀的颜色序列。插入操作可以保持树的有序性,便于后续的遍历和查询。 【Merge...

    数据结构字母字符串操作

    在字符串处理中,常见的数据结构还包括栈(Stack)、队列(Queue)、哈希表(Hash Table)和树(Tree)。栈可以用于回文检测,通过入栈和出栈操作对比字符;队列在处理字符串队列输入输出时发挥作用;哈希表则用于...

    10 深入学习字符串.zip

    9. **字符串在数据结构中的应用**:如Trie树(字典树)和suffix tree(后缀树)等,用于快速查找和操作字符串集合。 10. **正则表达式**:正则表达式是一种强大的文本处理工具,用于模式匹配和提取。学习正则表达式...

    字符串算法

    6. **Trie(字典树)**:Trie是一种前缀树结构,用于高效地存储和检索字符串集合。每个节点代表一个字符串前缀,从根节点到某个节点的路径表示一个完整的字符串。Trie常用于关键词搜索、自动补全等功能。 7. **...

    go-succinct-data-structure-trie:Trie的简洁数据结构,用Go编写

    总的来说,了解和掌握Trie数据结构对于任何IT专业人员都是有益的,特别是在需要高效处理字符串集合或实现自动补全功能的场景下。Go语言提供的简洁语法和强大的类型系统使得实现这样的数据结构变得更加直观和易于维护...

    patricia tree

    PAT树,又称PATricia树,是一种特殊的字典树或前缀树(Trie),在信息检索、文本处理和数据压缩等领域有广泛应用。它的设计目的是为了更有效地存储和查找具有共同前缀的字符串。PAT树的主要特点是它通过合并共享相同...

    data structures for text sequences.zip

    一、字符串和字符数组 在Java中,最基础的文本数据结构是字符串(String)。字符串是由Unicode字符组成的不可变序列,Java提供了丰富的API进行字符串操作。字符数组(char[])则是另一种常见表示文本的方式,它允许直接...

    Trie树(字典树)的介绍及Java实现

    Trie树,也被称为字典树或前缀树,是一种数据结构,主要用于高效地存储和检索字符串集合中的数据。在Trie树中,每个节点代表一个字符串的前缀,从根节点到某个节点的路径上的字符序列构成了该节点对应的字符串。这种...

    ACM算法类型大纲 思维导图全

    9. Trie树:用于存储字符串集合的数据结构,支持快速查找和插入。 10. 可持久化Trie:支持历史版本查询的Trie树实现。 【数学和数论】 1. 计算几何基础:包括点、线、面的性质和基本操作,如点积、叉积、凸包等。 ...

    99%的海量数据处理面试题

    3. **分布式处理**:对于单机处理能力有限的情况,可以采用集群进行分布式处理,通过并行计算分担任务,考虑节点间的通信和数据交互。例如,Hadoop和MapReduce是常见的分布式处理框架。 在处理海量数据时,常见的...

    海量数据处理面试题.pdf

    双层桶划分是另一种处理大数据分布情况的技术,它将数据分为两层进行统计和处理,有助于提升大规模数据处理的效率。 总结而言,海量数据处理在现代IT行业中的地位愈发重要,它要求面试者不仅要掌握数据结构和算法...

    数据结构算法演示

    字符串处理包括模式匹配、字符串搜索、编辑距离计算等,常用的字符串数据结构有Trie树和后缀数组。 8. 图(Graph):图由节点(或顶点)和连接这些节点的边构成,可以用来表示各种关系,如社交网络、道路网络等。图...

    ACM学习资料

    在ACM竞赛中,这可能涉及到字符串处理、哈希表或字典树(Trie)的应用。字典树是一种高效的数据结构,用于存储字符串集合,并支持快速的前缀匹配和插入操作。通过构建字典树,可以高效地进行关键词查找和统计,尤其...

    Prefix Tree:快速的前缀树和DAWG在C和python中的实现-开源

    前缀树,也被称为Trie或字典树,是一种数据结构,主要用于存储字符串集合,它以一种高效的方式支持前缀查询。在这个开源项目中,前缀树被实现为C和Python两种语言的库,用于执行诸如最长前缀匹配、代码补全、字典...

    AC自动机.pdf

    ### AC自动机详解 #### 一、AC自动机概述 AC自动机(Aho-Corasick Automaton)是一种...与传统的模式匹配算法相比,AC自动机不仅具有较高的匹配效率,还能一次性处理多个模式串,非常适合于大规模文本搜索和处理场景。

    数据结构的思维导图-全

    常见的字符串操作包括拼接、查找、替换等,字符串处理的算法在文本处理和自然语言处理领域广泛应用。 这些思维导图可能涵盖了上述各种数据结构的概念、操作、优缺点以及应用场景,帮助学习者理解和记忆。通过深入...

    数据结构&算法1

    并查集用于处理集合合并与查询,而Trie树是字符串查找的高效工具。AVL树和红黑树是自平衡二叉查找树,能保证操作的平均时间复杂度为O(log n)。 算法方面,分支迭代(branch iteration)、递归(recursion)和搜索...

    收集的 23 个树形结构

    6. **Trie(字典树)**:用于快速查找字符串,每个节点代表一个字符串前缀,根节点代表空字符串。 7. **BST(二叉搜索树)**:二叉树的一种,其中每个节点的值大于其左子树中的任何节点值,小于其右子树中的任何...

    算法模板.pdf

    - 字典树(Trie):一种用于快速检索字符串数据集中的键的技术。 - AC自动机:一种用于多模式匹配的字符串搜索算法。 6. 线段树 - 线段树是一种可用于存储区间或线段的树形数据结构,支持快速查询。 - 点更新:线段...

    Java常用算法手册源码

    - **Trie树(Prefix Tree)**:一种字符串检索数据结构,用于快速查找以特定前缀开头的字符串。 - **Manacher's Algorithm**:在线性时间内找出字符串中的所有回文子串。 6. **数据结构** - **栈(Stack)**:...

Global site tag (gtag.js) - Google Analytics