字典树简介和简易应用

(2010-03-13)


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) 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找,其他操作类似处理。一个简易字典树结构的操作简要代码如下:

/***************************************************

Name: Trie树的基本实现 
Description: Trie树的基本实现,包括查找、插入和删除操作

***************************************************/
#include<algorithm>
#include<iostream>
using namespace std;

const int sonnum=26,base='a';
struct Trie
{
    int num;//记录多少单词途径该节点,即多少单词拥有以该节点为末尾的前缀
    bool terminal;//若terminal==true,该节点没有后续节点
    int count;//记录单词的出现次数,此节点即一个完整单词的末尾字母
    struct Trie *son[sonnum];//后续节点
};
/*********************************
创建一个新节点
*********************************/
Trie *NewTrie()
{
    Trie *temp=new Trie;
    temp->num=1;
    temp->terminal=false;
    temp->count=0;
    for(int i=0;i<sonnum;++i)temp->son[i]=NULL;
    return temp;
}
/*********************************
插入一个新词到字典树
pnt:树根
s  :新词
len:新词长度
*********************************/
void Insert(Trie *pnt,char *s,int len)
{
    Trie *temp=pnt;
    for(int i=0;i<len;++i)
    {
        if(temp->son[s[i]-base]==NULL)temp->son[s[i]-base]=NewTrie();
        else {temp->son[s[i]-base]->num++;temp->terminal=false;}
        temp=temp->son[s[i]-base];
    }
    temp->terminal=true;
    temp->count++;
}
/*********************************
删除整棵树
pnt:树根
*********************************/
void Delete(Trie *pnt)
{
    if(pnt!=NULL)
    {
        for(int i=0;i<sonnum;++i)if(pnt->son[i]!=NULL)Delete(pnt->son[i]);
        delete pnt; 
        pnt=NULL;
    }
}
/*********************************
查找单词在字典树中的末尾节点
pnt:树根
s  :单词
len:单词长度
*********************************/
Trie* Find(Trie *pnt,char *s,int len)
{
    Trie *temp=pnt;
    for(int i=0;i<len;++i)
        if(temp->son[s[i]-base]!=NULL)temp=temp->son[s[i]-base];
        else return NULL;
    return temp;
}

上述字典树节点结构Tire里的成员除了后续节点组son之外,都是节点存储的信息,设计者可以根据需求进行修改。

4、应用

        从以上几节可以看出,字典树的应用主要用于海量字符串数据的统计分析,以下列举了两个简单的字典树应用例子:
(1)有一个简单的搜索引擎,会将用户每天输入的检索词进行保存,并定期进行统计,查出频率最高的几个检索词。其实这就是将第一节的例子稍微实例化了一点,在搜索引擎后台建立一个字典树,在用户输入检索词的时候,如果该词不在字典树中将该词插入字典树,否则在该词的末节点的次数存储标记加1,则在定期统计时,计算各个节点的存储标记次数即可得检索频率最高的词了。
(2)给定N个单词,能以较小的时间复杂度将这N个单词按字典序排序。如果将这N个单词读入普通的字符串数组,则排序的时候使用普通排序或快速排序所耗费的时间为O(N^2)或O(NlogN);如果将这N个单词读入字典树(节点的子节点字符值从左到右为字典序),则采用对字典树的前序遍历就能输出排好序的单词表,时间复杂度为O(Nd)。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值