字符串处理——字典树
【概述】
字典樹(shù),又稱為單詞查找樹(shù),Tire 樹(shù),是一種樹(shù)形結(jié)構(gòu),它是哈希樹(shù)的變種。
字典樹(shù)與字典很相似,當(dāng)要查一個(gè)單詞是不是在字典樹(shù)中,首先看單詞的第一個(gè)字母是不是在字典的第一層,如果不在,說(shuō)明字典樹(shù)里沒(méi)有該單詞,如果在就在該字母的孩子節(jié)點(diǎn)里找是不是有單詞的第二個(gè)字母,沒(méi)有說(shuō)明沒(méi)有該單詞,有的話用同樣的方法繼續(xù)查找,以此類推。
基本性質(zhì):
- 字典樹(shù)用邊表示字符
- 有相同前綴的單詞共用前綴結(jié)點(diǎn)
- 根節(jié)點(diǎn)不包含字符
- 每個(gè)單詞結(jié)束的時(shí)候都用一個(gè)特殊字符表示,圖中用的紅色 $ 符
- 從根節(jié)點(diǎn)到一個(gè)紅點(diǎn)所經(jīng)過(guò)的所有邊的字母就是一個(gè)字符串
應(yīng)用:其常用于統(tǒng)計(jì)、排序和保存大量的字符串(不僅限于字符串),所以經(jīng)常被搜索引擎系統(tǒng)用于文本詞頻統(tǒng)計(jì)。
優(yōu)點(diǎn):其利用字符串的公共前綴來(lái)節(jié)約存儲(chǔ)空間,最大限度地減少無(wú)謂的字符串比較,查詢效率比哈希表高。?
【構(gòu)建過(guò)程】
1.插入
1)思路
對(duì)于字典樹(shù),從左到右掃描一個(gè)單詞,若字母在相應(yīng)根節(jié)點(diǎn)下沒(méi)有出現(xiàn),就插入這個(gè)字母,若出現(xiàn)過(guò),則沿著樹(shù)走下去,看單詞的下一個(gè)字母。
那么此時(shí)產(chǎn)生一個(gè)問(wèn)題,對(duì)于從左到右掃描的這個(gè)單詞,若字母在相應(yīng)根節(jié)點(diǎn)下沒(méi)有出現(xiàn)過(guò),如何去選擇位置插入?計(jì)算機(jī)不會(huì)自動(dòng)選擇位置,因此需要給他指定一個(gè)位置,這樣就需要對(duì)每個(gè)字母進(jìn)行編號(hào)。
設(shè)數(shù)組 tire[i][j]=k,表示編號(hào)為 i 的結(jié)點(diǎn)的第 j 個(gè)孩子是編號(hào)為 k 的結(jié)點(diǎn),此時(shí)有兩種編號(hào):
- i、k:表示結(jié)點(diǎn)位置的編號(hào),相對(duì)整棵樹(shù)而言,此處相同字母編號(hào)不同
- j:表示幾點(diǎn) i 的第 j 個(gè)孩子,相對(duì)結(jié)點(diǎn) i 而言,此處編碼應(yīng)按照 ASCLL 碼來(lái)編,用到哪個(gè)編哪個(gè),相同字母編號(hào)相同
以單詞 cat,cash,app,apple,aply,ok 為例,按照輸入順序?qū)ζ溥M(jìn)行編號(hào)?
第一種編號(hào)結(jié)果:
第二種編號(hào)結(jié)果:
經(jīng)過(guò)這樣的編號(hào)后,這樣數(shù)組?trie[i][j]=k,表示編號(hào)為 i 的節(jié)點(diǎn)的第 j 個(gè)孩子是編號(hào)為 k 的節(jié)點(diǎn),那么第一種編號(hào)即為 i、k,第二種編號(hào)即為 j,從而可以實(shí)現(xiàn)插入
2)實(shí)現(xiàn)
int tot=0;//編號(hào) int trie[N][26];//字典樹(shù) int val[N];//字符串結(jié)尾標(biāo)記,val[i]=x表示第i個(gè)節(jié)點(diǎn)的權(quán)值為x void insert(char *s){//插入單詞sint len=strlen(s);//單詞s的長(zhǎng)度int root=0;//字典樹(shù)上當(dāng)前匹配到的結(jié)點(diǎn)for(int i=0;i<len;i++){int id=s[i]-'a';//子節(jié)點(diǎn)編號(hào)if(trie[root][id]==0)//如果之前沒(méi)有從root到id的前綴 trie[root][id]=++tot;//插入root=trie[root][id];//順著字典樹(shù)往下走}val[root]=n; }2.查找
1)思路
查找有很多種,可以查找某一個(gè)前綴,也可以查找整個(gè)單詞。一般來(lái)說(shuō),對(duì)于一個(gè)單詞,從左到右依次掃描每個(gè)字母,順著字典樹(shù)往下找,能找到這個(gè)字母,往下走,否則結(jié)束查找,即沒(méi)有這個(gè)單詞;單詞掃完了,表示有這個(gè)單詞。
如果是查詢某個(gè)單詞是否在字典樹(shù)中的話,可用布爾變量 vis[i] 表示節(jié)點(diǎn) i 是否是單詞結(jié)束的標(biāo)志,那么返回的是 vis[root],所以在插入操作中插入完每個(gè)單詞時(shí),要對(duì)單詞最后一個(gè)字母的 vis[i] 置為 true
如果是查詢前綴出現(xiàn)的次數(shù)的話,那就在開(kāi)一個(gè)數(shù)組 sum[i],表示位置 i 被訪問(wèn)過(guò)的次數(shù),那么最后返回的是 sum[root],所以插入操作中每訪問(wèn)一個(gè)節(jié)點(diǎn),都要讓他的 sum++。在這里,前綴的次數(shù)是標(biāo)記在前綴的最后一個(gè)字母所在位置的后一個(gè)位置上。
例如:前綴 abc 出現(xiàn)的次數(shù)標(biāo)記在 c 所在位置的后一個(gè)位置上
2)實(shí)現(xiàn)
bool find(char *s){//查詢單詞是否在樹(shù)中int len=strlen(s);//單詞長(zhǎng)度int root=0;//從根結(jié)點(diǎn)開(kāi)始找for(int i=0;s[i];i++){int x=s[i]-'a';if(trie[root][x]==0)//以root為頭結(jié)點(diǎn)的x字母不存在return false;root=trie[root][x];//為查詢下個(gè)字母做準(zhǔn)備,往下走 }return val[root];//找到 }3.刪除
1)思路
對(duì)于一個(gè)單詞,如果要在樹(shù)中將其刪除,有三種情況:
- 沒(méi)找到這個(gè)單詞
- 找到葉節(jié)點(diǎn)的時(shí),葉節(jié)點(diǎn)的 cnt 標(biāo)志清零,代表不是葉節(jié)點(diǎn)
- 當(dāng)前節(jié)點(diǎn)沒(méi)有其他孩子節(jié)點(diǎn)的時(shí),可直接刪除這個(gè)節(jié)點(diǎn)
2)實(shí)現(xiàn)
void del(char *str,int word){//word為要?jiǎng)h除的單詞的標(biāo)號(hào),一般為seach("單詞");int len=strlen(str);int root=0;if(word==0)//沒(méi)找到單詞return;for(int i=0;i<len;i++){int x=str[i]-'a';if(tire[root][x]==0)//沒(méi)有子節(jié)點(diǎn)return;sum[root]-=cnt;//減去前綴root=tire[root][x];}sum[root]=0;//前綴清零for(int i=0;i<26;i++)//刪除節(jié)點(diǎn)tire[root][i]=0; }【模版】
1.查詢單詞/前綴是否出現(xiàn)
int tot; int trie[N][26];//trie[rt][x]=tot,root是上個(gè)節(jié)點(diǎn)編號(hào),x是字母,tot是下個(gè)節(jié)點(diǎn)編號(hào) //bool vis[N];//查詢整個(gè)單詞用 void insert(char *s,int root){for(int i=0;s[i];i++){int x=s[i]-'a';if(trie[root][x]==0)//現(xiàn)在插入的字母在之前同一節(jié)點(diǎn)處未出現(xiàn)過(guò) trie[root][x]=++tot;//字母插入一個(gè)新的位置,否則不做處理 root=trie[root][x];//為下個(gè)字母的插入做準(zhǔn)備 }//vis[root]=true;//標(biāo)志該單詞末位字母的尾結(jié)點(diǎn),在查詢整個(gè)單詞時(shí)用 } bool find(char *s,int root){for(int i=0;s[i];i++){int x=s[i]-'a';if(trie[root][x]==0)return false;//以root為頭結(jié)點(diǎn)的x字母不存在,返回0 root=trie[root][x];//為查詢下個(gè)字母做準(zhǔn)備 }return true;//return vis[root];//查詢整個(gè)單詞時(shí) }int main(){int n,m;char s[22];tot=1;cin>>n;//插入單詞個(gè)數(shù)for(int i=1;i<=n;i++){cin>>s;insert(s,1);}cin>>m;//查詢單詞個(gè)數(shù)for(int i=1;i<=n;i++){cin>>s;if(find(s,1))printf("YES\n");else printf("NO\n");}return 0; }2.查詢前綴出現(xiàn)次數(shù)
int trie[400001][26],tot; int sum[400001]; void insert(char *s){int root=0;int len=strlen(s);for(int i=0;i<len;i++){int id=s[i]-'a';if(!trie[root][id]) trie[root][id]=++tot;sum[trie[root][id]]++;//前綴保存 root=trie[root][id];} } int search(char *s){int root=0;int len=strlen(s);for(int i=0;i<len;i++){//root經(jīng)過(guò)循環(huán)后變成前綴最后一個(gè)字母所在位置int id=s[i]-'a';if(!trie[root][id]) return 0;root=trie[root][id];}return sum[root]; } int main(){int n,m;char s[11];tot=1;cin>>n;//插入單詞個(gè)數(shù)for(int i=1;i<=n;i++){cin>>s;insert(s);}cin>>m;//查詢次數(shù)for(int i=1;i<=m;i++){cin>>s;printf("%d\n",search(s));} }3.結(jié)構(gòu)體實(shí)現(xiàn)的增刪查字典樹(shù)
struct Node{int sum;//前綴int next[26];//子節(jié)點(diǎn)void init(){sum=0;memset(next,-1,sizeof next);} }tire[N]; int tot; void insert(char *str){int len=strlen(str);int root=0;for(int i=0;i<len;i++){int x=str[i]-'a';if(tire[root].next[x]==-1)tire[root].next[x]=tot++;root=tire[root].next[x];tire[root].sum++;} } int search(char *str){int len=strlen(str);int root=0;for(int i=0;i<len;i++){int x=str[i]-'a';if(tire[root].next[x]==-1)return 0;root=tire[root].next[x];}return tire[root].sum; } void del(char *str,int word){int len=strlen(str);int root=0;if(word<0)return;for(int i=0;i<len;i++){int x=str[i]-'a';if(tire[root].next[x]==-1)return;tire[root].sum-=word;root=tire[root].next[x];}tire[root].sum=0;for(int i=0;i<26;i++)tire[root].next[i]=-1; }int main(){tot=1;for(int i=0;i<N;i++)tire[i].init();int t;scanf("%d",&t);while(t--){char str[10],word[35];scanf("%s%s",str,word);if(str[0]=='i')//插入insert(word);else if(str[0]=='d')//刪除del(word,search(word));else{//查詢if(search(word)>0)printf("Yes\n");elseprintf("No\n");}}return 0; }4.輸出唯一前綴
int tot; int trie[N][26];//trie[rt][x]=tot,root是上個(gè)節(jié)點(diǎn)編號(hào),x是字母,tot是下個(gè)節(jié)點(diǎn)編號(hào) int val[N]; void insert(char *s,int root){int len=strlen(s);for(int i=0;i<len;i++){int x=s[i]-'a';if(trie[root][x]==0){//現(xiàn)在插入的字母在之前同一節(jié)點(diǎn)處未出現(xiàn)過(guò)trie[root][x]=tot;//字母插入一個(gè)新的位置,否則不做處理val[tot]=0;//記錄以當(dāng)前結(jié)點(diǎn)為根的子樹(shù)下單詞個(gè)數(shù)tot++;}root=trie[root][x];//為下個(gè)字母的插入做準(zhǔn)備val[root]++;} } void find(char *s,int root){int len=strlen(s);for(int i=0;i<len;i++){int x=s[i]-'a';root=trie[root][x];//為查詢下個(gè)字母做準(zhǔn)備printf("%c",s[i]);//輸出當(dāng)前字母if(val[root]==1)//為1時(shí)直接返回return;} } char word[N][50]; int main(){int cnt=0;tot=1;while(scanf("%s",word[cnt])!=EOF)insert(word[cnt++],0);for(int i=0;i<cnt;i++){//枚舉所有單詞printf("%s ",word[i]);find(word[i],0);printf("\n");}return 0; }【例題】
- 統(tǒng)計(jì)難題(HDU-1251)(查詢前綴次數(shù)):點(diǎn)擊這里
- Hat’s Words(HDU-1247)(字典樹(shù)+單詞劃分):點(diǎn)擊這里
- Problem C(HDU-5687)(帶刪除的字典樹(shù)):點(diǎn)擊這里
- Phone List(HDU-1671)(有限空間+指針的靈活應(yīng)用):點(diǎn)擊這里
- Shortest Prefixes(POJ-2001)(輸出唯一前綴):點(diǎn)擊這里
總結(jié)
以上是生活随笔為你收集整理的字符串处理——字典树的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Simpsons’ Hidden Tal
- 下一篇: 营救(洛谷-P1396)