哈哈哈哈哈操欧洲电影,久草网在线,亚洲久久熟女熟妇视频,麻豆精品色,久久福利在线视频,日韩中文字幕的,淫乱毛视频一区,亚洲成人一二三,中文人妻日韩精品电影

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Trie樹(shù)數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)原理和題目實(shí)踐

算法與數(shù)據(jù)結(jié)構(gòu) ? 來(lái)源:labuladong ? 作者:labuladong ? 2022-05-11 17:47 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

作者:labuladong

公眾號(hào):labuladong

讀完本文,可以去力扣解決如下題目:

208. 實(shí)現(xiàn) Trie (前綴樹(shù))(Medium

1804. 實(shí)現(xiàn) Trie (前綴樹(shù)) II(Medium

648. 單詞替換(Medium

211. 添加與搜索單詞(Medium

677. 鍵值映射Medium

Trie 樹(shù)又叫字典樹(shù)、前綴樹(shù)、單詞查找樹(shù),是一種二叉樹(shù)衍生出來(lái)的高級(jí)數(shù)據(jù)結(jié)構(gòu),主要應(yīng)用場(chǎng)景是處理字符串前綴相關(guān)的操作。

后臺(tái)有挺多讀者說(shuō)今年的面試筆試題涉及到這種數(shù)據(jù)結(jié)構(gòu)了,所以請(qǐng)我講一講。

幾年前我在《算法 4》第一次學(xué)到這種數(shù)據(jù)結(jié)構(gòu),不過(guò)個(gè)人認(rèn)為講解不是特別通俗易懂,所以本文按照我的邏輯幫大家重新梳理一遍 Trie 樹(shù)的原理,并基于《算法 4》的代碼實(shí)現(xiàn)一套更通用易懂的代碼模板,用于處理力扣上一系列字符串前綴問(wèn)題。

閱讀本文之前希望你讀過(guò)我舊文講過(guò)的回溯算法代碼模板手把手刷二叉樹(shù)(總結(jié)篇)。

本文主要分三部分:

1、講解 Trie 樹(shù)的工作原理。

2、給出一套TrieMapTrieSet的代碼模板,實(shí)現(xiàn)幾個(gè)常用 API。

3、實(shí)踐環(huán)節(jié),直接套代碼模板秒殺 5 道算法題。本來(lái)可以秒殺七八道題,篇幅考慮,剩下的我集成到刷題插件中。

關(guān)于MapSet,是兩個(gè)抽象數(shù)據(jù)結(jié)構(gòu)(接口),Map存儲(chǔ)一個(gè)鍵值對(duì)集合,其中鍵不重復(fù),Set存儲(chǔ)一個(gè)不重復(fù)的元素集合。

常見(jiàn)的MapSet的底層實(shí)現(xiàn)原理有哈希表和二叉搜索樹(shù)兩種,比如 Java 的 HashMap/HashSet 和 C++ 的 unorderd_map/unordered_set 底層就是用哈希表實(shí)現(xiàn),而 Java 的 TreeMap/TreeSet 和 C++ 的 map/set 底層使用紅黑樹(shù)這種自平衡 BST 實(shí)現(xiàn)的。

而本文實(shí)現(xiàn)的 TrieSet/TrieMap 底層則用 Trie 樹(shù)這種結(jié)構(gòu)來(lái)實(shí)現(xiàn)。

了解數(shù)據(jù)結(jié)構(gòu)的讀者應(yīng)該知道,本質(zhì)上Set可以視為一種特殊的MapSet其實(shí)就是Map中的鍵。

所以本文先實(shí)現(xiàn)TrieMap,然后在TrieMap的基礎(chǔ)上封裝出TrieSet。

PS:為了模板通用性的考慮,后文會(huì)用到 Java 的泛型,也就是用尖括號(hào)<>指定的類(lèi)型變量。這些類(lèi)型變量的作用是指定我們實(shí)現(xiàn)的容器中存儲(chǔ)的數(shù)據(jù)類(lèi)型,類(lèi)比 Java 標(biāo)準(zhǔn)庫(kù)的那些容器的用法應(yīng)該不難理解。

前文學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的框架思維說(shuō)過(guò),各種亂七八糟的結(jié)構(gòu)都是為了在「特定場(chǎng)景」下盡可能高效地進(jìn)行增刪查改。

你比如HashMap的優(yōu)勢(shì)是能夠在 O(1) 時(shí)間通過(guò)鍵查找對(duì)應(yīng)的值,但要求鍵的類(lèi)型K必須是「可哈希」的;而TreeMap的特點(diǎn)是方便根據(jù)鍵的大小進(jìn)行操作,但要求鍵的類(lèi)型K必須是「可比較」的。

本文要實(shí)現(xiàn)的TrieMap也是類(lèi)似的,由于 Trie 樹(shù)原理,我們要求TrieMap的鍵必須是字符串類(lèi)型,值的類(lèi)型V可以隨意。

接下來(lái)我們了解一下 Trie 樹(shù)的原理,看看為什么這種數(shù)據(jù)結(jié)構(gòu)能夠高效操作字符串。

Trie 樹(shù)原理

Trie 樹(shù)本質(zhì)上就是一棵從二叉樹(shù)衍生出來(lái)的多叉樹(shù)。

二叉樹(shù)節(jié)點(diǎn)的代碼實(shí)現(xiàn)是這樣:

/*基本的二叉樹(shù)節(jié)點(diǎn)*/
classTreeNode{
intval;
TreeNodeleft,right;
}

其中left, right存儲(chǔ)左右子節(jié)點(diǎn)的指針,所以二叉樹(shù)的結(jié)構(gòu)是這樣:

2fee80a6-d0f1-11ec-bce3-dac502259ad0.jpg

多叉樹(shù)節(jié)點(diǎn)的代碼實(shí)現(xiàn)是這樣:

/*基本的多叉樹(shù)節(jié)點(diǎn)*/
classTreeNode{
intval;
TreeNode[]children;
}

其中children數(shù)組中存儲(chǔ)指向孩子節(jié)點(diǎn)的指針,所以多叉樹(shù)的結(jié)構(gòu)是這樣:

2fff692a-d0f1-11ec-bce3-dac502259ad0.jpg

TrieMap中的樹(shù)節(jié)點(diǎn)TrieNode的代碼實(shí)現(xiàn)是這樣:

/*Trie樹(shù)節(jié)點(diǎn)實(shí)現(xiàn)*/
classTrieNode<V>{
Vval=null;
TrieNode[]children=newTrieNode[256];
}

這個(gè)val字段存儲(chǔ)鍵對(duì)應(yīng)的值,children數(shù)組存儲(chǔ)指向子節(jié)點(diǎn)的指針。

但是和之前的普通多叉樹(shù)節(jié)點(diǎn)不同,TrieNodechildren數(shù)組的索引是有意義的,代表鍵中的一個(gè)字符。

比如說(shuō)children[97]如果非空,說(shuō)明這里存儲(chǔ)了一個(gè)字符'a',因?yàn)?code style="margin-right:2px;margin-left:2px;padding:2px 4px;font-size:inherit;line-height:inherit;color:rgb(233,105,0);background:rgb(248,248,248);">'a'的 ASCII 碼為 97。

我們的模板只考慮處理 ASCII 字符,所以children數(shù)組的大小設(shè)置為 256。不過(guò)這個(gè)可以根據(jù)具體問(wèn)題修改,比如改成更小的數(shù)組或者HashMap都是一樣的效果。

有了以上鋪墊,Trie 樹(shù)的結(jié)構(gòu)是這樣的:

300db21e-d0f1-11ec-bce3-dac502259ad0.jpg

一個(gè)節(jié)點(diǎn)有 256 個(gè)子節(jié)點(diǎn)指針,但大多數(shù)時(shí)候都是空的,可以省略掉不畫(huà),所以一般你看到的 Trie 樹(shù)長(zhǎng)這樣

301d5c64-d0f1-11ec-bce3-dac502259ad0.jpg

這是在TrieMap中插入一些鍵值對(duì)后的樣子,白色節(jié)點(diǎn)代表val字段為空,橙色節(jié)點(diǎn)代表val字段非空。

這里要特別注意,TrieNode節(jié)點(diǎn)本身只存儲(chǔ)val字段,并沒(méi)有一個(gè)字段來(lái)存儲(chǔ)字符,字符是通過(guò)子節(jié)點(diǎn)在父節(jié)點(diǎn)的children數(shù)組中的索引確定的。

形象理解就是,Trie 樹(shù)用「樹(shù)枝」存儲(chǔ)字符串(鍵),用「節(jié)點(diǎn)」存儲(chǔ)字符串(鍵)對(duì)應(yīng)的數(shù)據(jù)(值)。所以我在圖中把字符標(biāo)在樹(shù)枝,鍵對(duì)應(yīng)的值val標(biāo)在節(jié)點(diǎn)上

307412de-d0f1-11ec-bce3-dac502259ad0.jpg

明白這一點(diǎn)很重要,有助于之后你理解代碼實(shí)現(xiàn)。

PS:《算法 4》以及網(wǎng)上講 Trie 樹(shù)的很多文章中都是把字符標(biāo)在節(jié)點(diǎn)上,我認(rèn)為這樣很容易讓初學(xué)者產(chǎn)生誤解,所以建議大家按照我的這種理解來(lái)記憶 Trie 樹(shù)結(jié)構(gòu)。

現(xiàn)在你應(yīng)該知道為啥 Trie 樹(shù)也叫前綴樹(shù)了,因?yàn)槠渲械淖址蚕砬熬Y,相同前綴的字符串集中在 Trie 樹(shù)中的一個(gè)子樹(shù)上,給字符串的處理帶來(lái)很大的便利。

TrieMap/TrieSet API 及實(shí)現(xiàn)

首先我們看一下本文實(shí)現(xiàn)的TrieMap的 API,為了舉例 API 的功能,假設(shè) TrieMap 中已經(jīng)存儲(chǔ)了如下鍵值對(duì):

301d5c64-d0f1-11ec-bce3-dac502259ad0.jpg

//底層用Trie樹(shù)實(shí)現(xiàn)的鍵值映射
//鍵為String類(lèi)型,值為類(lèi)型VclassTrieMap<V>{

/*****增/改*****/

//在Map中添加key
publicvoidput(Stringkey,Vval);

/*****刪*****/

//刪除鍵key以及對(duì)應(yīng)的值
publicvoidremove(Stringkey);

/*****查*****/

//搜索key對(duì)應(yīng)的值,不存在則返回null
//get("the")->4
//get("tha")->null
publicVget(Stringkey);

//判斷key是否存在在Map中
//containsKey("tea")->false
//containsKey("team")->true
publicbooleancontainsKey(Stringkey);

//在Map的所有鍵中搜索query的最短前綴
//shortestPrefixOf("themxyz")->"the"
publicStringshortestPrefixOf(Stringquery);

//在Map的所有鍵中搜索query的最長(zhǎng)前綴
//longestPrefixOf("themxyz")->"them"
publicStringlongestPrefixOf(Stringquery);

//搜索所有前綴為prefix的鍵
//keysWithPrefix("th")->["that","the","them"]
publicListkeysWithPrefix(Stringprefix);

//判斷是和否存在前綴為prefix的鍵
//hasKeyWithPrefix("tha")->true
//hasKeyWithPrefix("apple")->false
publicbooleanhasKeyWithPrefix(Stringprefix);

//通配符.匹配任意字符,搜索所有匹配的鍵
//keysWithPattern("t.a.")->["team","that"]
publicListkeysWithPattern(Stringpattern);

//通配符.匹配任意字符,判斷是否存在匹配的鍵
//hasKeyWithPattern(".ip")->true
//hasKeyWithPattern(".i")->false
publicbooleanhasKeyWithPattern(Stringpattern);

//返回Map中鍵值對(duì)的數(shù)量
publicintsize();
}

至于TrieSet的 API 大同小異,所以這里不重復(fù)列舉,后文直接給出實(shí)現(xiàn)。

接下來(lái)是重頭戲,我們一個(gè)一個(gè)實(shí)現(xiàn)TrieMap的上述 API 函數(shù)。

首先,TrieMap類(lèi)中一定需要記錄 Trie 的根節(jié)點(diǎn)root,以及 Trie 樹(shù)中的所有節(jié)點(diǎn)數(shù)量用于實(shí)現(xiàn)size()方法:

classTrieMap<V>{
//ASCII碼個(gè)數(shù)
privatestaticfinalintR=256;
//當(dāng)前存在Map中的鍵值對(duì)個(gè)數(shù)
privateintsize=0;

privatestaticclassTrieNode<V>{
Vval=null;
TrieNode[]children=newTrieNode[R];
}

//Trie樹(shù)的根節(jié)點(diǎn)
privateTrieNoderoot=null;

/*其他API的實(shí)現(xiàn)...*/

publicintsize(){
returnsize;
}
}

另外,我們?cè)賹?shí)現(xiàn)一個(gè)工具函數(shù)getNode

//從節(jié)點(diǎn)node開(kāi)始搜索key,如果存在返回對(duì)應(yīng)節(jié)點(diǎn),否則返回null
privateTrieNodegetNode(TrieNodenode,Stringkey){
TrieNodep=node;
//從節(jié)點(diǎn)node開(kāi)始搜索key
for(inti=0;iif(p==null){
//無(wú)法向下搜索
returnnull;
}
//向下搜索
charc=key.charAt(i);
p=p.children[c];
}
returnp;
}
30da3190-d0f1-11ec-bce3-dac502259ad0.jpg

有了這個(gè)getNode函數(shù),就能實(shí)現(xiàn)containsKey方法和get方法了:

//搜索key對(duì)應(yīng)的值,不存在則返回null
publicVget(Stringkey){
//從root開(kāi)始搜索key
TrieNodex=getNode(root,key);
if(x==null||x.val==null){
//x為空或x的val字段為空都說(shuō)明key沒(méi)有對(duì)應(yīng)的值
returnnull;
}
returnx.val;
}

//判斷key是否存在在Map中
publicbooleancontainsKey(Stringkey){
returnget(key)!=null;
}

這里需要注意,就算getNode(key)的返回值x非空,也只能說(shuō)字符串key是一個(gè)「前綴」;除非x.val同時(shí)非空,才能判斷鍵key存在。

不過(guò),這個(gè)特性恰好能夠幫我們實(shí)現(xiàn)hasKeyWithPrefix方法:

//判斷是和否存在前綴為prefix的鍵
publicbooleanhasKeyWithPrefix(Stringprefix){
//只要能找到 prefix 對(duì)應(yīng)的節(jié)點(diǎn),就是存在前綴
returngetNode(root,prefix)!=null;
}

類(lèi)似getNode方法的邏輯,我們可以實(shí)現(xiàn)shortestPrefixOf方法,只要在第一次遇到存有val的節(jié)點(diǎn)的時(shí)候返回就行了:

//在所有鍵中尋找query的最短前綴
publicStringshortestPrefixOf(Stringquery){
TrieNodep=root;
//從節(jié)點(diǎn)node開(kāi)始搜索key
for(inti=0;iif(p==null){
//無(wú)法向下搜索
return"";
}
if(p.val!=null){
//找到一個(gè)鍵是query的前綴
returnquery.substring(0,i);
}
//向下搜索
charc=query.charAt(i);
p=p.children[c];
}

if(p!=null&&p.val!=null){
//如果query本身就是一個(gè)鍵
returnquery;
}
return"";
}

這里需要注意的是 for 循環(huán)結(jié)束之后我們還需要額外檢查一下。

因?yàn)橹罢f(shuō)了 Trie 樹(shù)中「樹(shù)枝」存儲(chǔ)字符串,「節(jié)點(diǎn)」存儲(chǔ)字符串對(duì)應(yīng)的值,for 循環(huán)相當(dāng)于只遍歷了「樹(shù)枝」,但漏掉了最后一個(gè)「節(jié)點(diǎn)」,即query本身就是TrieMap中的一個(gè)鍵的情況。

如果你理解了shortestPrefixOf的實(shí)現(xiàn),那么longestPrefixOf也是非常類(lèi)似的:

//在所有鍵中尋找query的最長(zhǎng)前綴
publicStringlongestPrefixOf(Stringquery){
TrieNodep=root;
//記錄前綴的最大長(zhǎng)度
intmax_len=0;

//從節(jié)點(diǎn)node開(kāi)始搜索key
for(inti=0;iif(p==null){
//無(wú)法向下搜索
break;
}
if(p.val!=null){
//找到一個(gè)鍵是query的前綴,更新前綴的最大長(zhǎng)度
max_len=i;
}
//向下搜索
charc=query.charAt(i);
p=p.children[c];
}

if(p!=null&&p.val!=null){
//如果query本身就是一個(gè)鍵
returnquery;
}
returnquery.substring(0,max_len);
}

每次遇到p.val非空的時(shí)候說(shuō)明找到一個(gè)鍵,但是我們不急著返回,而是更新max_len變量,記錄最長(zhǎng)前綴的長(zhǎng)度。

同樣的,在 for 循環(huán)結(jié)束時(shí)還是要特殊判斷一下,處理query本身就是鍵的情況。

接下來(lái),我們來(lái)實(shí)現(xiàn)keysWithPrefix方法,得到所有前綴為prefix的鍵。

看過(guò)前文手把手刷二叉樹(shù)(總結(jié)篇)的讀者應(yīng)該可以想到,先利用getNode函數(shù)在 Trie 樹(shù)中找到prefix對(duì)應(yīng)的節(jié)點(diǎn)x,然施展多叉樹(shù)的遍歷算法,遍歷以x為根的這棵 Trie 樹(shù),找到所有鍵值對(duì):

30ff8a4e-d0f1-11ec-bce3-dac502259ad0.jpg

代碼實(shí)現(xiàn)如下:

//搜索前綴為prefix的所有鍵
publicListkeysWithPrefix(Stringprefix){
Listres=newLinkedList<>();
//找到匹配prefix在Trie樹(shù)中的那個(gè)節(jié)點(diǎn)
TrieNodex=getNode(root,prefix);
if(x==null){
returnres;
}
//DFS遍歷以x為根的這棵Trie樹(shù)
traverse(x,newStringBuilder(prefix),res);
returnres;
}

//遍歷以node節(jié)點(diǎn)為根的Trie樹(shù),找到所有鍵
privatevoidtraverse(TrieNodenode,StringBuilderpath,Listres){
if(node==null){
//到達(dá)Trie樹(shù)底部葉子結(jié)點(diǎn)
return;
}

if(node.val!=null){
//找到一個(gè)key,添加到結(jié)果列表中
res.add(path.toString());
}

//回溯算法遍歷框架
for(charc=0;c//做選擇
path.append(c);
traverse(node.children[c],path,res);
//撤銷(xiāo)選擇
path.deleteCharAt(path.length()-1);
}
}

這段代碼中traverse函數(shù)你可能看起來(lái)特別熟悉,就是回溯算法核心套路中講的回溯算法代碼框架。

關(guān)于回溯算法框架和標(biāo)準(zhǔn)多叉樹(shù)框架的區(qū)別我在圖論算法基礎(chǔ)中探討過(guò),關(guān)鍵在于遍歷「節(jié)點(diǎn)」和遍歷「樹(shù)枝」的區(qū)別。由于 Trie 樹(shù)將字符存儲(chǔ)在「樹(shù)枝」上,traverse函數(shù)是在遍歷樹(shù)枝上的字符,所以采用的是回溯算法框架。

另外,再注意一下這段邏輯:

//回溯算法遍歷框架
for(charc=0;c//做選擇
path.append(c);
traverse(node.children[c],path,res);
//撤銷(xiāo)選擇
path.deleteCharAt(path.length()-1);
}

回顧一下我們 Trie 樹(shù)的圖:

301d5c64-d0f1-11ec-bce3-dac502259ad0.jpg

你是否會(huì)有疑問(wèn):代碼中 for 循環(huán)會(huì)執(zhí)行 256 次,但是圖中的一個(gè)節(jié)點(diǎn)只有幾個(gè)子節(jié)點(diǎn),也就是說(shuō)每個(gè)節(jié)點(diǎn)的children數(shù)組中大部分都是空指針,這不會(huì)有問(wèn)題嗎?

是不是應(yīng)該把代碼改成這樣:

//回溯算法遍歷框架
for(charc=0;c//做選擇
path.append(c);
if(node.children[c]!=null){
traverse(node.children[c],path,res);
}
//撤銷(xiāo)選擇
path.deleteCharAt(path.length()-1);
}

答案是,改不改都行,這兩種寫(xiě)法從邏輯上講完全相同,因?yàn)?code style="margin-right:2px;margin-left:2px;padding:2px 4px;font-size:inherit;line-height:inherit;color:rgb(233,105,0);background:rgb(248,248,248);">traverse函數(shù)開(kāi)始的時(shí)候如果發(fā)現(xiàn)node == null也會(huì)直接返回。

我為了保持框架的一致性,就沒(méi)有在 for 循環(huán)中判斷子節(jié)點(diǎn)是否為空,而是依賴遞歸函數(shù)的 base case。當(dāng)然你完全可以按照自己的喜好來(lái)實(shí)現(xiàn)。

下面來(lái)實(shí)現(xiàn)keysWithPattern方法,使用通配符來(lái)匹配多個(gè)鍵,其關(guān)鍵就在于通配符.可以匹配所有字符。

在代碼實(shí)現(xiàn)上,用path變量記錄匹配鍵的路徑,遇到通配符時(shí)使用類(lèi)似回溯算法的框架就行了:

//通配符.匹配任意字符
publicListkeysWithPattern(Stringpattern){
Listres=newLinkedList<>();
traverse(root,newStringBuilder(),pattern,0,res);
returnres;
}

//遍歷函數(shù),嘗試在「以node為根的Trie樹(shù)中」匹配pattern[i..]
privatevoidtraverse(TrieNodenode,StringBuilderpath,Stringpattern,inti,Listres){
if(node==null){
//樹(shù)枝不存在,即匹配失敗
return;
}
if(i==pattern.length()){
//pattern匹配完成
if(node.val!=null){
//如果這個(gè)節(jié)點(diǎn)存儲(chǔ)著val,則找到一個(gè)匹配的鍵
res.add(path.toString());
}
return;
}
charc=pattern.charAt(i);
if(c=='.'){
//pattern[i]是通配符,可以變化成任意字符
//多叉樹(shù)(回溯算法)遍歷框架
for(charj=0;j1,res);
path.deleteCharAt(path.length()-1);
}
}else{
//pattern[i]是普通字符c
path.append(c);
traverse(node.children[c],path,pattern,i+1,res);
path.deleteCharAt(path.length()-1);
}
}

下面這個(gè) GIF 畫(huà)了匹配"t.a."的過(guò)程,應(yīng)該就容易理解上述代碼的邏輯了:

31a62322-d0f1-11ec-bce3-dac502259ad0.gif

可以看到,keysWithPatternkeysWithPrefix的實(shí)現(xiàn)是有些類(lèi)似的,而且這兩個(gè)函數(shù)還有一個(gè)潛在的特性:它們返回的結(jié)果列表一定是符合「字典序」的。

原因應(yīng)該不難理解,每一個(gè)節(jié)點(diǎn)的children數(shù)組都是從左到右進(jìn)行遍歷,即按照 ASCII 碼從小到大的順序遞歸遍歷,得到的結(jié)果自然是符合字典序的。

好,現(xiàn)在我們實(shí)現(xiàn)了keysWithPattern方法得到模式串匹配的所有鍵,那你是否可以實(shí)現(xiàn)hasKeyWithPattern方法,僅僅判斷是否存在鍵匹配模式串?

//一個(gè)偷懶的實(shí)現(xiàn)
publicbooleanhasKeyWithPattern(Stringpattern){
return!keysWithPattern(pattern).isEmpty();
}

這是一個(gè)偷懶的實(shí)現(xiàn),因?yàn)樗膹?fù)雜度比較高。我們的目的僅僅是判斷是否存在匹配模式串的鍵,你卻把所有匹配的鍵都算出來(lái)了,這顯然是沒(méi)有必要的。

我們只需稍微改寫(xiě)一下keysWithPattern方法就可以高效實(shí)現(xiàn)hasKeyWithPattern方法:

//判斷是和否存在前綴為prefix的鍵
publicbooleanhasKeyWithPattern(Stringpattern){
//從root節(jié)點(diǎn)開(kāi)始匹配pattern[0..]
returnhasKeyWithPattern(root,pattern,0);
}

//函數(shù)定義:從 node 節(jié)點(diǎn)開(kāi)始匹配 pattern[i..],返回是否成功匹配
privatebooleanhasKeyWithPattern(TrieNodenode,Stringpattern,inti){
if(node==null){
//樹(shù)枝不存在,即匹配失敗
returnfalse;
}
if(i==pattern.length()){
//模式串走到頭了,看看匹配到的是否是一個(gè)鍵
returnnode.val!=null;
}
charc=pattern.charAt(i);
//沒(méi)有遇到通配符
if(c!='.'){
//從node.children[c]節(jié)點(diǎn)開(kāi)始匹配pattern[i+1..]
returnhasKeyWithPattern(node.children[c],pattern,i+1);
}
//遇到通配符
for(intj=0;j//pattern[i]可以變化成任意字符,嘗試所有可能,只要遇到一個(gè)匹配成功就返回
if(hasKeyWithPattern(node.children[j],pattern,i+1)){
returntrue;
}
}
//都沒(méi)有匹配
returnfalse;
}

有之前的鋪墊,這個(gè)實(shí)現(xiàn)應(yīng)該是不難理解的,類(lèi)似于回溯算法解數(shù)獨(dú)游戲中找到一個(gè)可行解就提前結(jié)束遞歸的做法。

到這里,TrieMap的所有和前綴相關(guān)的方法都實(shí)現(xiàn)完了,還剩下putremove這兩個(gè)基本方法了,其實(shí)它們的難度不大,就是遞歸修改數(shù)據(jù)結(jié)構(gòu)的那一套,如果不熟悉的話可以參見(jiàn)二叉搜索樹(shù)基本操作

先說(shuō)put方法的實(shí)現(xiàn)吧,直接看代碼:

//在map中添加或修改鍵值對(duì)
publicvoidput(Stringkey,Vval){
if(!containsKey(key)){
//新增鍵值對(duì)
size++;
}
//需要一個(gè)額外的輔助函數(shù),并接收其返回值
root=put(root,key,val,0);
}

//定義:向以 node 為根的 Trie 樹(shù)中插入 key[i..],返回插入完成后的根節(jié)點(diǎn)
privateTrieNodeput(TrieNodenode,Stringkey,Vval,inti){
if(node==null){
//如果樹(shù)枝不存在,新建
node=newTrieNode<>();
}
if(i==key.length()){
//key的路徑已插入完成,將值val存入節(jié)點(diǎn)
node.val=val;
returnnode;
}
charc=key.charAt(i);
//遞歸插入子節(jié)點(diǎn),并接收返回值
node.children[c]=put(node.children[c],key,val,i+1);
returnnode;
}

因?yàn)槭沁f歸修改數(shù)據(jù)結(jié)構(gòu),所以我們必須額外創(chuàng)建一個(gè)返回類(lèi)型為TrieNode的輔助函數(shù),并且在遞歸調(diào)用的時(shí)候接收其返回值,拼接到父節(jié)點(diǎn)上。

前文說(shuō)了,Trie 樹(shù)中的鍵就是「樹(shù)枝」,值就是「節(jié)點(diǎn)」,所以插入的邏輯就是沿路新建「樹(shù)枝」,把key的整條「樹(shù)枝」構(gòu)建出來(lái)之后,在樹(shù)枝末端的「節(jié)點(diǎn)」中存儲(chǔ)val

32a84cbe-d0f1-11ec-bce3-dac502259ad0.gif

最后,我們說(shuō)一下remove函數(shù),似乎所有數(shù)據(jù)結(jié)構(gòu)的刪除操作相對(duì)其他操作都會(huì)更復(fù)雜一些。

比如說(shuō)下圖這個(gè)場(chǎng)景,如果你想刪除鍵"team",那么需要?jiǎng)h掉"eam"這條樹(shù)枝才是符合邏輯的:

32bba692-d0f1-11ec-bce3-dac502259ad0.jpg

刪多了肯定不行,但刪少了也不行,否則前文實(shí)現(xiàn)的hasKeyWithPrefix就會(huì)出錯(cuò)。

那么如何控制算法來(lái)正確地進(jìn)行刪除呢?

首先,遞歸修改數(shù)據(jù)結(jié)構(gòu)的時(shí)候,如果一個(gè)節(jié)點(diǎn)想刪掉自己,直接返回空指針就行了。

其次,一個(gè)節(jié)點(diǎn)如何知道自己是否需要被刪除呢?主要看自己的val字段是否為空以及自己的children數(shù)組是否全都是空指針。

這里就要利用前文手把手刷二叉樹(shù)(總結(jié)篇)中說(shuō)到的后序位置的特點(diǎn)了:

一個(gè)節(jié)點(diǎn)要先遞歸處理子樹(shù),然后在后序位置檢查自己的val字段和children列表,判斷自己是否需要被刪除。

如果自己的val字段為空,說(shuō)明自己沒(méi)有存儲(chǔ)值,如果同時(shí)自己的children數(shù)組全是空指針,說(shuō)明自己下面也沒(méi)有接樹(shù)枝,即不是任何一個(gè)鍵的前綴。這種情況下這個(gè)節(jié)點(diǎn)就沒(méi)有存在的意義了,應(yīng)該刪掉自己。

直接看代碼:

//在Map中刪除key
publicvoidremove(Stringkey){
if(!containsKey(key)){
return;
}
//遞歸修改數(shù)據(jù)結(jié)構(gòu)要接收函數(shù)的返回值
root=remove(root,key,0);
size--;
}

//定義:在以 node 為根的 Trie 樹(shù)中刪除 key[i..],返回刪除后的根節(jié)點(diǎn)
privateTrieNoderemove(TrieNodenode,Stringkey,inti){
if(node==null){
returnnull;
}
if(i==key.length()){
//找到了key對(duì)應(yīng)的TrieNode,刪除val
node.val=null;
}else{
charc=key.charAt(i);
//遞歸去子樹(shù)進(jìn)行刪除
node.children[c]=remove(node.children[c],key,i+1);
}
//后序位置,遞歸路徑上的節(jié)點(diǎn)可能需要被清理
if(node.val!=null){
//如果該TireNode存儲(chǔ)著val,不需要被清理
returnnode;
}
//檢查該TrieNode是否還有后綴
for(intc=0;cif(node.children[c]!=null){
//只要存在一個(gè)子節(jié)點(diǎn)(后綴樹(shù)枝),就不需要被清理
returnnode;
}
}
//既沒(méi)有存儲(chǔ)val,也沒(méi)有后綴樹(shù)枝,則該節(jié)點(diǎn)需要被清理
returnnull;
}

到這里,TrieMap的所有 API 就實(shí)現(xiàn)完了,完整代碼如下:

classTrieMap<V>{
//ASCII碼個(gè)數(shù)
privatestaticfinalintR=256;
//當(dāng)前存在Map中的鍵值對(duì)個(gè)數(shù)
privateintsize=0;
//Trie樹(shù)的根節(jié)點(diǎn)
privateTrieNoderoot=null;

privatestaticclassTrieNode<V>{
Vval=null;
TrieNode[]children=newTrieNode[R];
}

/*****增/改*****/

//在map中添加或修改鍵值對(duì)
publicvoidput(Stringkey,Vval){
if(!containsKey(key)){
//新增鍵值對(duì)
size++;
}
//需要一個(gè)額外的輔助函數(shù),并接收其返回值
root=put(root,key,val,0);
}

//定義:向以 node 為根的 Trie 樹(shù)中插入 key[i..],返回插入完成后的根節(jié)點(diǎn)
privateTrieNodeput(TrieNodenode,Stringkey,Vval,inti){
if(node==null){
//如果樹(shù)枝不存在,新建
node=newTrieNode<>();
}
if(i==key.length()){
//key的路徑已插入完成,將值val存入節(jié)點(diǎn)
node.val=val;
returnnode;
}
charc=key.charAt(i);
//遞歸插入子節(jié)點(diǎn),并接收返回值
node.children[c]=put(node.children[c],key,val,i+1);
returnnode;
}

/*****刪*****/

//在Map中刪除key
publicvoidremove(Stringkey){
if(!containsKey(key)){
return;
}
//遞歸修改數(shù)據(jù)結(jié)構(gòu)要接收函數(shù)的返回值
root=remove(root,key,0);
size--;
}

//定義:在以 node 為根的 Trie 樹(shù)中刪除 key[i..],返回刪除后的根節(jié)點(diǎn)
privateTrieNoderemove(TrieNodenode,Stringkey,inti){
if(node==null){
returnnull;
}
if(i==key.length()){
//找到了key對(duì)應(yīng)的TrieNode,刪除val
node.val=null;
}else{
charc=key.charAt(i);
//遞歸去子樹(shù)進(jìn)行刪除
node.children[c]=remove(node.children[c],key,i+1);
}
//后序位置,遞歸路徑上的節(jié)點(diǎn)可能需要被清理
if(node.val!=null){
//如果該TireNode存儲(chǔ)著val,不需要被清理
returnnode;
}
//檢查該TrieNode是否還有后綴
for(intc=0;cif(node.children[c]!=null){
//只要存在一個(gè)子節(jié)點(diǎn)(后綴樹(shù)枝),就不需要被清理
returnnode;
}
}
//既沒(méi)有存儲(chǔ)val,也沒(méi)有后綴樹(shù)枝,則該節(jié)點(diǎn)需要被清理
returnnull;
}

/*****查*****/

//搜索key對(duì)應(yīng)的值,不存在則返回null
publicVget(Stringkey){
//從root開(kāi)始搜索key
TrieNodex=getNode(root,key);
if(x==null||x.val==null){
//x為空或x的val字段為空都說(shuō)明key沒(méi)有對(duì)應(yīng)的值
returnnull;
}
returnx.val;
}

//判斷key是否存在在Map中
publicbooleancontainsKey(Stringkey){
returnget(key)!=null;
}

//判斷是和否存在前綴為prefix的鍵
publicbooleanhasKeyWithPrefix(Stringprefix){
//只要能找到一個(gè)節(jié)點(diǎn),就是存在前綴
returngetNode(root,prefix)!=null;
}

//在所有鍵中尋找query的最短前綴
publicStringshortestPrefixOf(Stringquery){
TrieNodep=root;
//從節(jié)點(diǎn)node開(kāi)始搜索key
for(inti=0;iif(p==null){
//無(wú)法向下搜索
return"";
}
if(p.val!=null){
//找到一個(gè)鍵是query的前綴
returnquery.substring(0,i);
}
//向下搜索
charc=query.charAt(i);
p=p.children[c];
}

if(p!=null&&p.val!=null){
//如果query本身就是一個(gè)鍵
returnquery;
}
return"";
}

//在所有鍵中尋找query的最長(zhǎng)前綴
publicStringlongestPrefixOf(Stringquery){
TrieNodep=root;
//記錄前綴的最大長(zhǎng)度
intmax_len=0;

//從節(jié)點(diǎn)node開(kāi)始搜索key
for(inti=0;iif(p==null){
//無(wú)法向下搜索
break;
}
if(p.val!=null){
//找到一個(gè)鍵是query的前綴,更新前綴的最大長(zhǎng)度
max_len=i;
}
//向下搜索
charc=query.charAt(i);
p=p.children[c];
}

if(p!=null&&p.val!=null){
//如果query本身就是一個(gè)鍵
returnquery;
}
returnquery.substring(0,max_len);
}

//搜索前綴為prefix的所有鍵
publicListkeysWithPrefix(Stringprefix){
Listres=newLinkedList<>();
//找到匹配prefix在Trie樹(shù)中的那個(gè)節(jié)點(diǎn)
TrieNodex=getNode(root,prefix);
if(x==null){
returnres;
}
//DFS遍歷以x為根的這棵Trie樹(shù)
traverse(x,newStringBuilder(prefix),res);
returnres;
}

//遍歷以node節(jié)點(diǎn)為根的Trie樹(shù),找到所有鍵
privatevoidtraverse(TrieNodenode,StringBuilderpath,Listres){
if(node==null){
//到達(dá)Trie樹(shù)底部葉子結(jié)點(diǎn)
return;
}

if(node.val!=null){
//找到一個(gè)key,添加到結(jié)果列表中
res.add(path.toString());
}

//回溯算法遍歷框架
for(charc=0;c//做選擇
path.append(c);
traverse(node.children[c],path,res);
//撤銷(xiāo)選擇
path.deleteCharAt(path.length()-1);
}
}

//通配符.匹配任意字符
publicListkeysWithPattern(Stringpattern){
Listres=newLinkedList<>();
traverse(root,newStringBuilder(),pattern,0,res);
returnres;
}

//遍歷函數(shù),嘗試在「以node為根的Trie樹(shù)中」匹配pattern[i..]
privatevoidtraverse(TrieNodenode,StringBuilderpath,Stringpattern,inti,Listres){
if(node==null){
//樹(shù)枝不存在,即匹配失敗
return;
}
if(i==pattern.length()){
//pattern匹配完成
if(node.val!=null){
//如果這個(gè)節(jié)點(diǎn)存儲(chǔ)著val,則找到一個(gè)匹配的鍵
res.add(path.toString());
}
return;
}
charc=pattern.charAt(i);
if(c=='.'){
//pattern[i]是通配符,可以變化成任意字符
//多叉樹(shù)(回溯算法)遍歷框架
for(charj=0;j1,res);
path.deleteCharAt(path.length()-1);
}
}else{
//pattern[i]是普通字符c
path.append(c);
traverse(node.children[c],path,pattern,i+1,res);
path.deleteCharAt(path.length()-1);
}
}

//判斷是和否存在前綴為prefix的鍵
publicbooleanhasKeyWithPattern(Stringpattern){
//從root節(jié)點(diǎn)開(kāi)始匹配pattern[0..]
returnhasKeyWithPattern(root,pattern,0);
}

//函數(shù)定義:從 node 節(jié)點(diǎn)開(kāi)始匹配 pattern[i..],返回是否成功匹配
privatebooleanhasKeyWithPattern(TrieNodenode,Stringpattern,inti){
if(node==null){
//樹(shù)枝不存在,即匹配失敗
returnfalse;
}
if(i==pattern.length()){
//模式串走到頭了,看看匹配到的是否是一個(gè)鍵
returnnode.val!=null;
}
charc=pattern.charAt(i);
//沒(méi)有遇到通配符
if(c!='.'){
//從node.children[c]節(jié)點(diǎn)開(kāi)始匹配pattern[i+1..]
returnhasKeyWithPattern(node.children[c],pattern,i+1);
}
//遇到通配符
for(intj=0;j//pattern[i]可以變化成任意字符,嘗試所有可能,只要遇到一個(gè)匹配成功就返回
if(hasKeyWithPattern(node.children[j],pattern,i+1)){
returntrue;
}
}
//都沒(méi)有匹配
returnfalse;
}

//從節(jié)點(diǎn)node開(kāi)始搜索key,如果存在返回對(duì)應(yīng)節(jié)點(diǎn),否則返回null
privateTrieNodegetNode(TrieNodenode,Stringkey){
TrieNodep=node;
//從節(jié)點(diǎn)node開(kāi)始搜索key
for(inti=0;iif(p==null){
//無(wú)法向下搜索
returnnull;
}
//向下搜索
charc=key.charAt(i);
p=p.children[c];
}
returnp;
}

publicintsize(){
returnsize;
}
}

接下來(lái)我們只要對(duì)TrieMap做簡(jiǎn)單的封裝,即可實(shí)現(xiàn)TrieSet

classTrieSet{
//底層用一個(gè)TrieMap,鍵就是TrieSet,值僅僅起到占位的作用
//值的類(lèi)型可以隨便設(shè)置,我參考Java標(biāo)準(zhǔn)庫(kù)設(shè)置成Object
privatefinalTrieMapmap=newTrieMap<>();

/*****增*****/

//在集合中添加元素key
publicvoidadd(Stringkey){
map.put(key,newObject());
}

/*****刪*****/

//從集合中刪除元素key
publicvoidremove(Stringkey){
map.remove(key);
}

/*****查*****/

//判斷元素key是否存在集合中
publicbooleancontains(Stringkey){
returnmap.containsKey(key);
}

//在集合中尋找query的最短前綴
publicStringshortestPrefixOf(Stringquery){
returnmap.shortestPrefixOf(query);
}

//在集合中尋找query的最長(zhǎng)前綴
publicStringlongestPrefixOf(Stringquery){
returnmap.longestPrefixOf(query);
}

//在集合中搜索前綴為prefix的所有元素
publicListkeysWithPrefix(Stringprefix){
returnmap.keysWithPrefix(prefix);
}

//判斷集合中是否存在前綴為prefix的元素
publicbooleanhasKeyWithPrefix(Stringprefix){
returnmap.hasKeyWithPrefix(prefix);
}

//通配符.匹配任意字符,返回集合中匹配pattern的所有元素
publicListkeysWithPattern(Stringpattern){
returnmap.keysWithPattern(pattern);
}

//通配符.匹配任意字符,判斷集合中是否存在匹配pattern的元素
publicbooleanhasKeyWithPattern(Stringpattern){
returnmap.hasKeyWithPattern(pattern);
}

//返回集合中元素的個(gè)數(shù)
publicintsize(){
returnmap.size();
}
}

	

有了TrieMapTrieSet,力扣上所有前綴樹(shù)相關(guān)的題目都可以直接套用了,下面我舉幾個(gè)題目實(shí)踐一下。

秒殺題目

首先需要說(shuō)明,上文實(shí)現(xiàn)的算法模板的執(zhí)行效率在具體的題目里面肯定是有優(yōu)化空間的

比如力扣前綴樹(shù)相關(guān)題目的輸入都被限制在小寫(xiě)英文字母a-z,所以TrieNode其實(shí)不用維護(hù)一個(gè)大小為 256 的children數(shù)組,大小設(shè)置為 26 就夠了,可以減小時(shí)間和空間上的復(fù)雜度。

不過(guò)本文只考慮模板的通用性,重在思路,所以就直接套用上文給出的算法模板解題,具體實(shí)現(xiàn)上的細(xì)節(jié)優(yōu)化我集成在刷題插件的「思路」按鈕中。

先看下力扣第 208 題「實(shí)現(xiàn)前綴樹(shù)」:

33188150-d0f1-11ec-bce3-dac502259ad0.png

題目讓我們實(shí)現(xiàn)的幾個(gè)函數(shù)其實(shí)就是TrieSet的部分 API,所以直接封裝一個(gè)TrieSet就能解決這道題了:

classTrie{
//直接封裝TrieSet
TrieSetset=newTrieSet();

//插入一個(gè)元素
publicvoidinsert(Stringword){
set.add(word);
}

//判斷元素是否在集合中
publicbooleansearch(Stringword){
returnset.contains(word);
}

//判斷集合中是否有前綴為prefix的元素
publicbooleanstartsWith(Stringprefix){
returnset.hasKeyWithPrefix(prefix);
}
}

classTrieSet{/*見(jiàn)上文*/}

classTrieMap{/*見(jiàn)上文*/}

接下來(lái)看下力扣第 648 題「單詞替換」:

33750308-d0f1-11ec-bce3-dac502259ad0.png

現(xiàn)在你學(xué)過(guò) Trie 樹(shù)結(jié)構(gòu),應(yīng)該可以看出來(lái)這題就在考察最短前綴問(wèn)題。

所以可以把輸入的詞根列表dict存入TrieSet,然后直接復(fù)用我們實(shí)現(xiàn)的shortestPrefixOf函數(shù)就行了:

StringreplaceWords(Listdict,Stringsentence){
//先將詞根都存入TrieSet
TrieSetset=newTrieSet();
for(Stringkey:dict){
set.add(key);
}
StringBuildersb=newStringBuilder();
String[]words=sentence.split("");
//處理句子中的單詞
for(inti=0;i//在Trie樹(shù)中搜索最短詞根(最短前綴)
Stringprefix=set.shortestPrefixOf(words[i]);
if(!prefix.isEmpty()){
//如果搜索到了,改寫(xiě)為詞根
sb.append(prefix);
}else{
//否則,原樣放回
sb.append(words[i]);
}

if(i!=words.length-1){
//添加單詞之間的空格
sb.append('');
}
}

returnsb.toString();
}

classTrieSet{/*見(jiàn)上文*/}

classTrieMap{/*見(jiàn)上文*/}

繼續(xù)看力扣第 211 題「添加與搜索單詞 - 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)」:

33a7caa4-d0f1-11ec-bce3-dac502259ad0.png

這道題的考點(diǎn)就在于這個(gè)search函數(shù)進(jìn)行通配符匹配,其實(shí)就是我們給TrieSet實(shí)現(xiàn)的hasKeyWithPattern方法,直接套就行了:

classWordDictionary{
TrieSetset=newTrieSet();

//在TrieSet中添加元素
publicvoidaddWord(Stringword){
set.add(word);
}

//通配符匹配元素
publicbooleansearch(Stringword){
returnset.hasKeyWithPattern(word);
}
}

classTrieSet{/*見(jiàn)上文*/}

classTrieMap{/*見(jiàn)上文*/}

上面列舉的這幾道題用的都是TrieSet,下面來(lái)看看TrieMap的題目。

先看力扣第 1804 題「實(shí)現(xiàn)前綴樹(shù) II」:

340d2502-d0f1-11ec-bce3-dac502259ad0.png

這題就可以用到TrieMap,每個(gè)插入的word就是鍵,插入的次數(shù)就是對(duì)應(yīng)的值,然后復(fù)用TrieMap的 API 就能實(shí)現(xiàn)題目要求的這些函數(shù):

classTrie{
//封裝我們實(shí)現(xiàn)的TrieMap
TrieMapmap=newTrieMap<>();

//插入word并記錄插入次數(shù)
publicvoidinsert(Stringword){
if(!map.containsKey(word)){
map.put(word,1);
}else{
map.put(word,map.get(word)+1);
}
}

//查詢word插入的次數(shù)
publicintcountWordsEqualTo(Stringword){
if(!map.containsKey(word)){
return0;
}
returnmap.get(word);
}

//累加前綴為prefix的鍵的插入次數(shù)總和
publicintcountWordsStartingWith(Stringprefix){
intres=0;
for(Stringkey:map.keysWithPrefix(prefix)){
res+=map.get(key);
}
returnres;
}

//word的插入次數(shù)減一
publicvoiderase(Stringword){
intfreq=map.get(word);
if(freq-1==0){
map.remove(word);
}else{
map.put(word,freq-1);
}
}
}

classTrieMap{/*見(jiàn)上文*/}

反正都是直接套模板,也沒(méi)什么意思,再看最后一道題目吧,這是力扣第 677 題「鍵值映射」:

341f1262-d0f1-11ec-bce3-dac502259ad0.png

這道題還是標(biāo)準(zhǔn)的TrieMap的應(yīng)用,直接看代碼吧:

classMapSum{
//封裝我們實(shí)現(xiàn)的TrieMap
TrieMapmap=newTrieMap<>();

//插入鍵值對(duì)
publicvoidinsert(Stringkey,intval){
map.put(key,val);
}

//累加所有前綴為prefix的鍵的值
publicintsum(Stringprefix){
Listkeys=map.keysWithPrefix(prefix);
intres=0;
for(Stringkey:keys){
res+=map.get(key);
}
returnres;
}
}

classTrieMap{/*見(jiàn)上文*/}

Trie 樹(shù)這種數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)原理和題目實(shí)踐就講完了,如果你能夠看到這里,真得給你鼓掌。

可以看到,我最近寫(xiě)的圖論算法以及本文講的 Trie 樹(shù)算法,都和「樹(shù)」這種基本數(shù)據(jù)結(jié)構(gòu)相關(guān),所以我才會(huì)在刷題插件中集成手把手刷 100 道二叉樹(shù)題目的功能。

最后,紙上得來(lái)終覺(jué)淺,絕知此事要躬行,我建議最好親手實(shí)現(xiàn)一遍上面的代碼,去把題目刷一遍,才能對(duì) Trie 樹(shù)有更深入的理解。

之后我準(zhǔn)備繼續(xù)講解一些基本數(shù)據(jù)結(jié)構(gòu)在高級(jí)數(shù)據(jù)結(jié)構(gòu)或?qū)嶋H算法題中的應(yīng)用,大家期待就好。



原文標(biāo)題:前綴樹(shù)算法模板秒殺 5 道算法題

文章出處:【微信公眾號(hào):算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 算法
    +關(guān)注

    關(guān)注

    23

    文章

    4797

    瀏覽量

    98447
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4975

    瀏覽量

    74314
  • 數(shù)據(jù)結(jié)構(gòu)

    關(guān)注

    3

    文章

    573

    瀏覽量

    41671

原文標(biāo)題:前綴樹(shù)算法模板秒殺 5 道算法題

文章出處:【微信號(hào):TheAlgorithm,微信公眾號(hào):算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    GPIB命令的數(shù)據(jù)結(jié)構(gòu)

    GPIB命令結(jié)點(diǎn);并考慮程序實(shí)現(xiàn)的效率問(wèn)題以及管理維護(hù)方面的因素,對(duì)普通的樹(shù)進(jìn)行改造,從而形成特有的"GPIB命令樹(shù)"?!娟P(guān)鍵詞】:通用接口總線(GPIB);;數(shù)據(jù)結(jié)構(gòu);;
    發(fā)表于 04-24 09:44

    收藏 | 程序員面試,你必須知道的8大數(shù)據(jù)結(jié)構(gòu)

    可以用兩種形式表示:鄰接矩陣鄰接表常見(jiàn)圖遍歷算法廣度優(yōu)先搜索深度優(yōu)先搜索面試中關(guān)于圖的常見(jiàn)問(wèn)題:實(shí)現(xiàn)廣度和深度優(yōu)先搜索檢查圖是否為樹(shù)計(jì)算圖的邊數(shù)找到兩個(gè)頂點(diǎn)之間的最短路徑樹(shù)樹(shù)形結(jié)構(gòu)是一
    發(fā)表于 09-30 09:35

    常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)

    順序表結(jié)構(gòu)的底層實(shí)現(xiàn)借助的就是數(shù)組,因此對(duì)于初學(xué)者來(lái)說(shuō),可以把順序表完全等價(jià)為數(shù)組,但實(shí)則不是這樣。數(shù)據(jù)結(jié)構(gòu)是研究數(shù)據(jù)存儲(chǔ)方式的一門(mén)學(xué)科,它囊括的都是各種存儲(chǔ)
    發(fā)表于 05-10 07:58

    GPIB命令的數(shù)據(jù)結(jié)構(gòu)

    針對(duì)GPIB命令的結(jié)構(gòu),提出一種存儲(chǔ)GPIB命令的數(shù)據(jù)結(jié)構(gòu)。根據(jù)GPIB命令的層次關(guān)系的特點(diǎn),選擇數(shù)據(jù)結(jié)構(gòu)中“樹(shù)”的概念來(lái)存儲(chǔ)GPIB命令結(jié)點(diǎn);并考慮程序
    發(fā)表于 02-10 16:20 ?70次下載

    GPIB命令的數(shù)據(jù)結(jié)構(gòu)

    針對(duì)GPIB命令的結(jié)構(gòu),提出一種存儲(chǔ)GPIB命令的數(shù)據(jù)結(jié)構(gòu)。根據(jù)GPIB命令的層次關(guān)系的特點(diǎn),選擇數(shù)據(jù)結(jié)構(gòu)中“樹(shù)”的概念來(lái)存儲(chǔ)GPIB命令結(jié)點(diǎn);并考慮程序
    發(fā)表于 01-04 10:13 ?0次下載

    java數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí)

    數(shù)據(jù)結(jié)構(gòu)是對(duì)計(jì)算機(jī)內(nèi)存中的數(shù)據(jù)的一種安排,數(shù)據(jù)結(jié)構(gòu)包括 數(shù)組, 鏈表, 棧, 二叉樹(shù), 哈希表等,算法則對(duì)對(duì)這些結(jié)構(gòu)中的
    發(fā)表于 11-29 09:46 ?1106次閱讀

    關(guān)于二叉樹(shù)一些數(shù)據(jù)結(jié)構(gòu)和算法相關(guān)的題目

    最近總結(jié)了一些數(shù)據(jù)結(jié)構(gòu)和算法相關(guān)的題目,這是第一篇文章,關(guān)于二叉樹(shù)的。
    的頭像 發(fā)表于 02-07 13:57 ?3692次閱讀

    什么是數(shù)據(jù)結(jié)構(gòu)?為什么要學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)?數(shù)據(jù)結(jié)構(gòu)的應(yīng)用實(shí)例分析

    本文檔的主要內(nèi)容詳細(xì)介紹的是什么是數(shù)據(jù)結(jié)構(gòu)?為什么要學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)?數(shù)據(jù)結(jié)構(gòu)的應(yīng)用實(shí)例分析包括了:數(shù)據(jù)結(jié)構(gòu)在串口通信當(dāng)中的應(yīng)用,數(shù)據(jù)結(jié)構(gòu)在按鍵
    發(fā)表于 09-26 15:45 ?14次下載
    什么是<b class='flag-5'>數(shù)據(jù)結(jié)構(gòu)</b>?為什么要學(xué)習(xí)<b class='flag-5'>數(shù)據(jù)結(jié)構(gòu)</b>?<b class='flag-5'>數(shù)據(jù)結(jié)構(gòu)</b>的應(yīng)用實(shí)例分析

    Linux 內(nèi)核里的數(shù)據(jù)結(jié)構(gòu)關(guān)鍵:基數(shù)樹(shù)

    基數(shù)樹(shù)是一種 壓縮的字典樹(shù)compressed trie ,而字典樹(shù)實(shí)現(xiàn)了關(guān)聯(lián)數(shù)組接口并允許以 鍵值對(duì) 方式存儲(chǔ)值的一種
    發(fā)表于 04-28 16:04 ?1222次閱讀

    Linux內(nèi)核數(shù)據(jù)結(jié)構(gòu):Radix 樹(shù)

    首先說(shuō)明一下什么是 radix tree ,Radix tree 是一個(gè) 壓縮 trie, trie 是一種通過(guò)保存關(guān)聯(lián)數(shù)組(associative array)來(lái)提供 關(guān)鍵字-值(key-value) 存儲(chǔ)與查找的數(shù)據(jù)結(jié)構(gòu)。通
    發(fā)表于 05-14 17:22 ?2529次閱讀

    數(shù)據(jù)結(jié)構(gòu)樹(shù)”的詳細(xì)介紹

    ,咱們今天要嘮啥了。 之前給大家介紹了鏈表,棧,哈希表 等數(shù)據(jù)結(jié)構(gòu) 今天咱們來(lái)看一種新的數(shù)據(jù)結(jié)構(gòu)樹(shù)。 PS:本篇文章內(nèi)容較基礎(chǔ),對(duì)于沒(méi)有學(xué)過(guò)數(shù)據(jù)結(jié)構(gòu)的同學(xué)會(huì)有一些幫助,如果你已經(jīng)學(xué)過(guò)
    的頭像 發(fā)表于 05-25 15:28 ?3013次閱讀
    新<b class='flag-5'>數(shù)據(jù)結(jié)構(gòu)</b>“<b class='flag-5'>樹(shù)</b>”的詳細(xì)介紹

    數(shù)據(jù)結(jié)構(gòu)字典樹(shù)實(shí)現(xiàn)

    什么是字典樹(shù)字典樹(shù),是一種空間換時(shí)間的數(shù)據(jù)結(jié)構(gòu),又稱Trie樹(shù)、前綴樹(shù),是一種樹(shù)形
    的頭像 發(fā)表于 09-07 15:03 ?2784次閱讀
    <b class='flag-5'>數(shù)據(jù)結(jié)構(gòu)</b>字典<b class='flag-5'>樹(shù)</b>的<b class='flag-5'>實(shí)現(xiàn)</b>

    數(shù)據(jù)結(jié)構(gòu)與算法分析中的二叉樹(shù)與堆有關(guān)知識(shí)匯總

    該資料包括數(shù)據(jù)結(jié)構(gòu)與算法分析中的二叉樹(shù)與堆有關(guān)的一些知識(shí)
    發(fā)表于 11-03 09:37 ?0次下載

    C語(yǔ)言數(shù)據(jù)結(jié)構(gòu):什么是二叉樹(shù)?

    完全二叉樹(shù):完全二叉樹(shù)是效率很高的數(shù)據(jù)結(jié)構(gòu)。對(duì)于深度為K,有n個(gè)節(jié)點(diǎn)的二叉樹(shù),當(dāng)且僅當(dāng)每一個(gè)節(jié)點(diǎn)都與深度為K的滿二叉樹(shù)中編號(hào)從1至n的節(jié)點(diǎn)一
    的頭像 發(fā)表于 04-21 16:20 ?4626次閱讀

    redis數(shù)據(jù)結(jié)構(gòu)的底層實(shí)現(xiàn)

    Redis是一種內(nèi)存鍵值數(shù)據(jù)庫(kù),常用于緩存、消息隊(duì)列、實(shí)時(shí)數(shù)據(jù)分析等場(chǎng)景。它的高性能得益于其精心設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu)和底層實(shí)現(xiàn)。本文將詳細(xì)介紹Redis常用的
    的頭像 發(fā)表于 12-05 10:14 ?1395次閱讀
    延川县| 托里县| 新闻| 新田县| 图木舒克市| 乾安县| 东兰县| 平凉市| 奈曼旗| 康平县| 垦利县| 化州市| 澄迈县| 自贡市| 江安县| 永善县| 德昌县| 大埔县| 宁阳县| 祁门县| 辽阳县| 固安县| 图木舒克市| 凤翔县| 九寨沟县| 南召县| 南城县| 高邮市| 梨树县| 克什克腾旗| 宜丰县| 灵丘县| 玛纳斯县| 丽江市| 龙海市| 桦甸市| 沙雅县| 桃源县| 平陆县| 旬邑县| 肥西县|