转自:http://book.51cto.com/art/201008/220537.htm
8.38 什么是字典树
字典树(Trie)和后缀树是单词处理的最流行数据结构。字典树于1960年由Fredkin作为搜索和排序数字数据的有效方法引入。名称Trie来自Information Retrieval(信息检索),这是一种特殊类型的树,它存储字符串并使其能够快速检索。如果我们仔细观察的话,就会注意到, 字典树不是别的什么东西,而是有向无环单词图(Directed Acyclic Word Graph,DAWG)。我们将在第9章中讲解这个术语。当我们需要频繁地从一组字符串中搜索一个字符串时,字典树是最佳数据结构的原因是,搜索时间为O(m),这里m是被搜索字符串的长度。搜索时间与集合中字符串的个数以及集合中最长字符串的长度无关。
在图8.33中,存储了单词BALK、 BALMY、BANAL、BANK和BANE。值得注意的是,这些字符串的一部分是相同的。这种树对于信息检索十分有用,并在下述领域得到应用。存在大量字典树的应用:
近似字符串匹配。
拼法检查软件。
动态键匹配(不同联机系统上的个人身份认证)。
动态散列。
冲突解析算法。
领导人选举算法。
IP 地址查找。
编码。
多项式因式分解。
Lempel-Ziv压缩方案,等等。
8.39 如何使用链表建立字典树的模型
字典树能够轻易地使用链表表示。这里,我们将表示一个字典树,它存储从特定字母开始的单词。这棵字典树的每一个结点都使用下述结构表示。Next是一个打算保存结点的26个可能孩子的地址。
- typedef struct TrieNode
- {
- char c;
- struct TrieNode* Next[26];
- }TrieNode;
8.40 如何向字典树中添加一个键
这个函数确定所传递的字符参数的整数等价值是什么。如果我们传递要确定的参数为"a",那么返回值为0;如果传递"z",那么返回值为25。
- int Determine(char c)
- {
- int i = 0;
- char alphabet[] ={"abcdefghijklmnopqrstuvwxyz"};
- for(i=0;i<26;i++)
- if(c==alphabet[i])
- return i;
- }
- void AddAKey(TrieNode *Root,char *Key)
- {
- int i = 0;
- int j = 0;
- int index = 0;
- for(i=1;i<strlen(Key);i++)
- {
- index = Determine(Key[i]);
- if(Root->Next[index]==NULL)
- {
- Root->Next[index] = (TrieNode *)malloc(sizeof(TrieNode));
- Root->Next[index]->c = Key[i];
- RootRoot = Root->Next[index];
- for(j=0;j<26;j++)
- Root->Next[j] = NULL;
- }
- else
- RootRoot = Root->Next[index];
- }
- }
8.41 如何在字典树中搜索键
有两种方法可用来搜索树,或者向函数传递要搜索的键,或者逐个地检查字符,后一种方法对于字典树的基本用途来说更加有用。下面我们将探索后一种方法。
- int Search(TrieNode *Root,char *Key)
- {
- enum {NOTFOUND,FOUND};
- int index = 0;
- if(Root==NULL)
- return NOTFOUND;
- else
- {
- for(int i=1;i<strlen(Key);i++)
- {
- index = Determine(Key[i]);
- if(Root->Next[index]!=NULL)
- RootRoot=Root->Next[index];
- else
- return NOTFOUND;
- }
- }
- return FOUND;
- }
示例8.3
编写一个程序,创建一个拥有少量单词的字典树,如图8.34所示,之后允许用户随着输入搜索键,并报告该键是否出现在字典树中。
解决方案
- int main()
- {
- int i = 0;
- char c;
- int flag = 0;
- TrieNode *Root=NULL, *Head = NULL;
- Root = (TrieNode *)malloc(sizeof(TrieNode));
- //这个字典树中的所有单词都以a开头
- Root->c = 'a';
- for(i=0;i<26;i++)
- Root->Next[i]=NULL;
- Head = Root;
- //在字典树中添加一些新的键
- AddAKey(Root,"admen");
- AddAKey(Root,"admix");
- AddAKey(Root,"admonish");
- AddAKey(Root,"adobe");
- AddAKey(Root,"adopt");
- AddAKey(Root,"adore");
- AddAKey(Root,"adorn");
- AddAKey(Root,"adult");
- AddAKey(Root,"adzes");
- AddAKey(Root,"aegis");
- AddAKey(Root,"aerie");
- AddAKey(Root,"affix");
- //图8.34显示了使用这些单词创建的字典树
- printf("%d\n",Search(Head,"adze"));
- printf("%c",Root->Next[Determine('d')]->Next[Determine('m')]
- ->Next[Determine('i')]->Next[Determine('x')]->c);
- puts("\nYour Key");
- //使用键搜索字典树
- c = getche();
- if(c=='a')
- {
- for(i=0;;i++)
- {
- c = getche();
- if(c==13)
- break;
- if(Head->Next[Determine(c)]!=NULL)
- {
- HeadHead = Head->Next[Determine(c)];
- continue;
- }
- else
- {
- puts("\nNo such key");
- flag = 1;
- break;
- }
- }
- if(flag==1)
- puts("\nNo Such Key");
- else
- puts("\nFound the Key");
- }
- else
- puts("\nNo Such Key");
- getch();
- return 0;
- }
图8.35展示了上述程序的几次运行结果。
(点击查看大图)图8.34 存储以"a"开头、少量单词的字典树 |
图8.35 上述程序的几次运行结果 |
一旦键入"n",程序就报告不存在这样的键。
另一次运行如图8.36所示。
图8.36 上述程序的另一次运行 |
"adz"是部分字符串,也就是说,它出现在字典树中。因此,搜索函数智能到即使子串出现在列表中也能找到。
8.42 如何知道字典树中的键能否被删除
为了从字典树中删除键,我们需要确定该键在字典树中是否存在,但仅仅这样还不够。即使该键在字典树中存在,我们也不一定要删除它,因为后一个结点可能是其他结点的父结点。
- int CanDeleteThisKey(TrieNode *Root,char *Key)
- {
- int index = 0;
- int flag = 0;
- int i = 0;
- int j = 0;
- //该键在字典树中不存在,
- //因此我们不能删除它
- if(Search(Root,Key)!=1)
- {
- return 0;
- }
- else
- {
- for(i=0;i<strlen(Key);i++)
- {
- index = Determine(Key[i]);
- if(Root->Next[index]!=NULL)
- RootRoot = Root->Next[index];
- else
- {
- flag = 1;
- break;
- }
- }
- if(flag == 0)
- {
- for(j=0;j<26;j++)
- {
- if(Root->Next[j]!=NULL)
- {
- flag=1;
- break;
- }
- }
- if(flag==1)
- return 0;
- else
- return 1;
- }
- }
- }
8.43 如何使用字典树进行拼法检查
我们都熟悉Microsoft Word的拼写检查特性,能够使用字典树设计这个特性。
这里是一个扫描单词字典树并对错误单词找出可能正确单词的函数,错误单词只有最后一个字母错误,比如单词"Fluctuatex",如图8.37所示。
图8.37 找出单词的正确拼写 |
- void DidYouMeanThese(TrieNode *Root,char *Key)
- {
- int i=0;
- int index = 0;
- char temp[25];
- strcpy(temp,Key);
- //该键不在字典树中
- if(Search(Root,Key)!=1)
- {
- for(i=0;i<strlen(Key);i++)
- {
- index = Determine(Key[i]);
- if(Root->Next[index]!=NULL)
- RootRoot = Root->Next[index];
- else
- break;
- }
- //现在我们到了这样的位置,
- //它没有孩子来补充完成这个单词
- for(int k=0;k<26;k++)
- {
- if(Root->Next[k]!=NULL)
- {
- for(i=0;i<strlen(Key)-1;i++)
- printf("%c",Key[i]); printf("%c\n",Root->Next[k]->c);
- }
- }
- }
- }
当函数依下述方式调用时:
- DidYouMeanThese(Root,"fluctuatex");
得到如图8.38所示的结果。
图8.38 单词fluctuatex的可能正确单词 |
自己试一试
试着改变或增强一下上述函数,突出字符串中拼写错误的位置。