Trie樹(字典樹):應用於統計和排序
轉載這篇關於字典樹的原因是看到騰訊面試相關的題:就是在海量資料中找出某一個數,比如2億QQ號中查找出某一個特定的QQ號。。
有人提到字典樹,我就順便了解下字典樹。
[轉自:http://blog.csdn.net/oncealong/article/details/51707256]
[轉自:http://blog.csdn.NET/hguisu/article/details/8131559]
1. 什麼是trie樹
1.Trie樹 (特例結構樹)
Trie樹,又稱單詞查詢樹、字典樹,是一種樹形結構,是一種雜湊樹的變種,是一種用於快速檢索的多叉樹結構。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:最大限度地減少無謂的字串比較,查詢效率比
Trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。
Trie樹也有它的缺點,Trie樹的記憶體消耗非常大.當然,或許用左兒子右兄弟的方法建樹的話,可能會好點.
2. 三個基本特性:
1)根節點不包含字元,除根節點外每一個節點都只包含一個字元。
2)從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
3)每個節點的所有子節點包含的字元都不相同。
3 .例子
和二叉查詢樹不同,在trie樹中,每個結點上並非儲存一個元素。 trie樹把要查詢的關鍵詞看作一個字元序列。並根據構成關鍵詞字元的先後順序構造用於檢索的樹結構。 在trie樹上進行檢索類似於查閱英語詞典。
一棵m度的trie樹或者為空,或者由m棵m度的trie樹構成。
例如,電子英文詞典,為了方便使用者快速檢索英語單詞,可以建立一棵trie樹。例如詞典由下面的單詞成:a、b、c、aa、ab、ac、ba、ca、aba、abc、baa、bab、bac、cab、abba、baba、caba、abaca、caaba
再舉一個例子。給出一組單詞,inn, int, at, age, adv, ant, 我們可以得到下面的Trie:
可以看出:
- 每條邊對應一個字母。
- 每個節點對應一項字首。葉節點對應最長字首,即單詞本身。
- 單詞inn與單詞int有共同的字首“in”, 因此他們共享左邊的一條分支,root->i->in。同理,ate, age, adv, 和ant共享字首"a",所以他們共享從根節點到節點"a"的邊。
查詢操縱非常簡單。比如要查詢int,順著路徑i -> in -> int就找到了。
2. trie樹的實現
1.插入過程
對於一個單詞,從根開始,沿著單詞的各個字母所對應的樹中的節點分支向下走,直到單詞遍歷完,將最後的節點標記為紅色,表示該單詞已插入trie樹。
2. 查詢過程
其方法為:
(1) 從根結點開始一次搜尋;
(2) 取得要查詢關鍵詞的第一個字母,並根據該字母選擇對應的子樹並轉到該子樹繼續進行檢索;
(3) 在相應的子樹上,取得要查詢關鍵詞的第二個字母,並進一步選擇對應的子樹進行檢索。
(4) 迭代過程……
(5) 在某個結點處,關鍵詞的所有字母已被取出,則讀取附在該結點上的資訊,即完成查詢。其他操作類似處理.
即從根開始按照單詞的字母順序向下遍歷trie樹,一旦發現某個節點標記不存在或者單詞遍歷完成而最後的節點未標記為紅色,則表示該單詞不存在,若最後的節點標記為紅色,表示該單詞存在。如下圖中:trie樹中存在的就是abc、d、da、dda四個單詞。在實際的問題中可以將標記顏色的標誌位改為數量count等其他符合題目要求的變數。
程式碼:
[cpp] view plain copy print?
- // stdafx.h : include file for standard system include files,
- // or project specific include files that are used frequently, but
- // are changed infrequently
- //
- #pragma once
- #include <stdio.h>
- #include "stdlib.h"
- #include <iostream>
- #include <string.h>
- using namespace std;
- //巨集定義
- #define TRUE 1
- #define FALSE 0
- #define NULL 0
- #define OK 1
- #define ERROR 0
- #define INFEASIBLE -1
- #define OVERFLOW -2
- const int num_chars = 26;
- class Trie {
- public:
- Trie();
- Trie(Trie& tr);
- virtual ~Trie();
- int trie_search(const char* word, char* entry ) const;
- int insert(const char* word, const char* entry);
- int remove(const char* word, char* entry);
- protected:
- struct Trie_node{
- char* data; //若不為空,表示從root到此結點構成一個單詞
- Trie_node* branch[num_chars]; //分支
- Trie_node(); //建構函式
- };
- Trie_node* root; //根結點(指標)
- };
[cpp] view plain copy print?
- // Test.cpp : Defines the entry point for the console application.
- //
- #include "stdafx.h"
- Trie::Trie_node::Trie_node() {
- data = NULL;
- for (int i=0; i<num_chars; ++i)
- branch[i] = NULL;
- }
- Trie::Trie():root(NULL) {}
- Trie::~Trie(){}
- int Trie::trie_search(const char* word, char* entry ) const {
- int position = 0; //層數
- char char_code;
- Trie_node *location = root; //從根結點開始
- while( location!=NULL && *word!=0 ) {
- if (*word >= 'A' && *word <= 'Z')
- char_code = *word-'A';
- else if (*word>='a' && *word<='z')
- char_code = *word-'a';
- else return 0;// 不合法的單詞
- //轉入相應分支指標
- location = location->branch[char_code];
- position++;
- word++;
- }
- //找到,獲取資料,成功返回
- if ( location != NULL && location->data != NULL ) {
- strcpy(entry,location->data);
- return 1;
- }
- else return 0;// 不合法的單詞
- }
- int Trie::insert(const char* word, const char* entry) {
- int result = 1, position = 0;
- if ( root == NULL ) root = new Trie_node; //初始插入,根結點為空
- char char_code;
- Trie_node *location = root; //從根結點開始
- while( location!=NULL && *word!=0 ) {
- if (*word>='A' && *word<='Z') char_code = *word-'A';
- else if (*word>='a' && *word<='z') char_code = *word-'a';
- else return 0;// 不合法的單詞
- //不存在此分支
- if( location->branch[char_code] == NULL )
- location->branch[char_code] = new Trie_node; //建立空分支
- //轉入分支
- location = location->branch[char_code];
- position++;word++; }
- if (location->data != NULL) result = 0;//欲插入的單詞已經存在
- else { //插入資料
- location->data = new char[strlen(entry)+1]; //分配記憶體
- strcpy(location->data, entry); //給data賦值表明單詞存在
- }
- return result;
- }
- int main(){
- Trie t;
- char entry[100];
- t.insert("a", "DET");
- t.insert("abacus","NOUN");
- t.insert("abalone","NOUN");
- t.insert("abandon","VERB");
- t.insert("abandoned","ADJ");
- t.insert("abashed","ADJ");
- t.insert("abate","VERB");
- t.insert("this", "PRON");
- if (t.trie_search("this", entry))
- cout<<"'this' was found. pos: "<<entry<<endl;
- if (t.trie_search("abate", entry))
- cout<<"'abate' is found. pos: "<<entry<<endl;
- if (t.trie_search("baby", entry))
- cout<<"'baby' is found. pos: "<<entry<<endl;
- else
- cout<<"'baby' does not exist at all!"<<endl;
- }
3. 查詢分析
在trie樹中查詢一個關鍵字的時間和樹中包含的結點數無關,而取決於組成關鍵字的字元數。而二叉查詢樹的查詢時間和樹中的結點數有關O(log2n)。
如果要查詢的關鍵字可以分解成字元序列且不是很長,利用trie樹查詢速度優於二叉查詢樹。如: 若關鍵字長度最大是5,則利用trie樹,利用5次比較可以從26^5=11881376個可能的關鍵字中檢索出指定的關鍵字。而利用二叉查詢樹至少要進行次比較。
3. trie樹的應用:
1. 字串檢索,詞頻統計,搜尋引擎的熱門查詢
事先將已知的一些字串(字典)的有關資訊儲存到trie樹裡,查詢另外一些未知字串是否出現過或者出現頻率。
舉例:
1)有一個1G大小的一個檔案,裡面每一行是一個詞,詞的大小不超過16位元組,記憶體限制大小是1M。返回頻數最高的100個詞。
2)給出N 個單片語成的熟詞表,以及一篇全用小寫英文書寫的文章,請你按最早出現的順序寫出所有不在熟詞表中的生詞。
3)給出一個詞典,其中的單詞為不良單詞。單詞均為小寫字母。再給出一段文字,文字的每一行也由小寫字母構成。判斷文字中是否含有任何不良單詞。例如,若rob是不良單詞,那麼文字problem含有不良單詞。
4)1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字串
5)尋找熱門查詢:搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255位元組。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,但是如果去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的使用者越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1G。
2. 字串最長公共字首
Trie樹利用多個字串的公共字首來節省儲存空間,反之,當我們把大量字串儲存到一棵trie樹上時,我們可以快速得到某些字串的公共字首。舉例:
1) 給出N 個小寫英文字母串,以及Q 個詢問,即詢問某兩個串的最長公共字首的長度是多少. 解決方案:
首先對所有的串建立其對應的字母樹。此時發現,對於兩個串的最長公共字首的長度即它們所在結點的公共祖先個數,於是,問題就轉化為了離線 (Offline)的最近公共祖先(Least Common Ancestor,簡稱LCA)問題。
而最近公共祖先問題同樣是一個經典問題,可以用下面幾種方法:
1. 利用並查集(Disjoint Set),可以採用採用經典的Tarjan 演算法;
2. 求出字母樹的尤拉序列(Euler Sequence )後,就可以轉為經典的最小值查詢(Range Minimum Query,簡稱RMQ)問題了;
3. 排序
Trie樹是一棵多叉樹,只要先序遍歷整棵樹,輸出相應的字串便是按字典序排序的結果。
舉例: 給你N 個互不相同的僅由一個單詞構成的英文名,讓你將它們按字典序從小到大排序輸出。
4 作為其他資料結構和演算法的輔助結構
如字尾樹,AC自動機等。
--------------------- 本文來自 EngZegNgi 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/wzy0754/article/details/69949949?utm_source=copy