1.背景
匹配算法的瓶頸之一在於如何判斷字典中是否含有字符串,如果用的是有序集合(TreeMap
)的話,複雜度是O(logn)
,如果用散列表(HashMap
),賬面上的時間複雜度雖然下降了,但內存複雜度上去了。我們要尋找一種速度又快,又省內存的數據結構。
2.字典樹概念:
又稱單詞查找樹,Trie樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。(看圖馬上理解)
3.字典樹特點:
- 根節點不包含字符,除根節點外每一個節點都只包含一個字符
- 從根節點到某一節點,路徑上經過的字符連接起來,爲該節點對應的字符串
- 每個節點的所有子節點包含的字符都不相同
4.字典樹的實現原理:
從確定有限狀態自動機(DFA)的角度來講,每個節點都是一個狀態,狀態表示當前已經查詢到的前綴。從父節點到子節點的移動過程可以看作一次狀態轉移
。以下是查詢步驟:
- 我們輸入一個想要查詢的詞,如果有滿足條件的邊,狀態轉移;如果找不到,直接失敗
- 完成了全部轉移時,拿到了最後一個字符的狀態,詢問該狀態是否爲終點狀態,如果是則查到了單詞,否則該單詞不在字典中
"刪改改查"都是一回事,以下不再贅述
5.字典樹節點結構:
我們爲了方便直接用數組實現
private final int SIZE = 26; //每個節點能包含的子節點數,即需要SIZE個指針來指向其孩子
private Node root; //字典樹的根節點
/**
* @author: Ragty
* @Date: 2020/2/24 21:10
* @Description: 字典樹節點
*/
private class Node {
private boolean status; //標識該節點是否爲某一字符串終端節點
private Node[] child; //該節點的子節點
public Node() {
child = new Node[SIZE];
status = false;
}
}
6.字典樹的實現:
/**
* @Author Ragty
* @Description 初始化一個節點
* @Date 2020/2/24 21:11
*/
public TrieTree() {
root = new Node();
}
/**
* @Author Ragty
* @Description 在字典樹中插入一個單詞
* @Date 2020/2/23 21:42
*/
public void insert(String word) {
if (word == null || word.isEmpty()) {
return;
}
Node pNode = this.root;
for (int i = 0; i < word.length(); i++)
{
int index = word.charAt(i) - 'a';
if (pNode.child[index] == null) { //如果不存在節點,則新建一個節點插入
Node tmpNode = new Node();
pNode.child[index] = tmpNode;
}
pNode = pNode.child[index]; //指向下一層
}
pNode.status = true;
}
/**
* @author: Ragty
* @Date: 2020/2/24 21:15
* @Description: 檢查字典樹中是否完全包含字符串
*/
public boolean hasStr(String word) {
Node pNode = this.root;
//逐個字符去檢查
for (int i = 0; i < word.length(); i++) {
int index = word.charAt(i) - 'a';
//在字典樹中沒有對應的節點,或者word字符串的最後一個字符在字典樹中檢測對應節點的isStr屬性爲false,則返回false
if (pNode.child[index] == null
|| (i + 1 == word.length() && pNode.child[index].status == false)) {
return false;
}
pNode = pNode.child[index];
}
return true;
}
/**
* @author: Ragty
* @Date: 2020/2/24 21:21
* @Description: 先序遍歷
*/
public void preWalk(Node root) {
Node pNode = root;
for (int i = 0; i < SIZE; i++) {
if (pNode.child[i] != null) {
System.out.print((char) ('a' + i) + "--");
preWalk(pNode.child[i]);
}
}
}
/**
* @author: Ragty
* @Date: 2020/2/24 21:17
* @Description: 返回字典樹的根節點
*/
public Node getRoot() {
return root;
}
7.測試:
public static void main(String[] args) {
TrieTree trieTree = new TrieTree();
trieTree.insert("sad");
trieTree.insert("say");
trieTree.insert("to");
trieTree.insert("too");
System.out.println(trieTree.hasStr("say"));
System.out.println(trieTree.hasStr("toooo"));
Node root = trieTree.getRoot();
trieTree.preWalk(root);
}
8.測試結果:
true
false
s--a--d--y--t--o--o--
9.算法分析:
當字典大小爲n時,雖然最壞情懷下字典樹的複雜度依然是O(logn)。但它的實際速度比二分查找快,這是因爲隨着路徑的深入,前綴匹配是遞進的過程,算法不必比較字符串的前綴,因此可以節省很多用來比較的時間。
10.算法改進:
這裏我們查詢某個詞的時候還需要逐個對比,若我們將對象轉換爲散列值,散列函數輸出區間爲[0,65535]之間的整數,這時候我們直接訪問下標就可以訪問到對應的字符,不過這種做法只適用於第一行,否則會內存指數膨脹,後邊的按數組存放即可,查詢時直接二分法查詢。