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