Trie字典樹又稱前綴樹,顧名思義,是查詢前綴匹配的一種樹形數據結構面試
能夠分爲插入(建立) 和 查詢兩部分。參考地址極客時間算法
下圖爲插入字符串的過程:
數組
建立完成後,每一個字符串最後一個字母標記爲終結點(圖中顯示爲紅色)數據結構
下圖爲查詢字符串:「her」的過程:綠色箭頭表示查詢路徑
咱們將要查找的字符串分割成單個的字符 h,e,r,一個一個查詢
ide
下圖爲查詢字符串:「he」的過程:綠色箭頭表示查詢路徑
由於‘e’不是終結點,因此不能徹底匹配上。
函數
樹形結構,類比於二叉樹的存儲嘛,每一個結點兩條分支(二叉樹);
而字典樹,每一個節點能夠最多有 26個分支(存儲英文字母)。優化
1-1二維數組存儲字母this
int trie[MAX_NODE][26];//MAX_NODE表示結點數量,每一個結點有26個字母結點 int k;
MAX_NODE表示結點數量,每一個結點有26個字母結點
Trie[i][j]的值是0,表示trie樹中i號節點,並無一條連出去的邊知足邊上的字符標識是字符集中第j個字符(從0開始);
trie[i][j]的值是正整數x表示trie樹中i號節點,有一條連出去的邊知足邊上的字符標識是字符集中第j個字符,而且
這條邊的終點是x號節點。搜索引擎
1-2鏈表
我這裏用C++中的vector實現,3d
vector< pair<char, int> > trie[MAX_NODE]; int k;
也能夠寫一個真正的鏈表,包含二元組字段<char,int>型的對應關係
1-3hash,
map<char, int> trie[MAX_NODE];
每次咱們想找i號節點有沒有標識
是某個字符ch的邊時,只要看trie[i][ch]的值便可
可是實際上map時空複雜度的常數都比較大
插入 查詢 其實是相似的,就是從樹的根開始往下遍歷,
2-1插入:從樹的根開始往下遍歷,到達一個結點,沒有這個字母就插入到這個結點下,做爲這個結點的子節點
基於二維數組結構的插入功能實現
代碼的第6~8行,一開始trie[][]被初始化爲0,保證每一個節點被建立出來時,都沒有子節點。K初 始化爲1表示一開始只有1個節點,也就是0號節點根節點。Color是用來標記一個節點是否是終結 點。Color[i]=1標識i號節點是終結點。 第9~21行是插入函數insert(w),w是字符指針,實際上能夠看做是一個字符串。 第11行是p從0號節點開始。 第12~19行是依次插入w的每個字符。 第13行是計算w[i]是字符集第幾個字符,這裏咱們假設字符集只包含26個小寫字母。 第14~17行是若是p沒有連出標識是w[i]的邊,那麼就建立一個。這裏新建立的節點必定就是k號節 點。所謂建立新節點實際上也沒什麼可建立的,新節點就是個編號。因此咱們直接令trie[i][c]=k 便可,而後將k累加1,整個建立過程就完成了。 第18行是沿着標記着w[i]的邊移動到下一個節點。 最後第20行,是將最後到達的節點p標記爲終結點。
2-2查詢:從樹的根開始往下遍歷,查看是否匹配上當前正在查的單詞
基於二維數組結構的查詢功能實現
第24行是從p=0也就是根節點開始。 第25~29行是枚舉s的每個字符。 第26行是計算當前字符s[i]在字符集的序號。 第27行是判斷p節點有沒有連出標識s[i]字符的邊,若是沒有,說明如今無路可走,直接返回0;如 果有的話, 第28行就是移動到下一個節點。若是整個循環結束尚未return 0,那就說明成功沿着s的每個 字符到達了p節點。這時只要判斷p節點是否是終結點便可,也就是第30行的代
public class Trie { private TrieNode root = new TrieNode('/'); // 存儲無心義字符 // 往 Trie 樹中插入一個字符串 public void insert(char[] text) { TrieNode p = root; for (int i = 0; i < text.length; ++i) { int index = text[i] - 'a'; if (p.children[index] == null) { TrieNode newNode = new TrieNode(text[i]); p.children[index] = newNode; } p = p.children[index]; } p.isEndingChar = true; } // 在 Trie 樹中查找一個字符串 public boolean find(char[] pattern) { TrieNode p = root; for (int i = 0; i < pattern.length; ++i) { int index = pattern[i] - 'a'; if (p.children[index] == null) { return false; // 不存在 pattern } p = p.children[index]; } if (p.isEndingChar == false) return false; // 不能徹底匹配,只是前綴 else return true; // 找到 pattern } public class TrieNode { public char data; public TrieNode[] children = new TrieNode[26]; public boolean isEndingChar = false; public TrieNode(char data) { this.data = data; } } }
插入的時間複雜度:O(N),N爲全部待插入字符串的長度之和
查詢的時間複雜度:O(K),K爲待查詢字符串的長度
佔內存:若是用二維數組實現,每一個節點就會額外須要 26*8=208 個字節
優化思路:將每一個節點中的數組換成其餘數據結構,好比有序數組(能夠二分查找)、跳錶、散列表、紅黑樹等。
Trie變體,縮點優化:對只有一個子節點的節點,並且此節點不是一個串的結束節點,能夠將此節點與子節點合併
由於字典樹是查找 「與前綴匹配的字符串」,又稱爲前綴樹。
關鍵詞提示就是 查尋找前綴匹配的前綴合適關鍵詞,固然還有更復雜的關鍵詞排名問題,這裏再也不展開。
原理與搜索引擎相似。
解題思路:Trie字典樹
首先咱們把集合中的N個字符串都插入到trie中。
對於每個查詢s咱們在trie中查找s,若是查找過程當中無路可走,那麼必定沒有以s爲前綴的字符串。
若是最後停在一個節點p,那咱們就要看看以p爲根的子樹裏一共有多少終結點。
終結點的數目就是答案。
可是若是咱們每次都遍歷以P爲根的子樹,那時間複雜度就過高了。解決的辦法是用空間換時間,咱們增長一個數組intcnt[MAX_NODE]
cnt[i]記錄的是以i號節點爲根的子樹中,有幾個終結點。
而後咱們每次insert一個字符串的時候,順便就把沿途的節點的cnt值都+1。
這樣就不用每次遍歷以P爲根的子樹,而是直接輸出cnt[P]便可。
代碼:
其實就是找一個節點p,知足以p爲根的子樹中的終結點很少於5個,同時以p的父節點爲根的子樹中的終結點大於5個。
和上題同樣用cnt數組標記,以後dfs查找終結點的數目
給定一個包含N個整數的集合S={A1, A2, A3, … AN}。然 後有M個詢問,每次詢問給定一個整數X,讓你找一個Ai使得Ai xor X的值最大。
首先咱們知道一個整數能夠用二進制表示成一個01串。好比3=(011)2, 5=(101)2, 4=(100)2……。
咱們假設輸入的整數都在0~2^32-1之間,因而咱們能夠用一個長度是32位的01串表示一個整數。
而後對於給定的N個整數A1, A2, A3, … AN,咱們把它們對應的01串都插入到一個trie中。注意這裏字符集只有0和1,因此整個trie是一棵二叉樹。
下面咱們舉一個例子,爲了描述方便,咱們假設整數都在0~7之間,也就是能夠用3位01串表示。
如今假設S={1, 2, 7},也就是說咱們要在Trie中插入{001, 010, 111}:
這時假設咱們要查詢x=4,也就是哪一個數和4異或結果最大?4=(100)2, 咱們的作法是在trie樹中,儘可能與4的二進制位反着走。 好比4的第一位(最高位)是1,咱們從0出發第一步就儘可能沿着0走。由於咱們要異或和最大,01相反才能異或值是1。 而且這一步是能夠貪心的,也就是說若是有相反的邊,那麼咱們必定沿着這條邊走。由於最高位異或得1的話,即使後面都是0, 10000…000也要比最高位是0,後面都是1的011111…111大。 因此咱們第一步沿着標識是0的邊,移動到了1號節點;4第二位是0,因此咱們沿着標識是1的邊移動到4號節點; 4的第三位是0,可是4號節點沒有標識是1的邊,因此咱們也只好沿着標識是0的邊移動到5號節點。 已經到了終結點,因此5號節點對應的A2=(010)2=2就是咱們要求的答案,A2 xor 4 = 6是最大的。