摘要:很容易看出,前綴最壞情況下的查找也快過二叉搜索樹。這種壓縮后的被喚作前綴壓縮,或直接叫前綴樹,字典樹。最長公共前綴和的最長公共前綴是,遍歷字典樹到字母時,此時這些單詞的公共前綴是。
引子
前綴Trie, 又叫字符Tire, trie來自單詞retrieval, 一開始念作tree,后來改念try, 畢竟它與樹是不一樣的東西。網上許多文章都搞混了trie與樹。 trie是通過”邊“來儲存字符的一種樹狀結構,所謂邊就是節點與節點間的連接。trie每條邊只能存放一個字符。
它與hash樹很相似,或者說它是哈希樹的變種,哈希樹是用邊來存放一個整數(可以是一位數或兩位數)。前樹Tire與哈希樹都是多叉樹,換言之,父節點有N個子節點。
前綴Tire主要用于字符串的快速檢索,查詢效率比哈希表高。
前綴Trie的核心思想是空間換時間。利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。
前綴Trie樹也有它的缺點, 假定我們只對字母與數字進行處理,那么每個節點至少有52+10個子節點。為了節省內存,我們可以用鏈表或數組。在JS中我們直接用數組,因為JS的數組是動態的,自帶優化。
基本性質根節點不包含字符,除根節點外的每一個子節點都包含一個字符
從根節點到某一節點。路徑上經過的字符連接起來,就是該節點對應的字符串
每個節點的所有子節點包含的字符都不相同
程序實現</>復制代碼
// by 司徒正美
class Trie {
constructor() {
this.root = new TrieNode();
}
isValid(str) {
return /^[a-z1-9]+$/i.test(str);
}
insert(word) {
// addWord
if (this.isValid(word)) {
var cur = this.root;
for (var i = 0; i < word.length; i++) {
var c = word.charCodeAt(i);
c -= 48; //減少”0“的charCode
var node = cur.edges[c];
if (node == null) {
var node = (cur.edges[c] = new TrieNode());
node.value = word.charAt(i);
node.numPass = 1; //有N個字符串經過它
} else {
node.numPass++;
}
cur = node;
}
cur.isEnd = true; //檣記有字符串到此節點已經結束
cur.numEnd++; //這個字符串重復次數
return true;
} else {
return false;
}
}
remove(word){
if (this.isValid(word)) {
var cur = this.root;
var array = [], n = word.length
for (var i = 0; i < n; i++) {
var c = word.charCodeAt(i);
c = this.getIndex(c)
var node = cur.edges[c];
if(node){
array.push(node)
cur = node
}else{
return false
}
}
if(array.length === n){
array.forEach(function(){
el.numPass--
})
cur.numEnd --
if( cur.numEnd == 0){
cur.isEnd = false
}
}
}else{
return false
}
}
preTraversal(cb){//先序遍歷
function preTraversalImpl(root, str, cb){
cb(root, str);
for(let i = 0,n = root.edges.length; i < n; i ++){
let node = root.edges[i];
if(node){
preTraversalImpl(node, str + node.value, cb);
}
}
}
preTraversalImpl(this.root, "", cb);
}
// 在字典樹中查找是否存在某字符串為前綴開頭的字符串(包括前綴字符串本身)
isContainPrefix(word) {
if (this.isValid(word)) {
var cur = this.root;
for (var i = 0; i < word.length; i++) {
var c = word.charCodeAt(i);
c -= 48; //減少”0“的charCode
if (cur.edges[c]) {
cur = cur.edges[c];
} else {
return false;
}
}
return true;
} else {
return false;
}
}
isContainWord(str) {
// 在字典樹中查找是否存在某字符串(不為前綴)
if (this.isValid(word)) {
var cur = this.root;
for (var i = 0; i < word.length; i++) {
var c = word.charCodeAt(i);
c -= 48; //減少”0“的charCode
if (cur.edges[c]) {
cur = cur.edges[c];
} else {
return false;
}
}
return cur.isEnd;
} else {
return false;
}
}
countPrefix(word) {
// 統計以指定字符串為前綴的字符串數量
if (this.isValid(word)) {
var cur = this.root;
for (var i = 0; i < word.length; i++) {
var c = word.charCodeAt(i);
c -= 48; //減少”0“的charCode
if (cur.edges[c]) {
cur = cur.edges[c];
} else {
return 0;
}
}
return cur.numPass;
} else {
return 0;
}
}
countWord(word) {
// 統計某字符串出現的次數方法
if (this.isValid(word)) {
var cur = this.root;
for (var i = 0; i < word.length; i++) {
var c = word.charCodeAt(i);
c -= 48; //減少”0“的charCode
if (cur.edges[c]) {
cur = cur.edges[c];
} else {
return 0;
}
}
return cur.numEnd;
} else {
return 0;
}
}
}
class TrieNode {
constructor() {
this.numPass = 0;//有多少個單詞經過這節點
this.numEnd = 0; //有多少個單詞就此結束
this.edges = [];
this.value = ""; //value為單個字符
this.isEnd = false;
}
}
我們重點看一下TrieNode與Trie的insert方法。 由于字典樹是主要用在詞頻統計,因此它的節點屬性比較多, 包含了numPass, numEnd但非常重要的屬性。
insert方法是用于插入重詞,在開始之前,我們必須判定單詞是否合法,不能出現 特殊字符與空白。在插入時是打散了一個個字符放入每個節點中。每經過一個節點都要修改numPass。
優化現在我們每個方法中,都有一個c=-48的操作,其實數字與大寫字母與小寫字母間其實還有其他字符的,這樣會造成無謂的空間的浪費
</>復制代碼
// by 司徒正美
getIndex(c){
if(c < 58){//48-57
return c - 48
}else if(c < 91){//65-90
return c - 65 + 11
}else {//> 97
return c - 97 + 26+ 11
}
}
然后相關方法將c-= 48改成c = this.getIndex(c)即可
測試</>復制代碼
var trie = new Trie();
trie.insert("I");
trie.insert("Love");
trie.insert("China");
trie.insert("China");
trie.insert("China");
trie.insert("China");
trie.insert("China");
trie.insert("xiaoliang");
trie.insert("xiaoliang");
trie.insert("man");
trie.insert("handsome");
trie.insert("love");
trie.insert("Chinaha");
trie.insert("her");
trie.insert("know");
var map = {}
trie.preTraversal(function(node, str){
if(node.isEnd){
map[str] = node.numEnd
}
})
for(var i in map){
console.log(i+" 出現了"+ map[i]+" 次")
}
console.log("包含Chin(包括本身)前綴的單詞及出現次數:");
//console.log("China")
var map = {}
trie.preTraversal(function(node, str){
if(str.indexOf("Chin") === 0 && node.isEnd){
map[str] = node.numEnd
}
})
for(var i in map){
console.log(i+" 出現了"+ map[i]+" 次")
}
前綴Trie和其它數據結構的比較
前綴Trie與二叉搜索樹
二叉搜索樹應該是我們最早接觸的樹結構了,我們知道,數據規模為n時,二叉搜索樹插入、查找、刪除操作的時間復雜度通常只有O(log n),最壞情況下整棵樹所有的節點都只有一個子節點,退變成一個線性表,此時插入、查找、刪除操作的時間復雜度是O(n)。
通常情況下,前綴Trie的高度n要遠大于搜索字符串的長度m,故查找操作的時間復雜度通常為O(m),最壞情況下的時間復雜度才為O(n)。很容易看出,前綴Trie最壞情況下的查找也快過二叉搜索樹。
文中前綴Trie都是拿字符串舉例的,其實它本身對key的適宜性是有嚴格要求的,如果key是浮點數的話,就可能導致整個前綴Trie巨長無比,節點可讀性也非常差,這種情況下是不適宜用前綴Trie來保存數據的;而二叉搜索樹就不存在這個問題。
前綴Trie與Hash表? ?考慮一下Hash沖突的問題。Hash表通常我們說它的復雜度是O(1),其實嚴格說起來這是接近完美的Hash表的復雜度,另外還需要考慮到hash函數本身需要遍歷搜索字符串,復雜度是O(m)。在不同鍵被映射到“同一個位置”(考慮closed hashing,這“同一個位置”可以由一個普通鏈表來取代)的時候,需要進行查找的復雜度取決于這“同一個位置”下節點的數目,因此,在最壞情況下,Hash表也是可以成為一張單向鏈表的。
? ?前綴Trie可以比較方便地按照key的字母序來排序(整棵樹先序遍歷一次就好了),這跟絕大多數Hash表是不同的(Hash表一般對于不同的key來說是無序的)。
? ?在較理想的情況下,Hash表可以以O(1)的速度迅速命中目標,如果這張表非常大,需要放到磁盤上的話,Hash表的查找訪問在理想情況下只需要一次即可;但是Trie樹訪問磁盤的數目需要等于節點深度。
? ?很多時候前綴Trie比Hash表需要更多的空間,我們考慮這種一個節點存放一個字符的情況的話,在保存一個字符串的時候,沒有辦法把它保存成一個多帶帶的塊。前綴Trie的節點壓縮可以明顯緩解這個問題,后面會講到。
前綴Trie樹的改進 按位Trie樹(Bitwise Trie)原理上和普通Trie樹差不多,只不過普通Trie樹存儲的最小單位是字符,但是Bitwise Trie存放的是位而已。位數據的存取由CPU指令一次直接實現,對于二進制數據,它理論上要比普通Trie樹快。
節點壓縮分支壓縮:將一些連結線與節點進行合并,比如i-n-n可以合并成inn。這種壓縮后的Tire被喚作前綴壓縮Tire,或直接叫前綴樹, 字典樹。
節點映射表:這種方式也是在前綴Trie的節點可能已經幾乎完全確定的情況下采用的,針對前綴Trie中節點的每一個狀態,如果狀態總數重復很多的話,通過一個元素為數字的多維數組(比如Triple Array Trie)來表示,這樣存儲Trie樹本身的空間開銷會小一些,雖說引入了一張額外的映射表。
前綴Trie的應用前綴樹還是很好理解,它的應用也是非常廣的。
(1)字符串的快速檢索
字典樹的查詢時間復雜度是O(logL),L是字符串的長度。所以效率還是比較高的。字典樹的效率比hash表高。
(2)字符串排序
從上圖我們很容易看出單詞是排序的,先遍歷字母序在前面。減少了沒必要的公共子串。
(3)最長公共前綴
inn和int的最長公共前綴是in,遍歷字典樹到字母n時,此時這些單詞的公共前綴是in。
(4)自動匹配前綴顯示后綴
我們使用辭典或者是搜索引擎的時候,輸入appl,后面會自動顯示一堆前綴是appl的東東吧。那么有可能是通過前綴Trie實現的,前面也說了前綴Trie可以找到公共前綴,我們只需要把剩余的后綴遍歷顯示出來即可。
參考鏈接http://blog.csdn.net/abcd_d_/...
http://blog.csdn.net/u0139490...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92837.html
摘要:前言前綴樹是一種很常用的數據結構,例如我們常用的數據庫索引。而關于前綴樹的介紹,由于中國有關于前綴樹的教程,我就不班門弄斧了,我的答案也是參考教程的思路去解答,希望可以給大家一個參考。下面是原題目實現一個前綴樹,包含和這三個操作。 前言 前綴樹是一種很常用的數據結構,例如我們常用的數據庫索引。而關于前綴樹的介紹,由于LeetCode中國有關于前綴樹的教程,我就不班門弄斧了,我的答案也是...
摘要:壓縮前綴樹其實就是將所有只有一個子節點的節點合并成一個,以減少沒有意義的類似鏈表式的鏈接。然后我們開始遍歷這個前綴樹。 Implement Trie Implement a trie with insert, search, and startsWith methods. Note: You may assume that all inputs are consist of lowe...
摘要:是以太坊存儲數據的核心數據結構,它是由和結合的一種樹形結構,理解有助于我們更好的理解以太坊的數據存儲。所以就有了樹壓縮前綴樹,后面會介紹到,也被稱為,中文名稱默克爾樹,主要用于數據集較大時的文件校驗。 ??MPT(Merkle Patricia Tries)是以太坊存儲數據的核心數據結構,它是由Merkle Tree和Patricia Tree結合的一種樹形結構,理解MPT有助于我們更...
摘要:本文會集合多方資料以及我自己的一些理解,深入一些探究實現機制。位分區實際上是數字分區的一個子集,所有以的整數次冪,,,,為基數的數字分區前綴樹,都可以轉為位分區。其實舉個例子最好理解比如數字的二進制形式是,這是一個位的二進制數。 Immutable.js 采用了持久化數據結構和結構共享,保證每一個對象都是不可變的,任何添加、修改、刪除等操作都會生成一個新的對象,且通過結構共享等方式大幅...
摘要:首先,我們應該了解字典樹的性質和結構,就會很容易實現要求的三個相似的功能插入,查找,前綴查找。既然叫做字典樹,它一定具有順序存放個字母的性質。所以,在字典樹的里面,添加,和三個參數。 Problem Implement a trie with insert, search, and startsWith methods. Notice You may assume that all i...
閱讀 755·2023-04-26 01:30
閱讀 3305·2021-11-24 10:32
閱讀 2192·2021-11-22 14:56
閱讀 1986·2021-11-18 10:07
閱讀 559·2019-08-29 17:14
閱讀 629·2019-08-26 12:21
閱讀 3109·2019-08-26 10:55
閱讀 2945·2019-08-23 18:09