1、背景
词汇搜索、词频统计等字符串操作,是搜索引擎、文本处理系统等经常使用的业务,现在假设有这么一个简单的文本处理例子:有一篇10000个词的文章,要查出单词“was”在这篇文章中出现的次数。那么一般来说,没学过数据结构课程的读者可能会采用最简单但是最查找效率最低的穷举遍历法:读入整篇文章的词到一个字符串大数组中,然后一个一个地与“was”比较匹配。对于学习过数据结构课程的读者而言,不难想到数据结构的教材中介绍的2种经典的便于查找数据的结构——平衡查找二叉树和哈希表:对于平衡查找二叉树(或者是它的增强版-红黑树),以词汇作为关键字,其出现的次数作为键值,按平衡规则存入二叉树;对于哈希表,采用某种字符串哈希算法将散列出相同值(相同散列地址)的词汇存放在同一个哈希表项(或称为散列桶)中,以便查找。接着,简要分析一下三者的查找效率(假设词汇数为n,词汇平均长度为d):
(1)穷举法,很显然,要遍历n个词,每个词耗费的字符串比较时间是O(d),所以总时间复杂度为O(dn);
(2)平衡查找二叉树,查找一个词的时间为O(logn),则总查找时间复杂度为O(dlogn);
(3)哈希表,计算一个词的哈希值花时为O(d),假设所有词哈希出来的散列桶个数为m,词汇都平均地分布在这m个桶里,则可得总耗费的时间为O(d+n/m*d)=O(n/m*d)。
现在,介绍一种新的数据结构-字典树,它是专门用来进行文本的查存处理,且在文本的存储空间和查找效率上都优于平衡二叉树和哈希表。
2、概念
假设有一本英文字典,我们要查找某个单词,一般先在索引目录中查找该词的首字母构成的单词集合,然后在首字母的单词集合中查找第二个字母的子集合,以此类推,直到查找到整个单词,而字典树的构成和查找方式则与上述实体字典类似。
字典树(Tire),又称单词查找树,是一种树形结构,用于保存大量的字符串,它的优点是:利用字符串的公共前缀来节约存储空间,从而也能有效地提高查找效率。其基本性质有:
(1)根节点不包含字符,除根节点外每一个节点都只包含一个字符;
(2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
(3)每个节点的所有子节点包含的字符都不相同。
3、字典树的构建
从第二节的概念来理解,不难想到字典树的构建方法。比如有b,abc,abd,bcd,abcd,efg,hii 这6个单词,则所构建的字典树如下图:
从上图来看,对每一个节点而言,从根节点到它的路径就是一个单词,如果该节点被标记为红色则说明该单词存在。以第一节定义的时间单位为准,从查找效率来看,假设将文章读入字典树后,在每一个单词的末字母节点上标记上它出现的次数,则后续查找一个单词出现的总次数所花费的时间仅是O(d)。从存储空间上来看,第一节的三种结构都需要存储所有的单词,比如ab、abc、abde三个词需要存储所有单词的所有字母共9个字母,而字典树则可以利用相同前缀的单词共享前缀空间的特性,只需要存储5个字母。所以无论从时间效率还是空间容量来说,字典树对于大量字符串数据的处理都是优于一般的数据结构的。
字典树的基本操作有查找、插入。在字典树中搜索关键词的步骤为:
(1) 从根结点开始一次搜索;
(2) 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;
(3) 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。
(4) 迭代过程……
(5) 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找,其他操作类似处理。
一个简易字典树结构的操作简易JAVA简要代码如下:
import java.util.Scanner; class TrieNode{ boolean isWord; int count; TrieNode[] child; public TrieNode(){ child=new TrieNode[26]; isWord=false; } } public class Trie { TrieNode root; /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Scanner sc=new Scanner(System.in); Trie trie=new Trie(); int n=sc.nextInt();//建立字典树,输入当前字典树的字符串个数 sc.nextLine(); while(n!=0){ n--; trie.insert(sc.nextLine()); } int m=sc.nextInt();//输入要查找字符串的个数 sc.nextLine(); while(m!=0){ m--; System.out.println(trie.find(sc.nextLine())); } } public Trie(){ root=new TrieNode(); } public void insert(String s){ TrieNode tmp=root; for(char ch:s.toCharArray()){ int index=ch-'a'; if(tmp.child[index]==null){ tmp.child[index]=new TrieNode(); } tmp.count++; tmp=tmp.child[index]; } tmp.isWord=true;//标记是否是完整的单词 tmp.count++; } //如果查找到字符串则返回字典树中字符串的个数,否则则返回0 public int find(String s){ TrieNode tmp=root; for(char ch:s.toCharArray()){ int index=ch-'a'; if(tmp.child[index]==null){ return 0; } tmp=tmp.child[index]; } return tmp.count; } }