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;
}
}