1. 程式人生 > >Trie樹|字首樹的介紹與實現

Trie樹|字首樹的介紹與實現

本文嘗試用盡量簡潔的語言介紹一種樹形資料結構 —— Trie樹

一、什麼是Trie樹

Trie樹,又叫字典樹字首樹(Prefix Tree)單詞查詢樹 或 鍵樹,是一種多叉樹結構。如下圖:



上圖是一棵Trie樹,表示了關鍵字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。從上圖可以歸納出Trie樹的基本性質:

  1. 根節點不包含字元,除根節點外的每一個子節點都包含一個字元。
  2. 從根節點到某一個節點,路徑上經過的字元連線起來,為該節點對應的字串。
  3. 每個節點的所有子節點包含的字元互不相同

通常在實現的時候,會在節點結構中設定一個

標誌,用來標記該結點處是否構成一個單詞(關鍵字)。

可以看出,Trie樹的關鍵字一般都是字串,而且Trie樹把每個關鍵字儲存在一條路徑上,而不是一個結點中。另外,兩個有公共字首的關鍵字,在Trie樹中字首部分的路徑相同,所以Trie樹又叫做字首樹(Prefix Tree)

二、Trie樹的優缺點

Trie樹的核心思想是空間換時間,利用字串的公共字首來減少無謂的字串比較以達到提高查詢效率的目的

優點

  1. 插入和查詢的效率很高,都為O(m),其中 m 是待插入/查詢的字串的長度

    • 關於查詢,會有人說 hash 表時間複雜度是O(1)不是更快?但是,雜湊搜尋的效率通常取決於 hash 函式的好壞,若一個壞的 hash 函式導致
      很多的衝突,效率並不一定比Trie樹高。
  2. Trie樹中不同的關鍵字不會產生衝突

  3. Trie樹只有在允許一個關鍵字關聯多個值的情況下才有類似hash碰撞發生。

  4. Trie樹不用求 hash 值,對短字串有更快的速度。通常,求hash值也是需要遍歷字串的

  5. Trie樹可以對關鍵字按字典序排序。

缺點

  1. 當 hash 函式很好時,Trie樹的查詢效率會低於雜湊搜尋

  2. 空間消耗比較大。

三、Trie樹的應用

1、字串檢索

檢索/查詢功能是Trie樹最原始的功能。思路就是從根節點開始一個一個字元進行比較:

  • 如果沿路比較,發現不同的字元,則表示該字串在集合中不存在。
  • 如果所有的字元全部比較完並且全部相同,還需判斷最後一個節點的標誌位(標記該節點
    是否代表一個關鍵字)。
struct trie_node
{
    bool isKey;   // 標記該節點是否代表一個關鍵字
    trie_node *children[26]; // 各個子節點 
};

2、詞頻統計

Trie樹常被搜尋引擎系統用於文字詞頻統計

struct trie_node
{
    int count;   // 記錄該節點代表的單詞的個數
    trie_node *children[26]; // 各個子節點 
};
思路:為了實現詞頻統計,我們修改了節點結構,用一個整型變數count來計數。對每一個關鍵字執行插入操作,若已存在,計數加1,若不存在,插入後count置1。

注意:第一、第二種應用也都可以用 hash table 來做。

3、字串排序

Trie樹可以對大量字串按字典序進行排序,思路也很簡單:遍歷一次所有關鍵字,將它們全部插入trie樹,樹的每個結點的所有兒子很顯然地按照字母表排序,然後先序遍歷輸出Trie樹中所有關鍵字即可

4、字首匹配

例如:找出一個字串集合中所有以ab開頭的字串。我們只需要用所有字串構造一個trie樹,然後輸出以a->b->開頭的路徑上的關鍵字即可。

trie樹字首匹配常用於搜尋提示。如當輸入一個網址,可以自動搜尋出可能的選擇。當沒有完全匹配的搜尋結果,可以返回字首最相似的可能

5、作為其他資料結構和演算法輔助結構

字尾樹,AC自動機等。

四、Trie樹的實現

這裡為了方便,我們假設所有的關鍵字都由 a-z 的字母組成。下面是 trie 樹的一種典型實現

#include <iostream>
#include <string>
using namespace std;

#define ALPHABET_SIZE 26

typedef struct trie_node
{
	int count;   // 記錄該節點代表的單詞的個數
	trie_node *children[ALPHABET_SIZE]; // 各個子節點 
}*trie;

trie_node* create_trie_node()
{
	trie_node* pNode = new trie_node();
	pNode->count = 0;
	for(int i=0; i<ALPHABET_SIZE; ++i)
		pNode->children[i] = NULL;
	return pNode;
}

void trie_insert(trie root, char* key)
{
	trie_node* node = root;
	char* p = key;
	while(*p)
	{
		if(node->children[*p-'a'] == NULL)
		{
			node->children[*p-'a'] = create_trie_node();
		}
		node = node->children[*p-'a'];
		++p;
	}
	node->count += 1;
}

/**
 * 查詢:不存在返回0,存在返回出現的次數
 */ 
int trie_search(trie root, char* key)
{
	trie_node* node = root;
	char* p = key;
	while(*p && node!=NULL)
	{
		node = node->children[*p-'a'];
		++p;
	}
	
	if(node == NULL)
		return 0;
	else
		return node->count;
}

int main()
{
	// 關鍵字集合
	char keys[][8] = {"the", "a", "there", "answer", "any", "by", "bye", "their"};
	trie root = create_trie_node();

	// 建立trie樹
	for(int i = 0; i < 8; i++)
		trie_insert(root, keys[i]);

	// 檢索字串
	char s[][32] = {"Present in trie", "Not present in trie"};
	printf("%s --- %s\n", "the", trie_search(root, "the")>0?s[0]:s[1]);
	printf("%s --- %s\n", "these", trie_search(root, "these")>0?s[0]:s[1]);
	printf("%s --- %s\n", "their", trie_search(root, "their")>0?s[0]:s[1]);
	printf("%s --- %s\n", "thaw", trie_search(root, "thaw")>0?s[0]:s[1]);

	return 0;
}

對於Trie樹,我們一般只需要插入和搜尋操作。上面這段程式碼實現了一棵Trie樹,該Trie樹可以用來檢索單詞和統計詞頻。