詳解Java前綴樹Trie的原理及代碼實(shí)現(xiàn)
Trie的概念
Trie(發(fā)音類似 “try”)又被稱為前綴樹、字典樹。Trie利用字符串的公共前綴來高效地存儲(chǔ)和檢索字符串?dāng)?shù)據(jù)集中的關(guān)鍵詞,最大限度地減少無謂的字符串比較,其核心思想是用空間換時(shí)間。
Trie樹可被用來實(shí)現(xiàn)字符串查詢、前綴查詢、詞頻統(tǒng)計(jì)、自動(dòng)拼寫、補(bǔ)完檢查等等功能。
Trie樹的三個(gè)性質(zhì):
- 根節(jié)點(diǎn)不包含字符,除根節(jié)點(diǎn)外每一個(gè)節(jié)點(diǎn)都只包含一個(gè)字符。
- 從根節(jié)點(diǎn)到某一節(jié)點(diǎn),路徑上經(jīng)過的字符連接起來,為該節(jié)點(diǎn)對(duì)應(yīng)的字符串。
- 每個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)包含的字符都不相同。
Trie的實(shí)現(xiàn)
Trie是一顆非典型的多叉樹形結(jié)構(gòu),多叉樹是因?yàn)橐粋€(gè)節(jié)點(diǎn)可以有多個(gè)字節(jié)點(diǎn),而非典型在于節(jié)點(diǎn)中沒有專門的字段直接保存此節(jié)點(diǎn)的值,而是通過一個(gè)數(shù)組或者map保存了當(dāng)前節(jié)點(diǎn)的所有下層子節(jié)點(diǎn)的值,也正是因此,根節(jié)點(diǎn)不表示任何字符。
基本結(jié)構(gòu)
最簡(jiǎn)單的前綴樹的結(jié)構(gòu)如下:
- 內(nèi)部包含一個(gè)哈希表next,存儲(chǔ)著子節(jié)點(diǎn)的值到對(duì)應(yīng)的節(jié)點(diǎn)的映射關(guān)系。
- 還有一個(gè)布爾值isEnd,用來標(biāo)識(shí)該節(jié)點(diǎn)是否是一個(gè)字符串的結(jié)束。
- 調(diào)用無參構(gòu)造器將會(huì)初始化這兩個(gè)屬性。
實(shí)際上,還可以包含其他的屬性以實(shí)現(xiàn)特定的功能,例如加入count表示以當(dāng)前單詞結(jié)尾的單詞數(shù)量,加入prefix表示以該處節(jié)點(diǎn)之前的字符串為前綴的單詞數(shù)量。
另外,對(duì)于下層子節(jié)點(diǎn)的存儲(chǔ),如果字符串僅包含小寫字母,或者固定范圍的字符,那么我們可以使用定長(zhǎng)(例如26)的數(shù)組來表示next,這樣省去了hash操作的開銷,但同樣可能造成空間的浪費(fèi)。
class Trie { /** * 經(jīng)過該節(jié)點(diǎn)的字符串的下層節(jié)點(diǎn) */ Map<Character, Trie> next; /** * 該節(jié)點(diǎn)是否是一個(gè)字符串的結(jié)束 */ boolean isEnd; public Trie() { this.next = new HashMap<>(); this.isEnd = false; } }
構(gòu)建Trie
通過調(diào)用構(gòu)造器初始化一個(gè)Trie的根節(jié)點(diǎn),通過insert操作向前綴樹中插入關(guān)鍵詞字符串(模式串)。
可以看到其實(shí)現(xiàn)的方法比較簡(jiǎn)單:將字符串轉(zhuǎn)換為char數(shù)組,順序遍歷char數(shù)組的每個(gè)字符,然后從根節(jié)點(diǎn)開始判斷該節(jié)點(diǎn)的下層子節(jié)點(diǎn)映射next:
- 如果不包含此字符,那么加入一個(gè)新子節(jié)點(diǎn)進(jìn)去,值對(duì)應(yīng)著當(dāng)前字符。然后使用該子節(jié)點(diǎn),進(jìn)入下一次循環(huán)判斷下一個(gè)字符。
- 如果包含此字符或者新插入了節(jié)點(diǎn),那么當(dāng)前字符獲取對(duì)應(yīng)的子節(jié)點(diǎn),進(jìn)入下一次循環(huán)判斷下一個(gè)字符。
循環(huán)完畢,我們完成了當(dāng)前字符串的Trie構(gòu)建,那么還需要將最后一個(gè)節(jié)點(diǎn)的isEnd改為true,表示該節(jié)點(diǎn)是一個(gè)字符串的結(jié)束。
public void insert(String word) { //初始默認(rèn)為根節(jié)點(diǎn),根節(jié)點(diǎn)不包含任何字符 Trie cur = this; //遍歷該字符串的字符數(shù)組 for (char c : word.toCharArray()) { //如果該節(jié)點(diǎn)的下層不包含此字符,那么加入一個(gè)新節(jié)點(diǎn)進(jìn)去 if (!cur.next.containsKey(c)) { cur.next.put(c, new Trie()); } //查找下一層節(jié)點(diǎn) cur = cur.next.get(c); } //遍歷字符串完畢,最后的節(jié)點(diǎn)isEnd置為true,表示一個(gè)字符串的結(jié)束 cur.isEnd = true; }
查找字符串
基于Trie結(jié)構(gòu)可以查找字符串、匹配前綴、查找出現(xiàn)次數(shù)等等,這里我們給出查找字符串和查找前綴的方法。
比較簡(jiǎn)單,我們從字典樹的根開始,查找前綴:
- 如果子節(jié)點(diǎn)存在。沿著指針移動(dòng)到子節(jié)點(diǎn),繼續(xù)搜索下一個(gè)字符。
- 如果子節(jié)點(diǎn)不存在。說明字典樹中不包含該前綴,返回空指針。
可以看到查找字符串相比于匹配前綴,僅僅是多了一個(gè)最下層子節(jié)點(diǎn)是否是一個(gè)字符串的結(jié)束的判斷而已。
/** * 查找字符串 */ public boolean search(String word) { Trie end = searchPrefix(word); return end != null && end.isEnd; } /** * 匹配前綴 */ public boolean startsWith(String prefix) { return searchPrefix(prefix) != null; } private Trie searchPrefix(String prefix) { //初始默認(rèn)為根節(jié)點(diǎn),根節(jié)點(diǎn)不包含任何字符 Trie cur = this; //遍歷該字符串的字符數(shù)組 for (char c : prefix.toCharArray()) { //如果該節(jié)點(diǎn)的下層不包含此字符,那么直接返回null if (!cur.next.containsKey(c)) { return null; } //查找下一層節(jié)點(diǎn) cur = cur.next.get(c); } return cur; }
Trie的總結(jié)
假設(shè)我們加入了she、he、his、her這四個(gè)字符串,那么Trie的結(jié)構(gòu)如下,其中紅色節(jié)點(diǎn)表示其屬于某個(gè)字符串的尾部節(jié)點(diǎn)。
Trie時(shí)間復(fù)雜度:初始化為O(1),每次操作為O(N),N為插入或查找的字符串的長(zhǎng)度。
Trie空間復(fù)雜度:O(N),N表示Trie結(jié)點(diǎn)數(shù)量,或者說所有插入字符串的長(zhǎng)度之和(減去相同前綴長(zhǎng)度)。如果是采用定長(zhǎng)數(shù)組表示next,那么空間復(fù)雜度為O(N*M),M表示字符集的大小,即數(shù)組長(zhǎng)度。
可以看到,使用Trie的數(shù)據(jù)結(jié)構(gòu)使得插入、查詢?nèi)~、查詢前綴的時(shí)間復(fù)雜度與已插入的單詞數(shù)目和長(zhǎng)度無關(guān),這是它的一個(gè)優(yōu)點(diǎn)。
但是,Trie又名前綴樹,因?yàn)樗荒芑谇熬Y匹配實(shí)現(xiàn)某些功能。另一些功能,例如判斷一段字符串中是否包含某些關(guān)鍵詞,不需要前綴匹配,此時(shí)就無法使用Trie了。
相關(guān)題目如下:208. 實(shí)現(xiàn) Trie (前綴樹)
完整實(shí)現(xiàn)如下:
class Trie { /** * 經(jīng)過該節(jié)點(diǎn)的字符串的下層節(jié)點(diǎn) */ Map<Character, Trie> next; /** * 該節(jié)點(diǎn)是否是一個(gè)字符串的結(jié)束 */ boolean isEnd; public Trie() { this.next = new HashMap<>(); this.isEnd = false; } public void insert(String word) { //初始默認(rèn)為根節(jié)點(diǎn),根節(jié)點(diǎn)不包含任何字符 Trie cur = this; //遍歷該字符串的字符數(shù)組 for (char c : word.toCharArray()) { //如果該節(jié)點(diǎn)的下層不包含此字符,那么加入一個(gè)新節(jié)點(diǎn)進(jìn)去 if (!cur.next.containsKey(c)) { cur.next.put(c, new Trie()); } //查找下一層節(jié)點(diǎn) cur = cur.next.get(c); } //遍歷字符串完畢,最后的節(jié)點(diǎn)isEnd置為true,表示一個(gè)字符串的結(jié)束 cur.isEnd = true; } /** * 查找字符串 */ public boolean search(String word) { Trie end = searchPrefix(word); return end != null && end.isEnd; } /** * 匹配前綴 */ public boolean startsWith(String prefix) { return searchPrefix(prefix) != null; } private Trie searchPrefix(String prefix) { //初始默認(rèn)為根節(jié)點(diǎn),根節(jié)點(diǎn)不包含任何字符 Trie cur = this; //遍歷該字符串的字符數(shù)組 for (char c : prefix.toCharArray()) { //如果該節(jié)點(diǎn)的下層不包含此字符,那么直接返回null if (!cur.next.containsKey(c)) { return null; } //查找下一層節(jié)點(diǎn) cur = cur.next.get(c); } return cur; } public static void main(String[] args) { Trie trie = new Trie(); trie.insert("你是xxl嗎?"); } }
以上就是詳解Java前綴樹Trie的原理及代碼實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Java前綴樹Trie的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MybatisPlus調(diào)用原生SQL的三種方法實(shí)例詳解
這篇文章主要介紹了MybatisPlus調(diào)用原生SQL的三種方法,在有些情況下需要用到MybatisPlus查詢?cè)鶶QL,MybatisPlus其實(shí)帶有運(yùn)行原生SQL的方法,我這里列舉三種,需要的朋友可以參考下2022-09-09java連接zookeeper實(shí)現(xiàn)zookeeper教程
這篇文章主要介紹了java連接zookeeper實(shí)現(xiàn)zookeeper教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11SpringBoot整合RabbitMQ及生產(chǎn)全場(chǎng)景高級(jí)特性實(shí)戰(zhàn)
本文主要介紹了SpringBoot整合RabbitMQ及生產(chǎn)全場(chǎng)景高級(jí)特性實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10Java語言Iterator轉(zhuǎn)換成 List的方法
在 Java 中,迭代器(Iterator)是一種用于遍歷集合中元素的對(duì)象,它提供了一種簡(jiǎn)單而一致的方式來訪問集合中的元素,而不需要暴露集合內(nèi)部的結(jié)構(gòu),這篇文章主要介紹了Java語言Iterator轉(zhuǎn)換成 List的方法,需要的朋友可以參考下2023-08-08Spring的BeanFactoryPostProcessor接口示例代碼詳解
這篇文章主要介紹了Spring的BeanFactoryPostProcessor接口,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Java實(shí)現(xiàn)Excel轉(zhuǎn)PDF的兩種方法詳解
使用具將Excel轉(zhuǎn)為PDF的方法有很多,在這里我給大家介紹兩種常用的方法:使用spire轉(zhuǎn)化PDF、使用jacob實(shí)現(xiàn)Excel轉(zhuǎn)PDF,分別應(yīng)對(duì)兩種不一樣的使用場(chǎng)景,需要的可以參考一下2022-01-01