詳解Java中字典樹(Trie樹)的圖解與實(shí)現(xiàn)
簡(jiǎn)介
Trie又稱為前綴樹或字典樹,是一種有序樹,它是一種專門用來處理串匹配的數(shù)據(jù)結(jié)構(gòu),用來解決一組字符中快速查找某個(gè)字符串的問題。Google搜索的關(guān)鍵字提示功能相信大家都不陌生,我們?cè)谳斎肟蛑羞M(jìn)行搜索的時(shí)候,會(huì)下拉出一系列候選關(guān)鍵詞。
上面這個(gè)關(guān)鍵詞提示功能,底層最基本的原理就是我們今天說的數(shù)據(jù)結(jié)構(gòu):Trie樹
我們先看看Tire樹長(zhǎng)什么樣子,以單純的單詞匹配為例,首先它是一棵多叉樹結(jié)構(gòu),根節(jié)點(diǎn)是一個(gè)空字符,樹中節(jié)點(diǎn)分為普通節(jié)點(diǎn)和結(jié)尾節(jié)點(diǎn)(如圖中紅色節(jié)點(diǎn))。結(jié)尾節(jié)點(diǎn)表示加上前面前綴,可以稱為一個(gè)單詞,如圖中hi,him。
工作過程
Tire樹與之前串匹配最大的不同點(diǎn)是,之前我們都是單模式串,查看主串中是否有與模式串匹配的子串,操作過程也是用模式串去與主串進(jìn)行比較。而Tire樹是多模式串,我們先將模式串提前構(gòu)建成Tire樹,然后查看主串是否匹配模式串,且更適用于類似如上關(guān)鍵詞提示的前綴匹配。接下來我們自己通過實(shí)現(xiàn)一個(gè)簡(jiǎn)易的關(guān)鍵詞提示功能來講解Tire樹。
數(shù)據(jù)結(jié)構(gòu)
一個(gè)value存儲(chǔ)當(dāng)前節(jié)點(diǎn)值,用一個(gè)26大小的數(shù)組存儲(chǔ)當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn),這是一個(gè)簡(jiǎn)單但是可能產(chǎn)生浪費(fèi)的方法,可以采用有序存入采用二分法查找,或者采用hash表,跳表進(jìn)行優(yōu)化。一個(gè)標(biāo)志當(dāng)前節(jié)點(diǎn)是否可作為尾節(jié)點(diǎn)。
/** * Trie樹節(jié)點(diǎn) * 假設(shè)我們只做26個(gè)小寫字母下的匹配 */ public static class Node{ //當(dāng)前節(jié)點(diǎn)值 private char value; //當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn) private Node[] childNode; //標(biāo)志當(dāng)前節(jié)點(diǎn)是否是某單詞結(jié)尾 private boolean isTail; public Node(char value) { this.value = value; } }
初始化
初始化一個(gè)僅有root節(jié)點(diǎn)的Tire樹,root節(jié)點(diǎn)值為'/0'。
Node root; public void init() { root = new Node('\0'); root.childNode = new Node[26]; }
構(gòu)建字典樹
將需要加入的模式串加入Tire樹,遍歷當(dāng)前字符串字符,從Tire樹根節(jié)點(diǎn)開始查找當(dāng)前字符,如果字符已經(jīng)存在不需要處理,并且從這個(gè)字符節(jié)點(diǎn)出發(fā),查看下一個(gè)字符是否存在,如果當(dāng)前節(jié)點(diǎn)不存Tire樹,才需要插入當(dāng)前字符,當(dāng)插入最后一個(gè)字符時(shí)需要標(biāo)志當(dāng)前字符節(jié)點(diǎn)為尾節(jié)點(diǎn)。
/** * 將當(dāng)前串插入字典樹 * @param chars */ public void insertStr(char[] chars) { //首先判斷首字符是否已經(jīng)在字典樹中,然后判斷第二字符,依次往下進(jìn)行判斷,找到第一個(gè)不存在的字符進(jìn)行插入孩節(jié)點(diǎn) Node p = root; //表明當(dāng)前處理到了第幾個(gè)字符 int chIndex = 0; while (chIndex < chars.length) { while (chIndex < chars.length && null != p) { Node[] children = p.childNode; boolean find = false; for (Node child : children) { if (null == child) {continue;} if (child.value == chars[chIndex]) { //當(dāng)前字符已經(jīng)存在,不需要再進(jìn)行存儲(chǔ) //從當(dāng)前節(jié)點(diǎn)出發(fā),存儲(chǔ)下一個(gè)字符 p = child; ++ chIndex; find = true; break; } } if (Boolean.TRUE.equals(find)) { //在孩子中找到了 不用再次存儲(chǔ) break; } //如果把孩子節(jié)點(diǎn)都找遍了,還沒有找到這個(gè)字符,直接將這個(gè)字符加入當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn) Node node = new Node(chars[chIndex]); node.childNode = new Node[26]; children[chars[chIndex] - 'a'] = node; p = node; ++ chIndex; } } //字符串中字符全部進(jìn)入tire樹中后,將最后一個(gè)字符所在節(jié)點(diǎn)標(biāo)志為結(jié)尾節(jié)點(diǎn) p.isTail = true; }
應(yīng)用
匹配有效單詞
遍歷字符串,從根節(jié)點(diǎn)出發(fā),查看字符是否存在,只要存在不存在的情況,直接返回false,如果每個(gè)字符都存在,判斷最后一個(gè)字符是否為結(jié)尾節(jié)點(diǎn),如果不是,到這里還不是一個(gè)有效單詞,返回false,否則,返回true。
/** * 查看當(dāng)前字符串是否可以在trie中找到 * @param str 主串 * @return true/false */ public boolean isMatch(String str) { //從root開始進(jìn)行匹配,只要有一個(gè)找不到即為匹配失敗 char[] chars = str.toCharArray(); int chIndex = 0; Node p = root; while (null != p) { Node[] children = p.childNode; boolean flag = false; for (Node child : children) { if (null == child) {continue;} if (child.value == chars[chIndex]) { flag = true; p = child; ++ chIndex; //當(dāng)比較最后一個(gè)字符的時(shí)候,這個(gè)字符需要是結(jié)尾字符才能完全匹配 if (chIndex == chars.length && p.isTail) { return true; } break; } } if (Boolean.FALSE.equals(flag)) { return false; } } return false; }
測(cè)試樣例
public static void main(String[] args) { //he, him, lot, a //初始化Tire樹 Trie trie = new Trie(); trie.init(); //構(gòu)建Tire樹,只有以下單詞才是有效單詞 trie.insertStr("he".toCharArray()); trie.insertStr("him".toCharArray()); trie.insertStr("lot".toCharArray()); trie.insertStr("a".toCharArray()); //匹配字符串是否為有效單詞 System.out.println(trie.isMatch("lot")); System.out.println(trie.isMatch("lit")); }
運(yùn)行結(jié)果
關(guān)鍵詞提示
根據(jù)輸入的關(guān)鍵詞前綴,匹配所有可能出現(xiàn)的關(guān)鍵詞。首先遍歷字符串,從節(jié)點(diǎn)出發(fā),只要有一個(gè)找不到,直接返回null,直至找到最后一個(gè)字符對(duì)應(yīng)的節(jié)點(diǎn),從該節(jié)點(diǎn)出發(fā)找到所有尾節(jié)點(diǎn)。
/** * 找到所有以str為前綴的字符串 * @param str 前綴串 * @return 所有以str為前綴的單詞 */ public List<String> findStrPrefix(String str) { //根據(jù)str首先找到str最后一個(gè)字符,然后從這個(gè)字符出發(fā),找到所有字符串 List<String> result = new ArrayList<>(); char[] chars = str.toCharArray(); //分成兩步走 //1。找到str最后一個(gè)自字符在字典樹中的node //2。從該node出發(fā),找到所有的結(jié)尾node,即為以str為前綴的字符串 int chIndex = 0; Node p = root; while (null != p && chIndex < chars.length) { Node[] children = p.childNode; boolean flag = false; for (Node child : children) { if (null == child) {continue;} if (child.value == chars[chIndex]) { //已經(jīng)找到 p = child; flag = true; ++ chIndex; break; } } //如果沒有找到,直接返回空 if (Boolean.FALSE.equals(flag)) { return null; } } //找到了最后一個(gè)節(jié)點(diǎn) //深度優(yōu)先遍歷,查找所有尾節(jié)點(diǎn) this.dfs(p, new StringBuilder(str), result); return result; } public void dfs(Node p, StringBuilder str, List<String> result) { Node[] children = p.childNode; for (Node child : children) { if (null == child) { continue; } str.append(child.value); if (child.isTail) { result.add(str.toString()); } //再遞歸查當(dāng)前節(jié)點(diǎn)的孩子節(jié)點(diǎn) dfs(child, str, result); //需要將剛剛set進(jìn)去的節(jié)點(diǎn)刪除,否則影響當(dāng)前節(jié)點(diǎn)的下一個(gè)孩子節(jié)點(diǎn) //舉個(gè)例子,h的孩子節(jié)點(diǎn)有e,i,當(dāng)e放進(jìn)去之后不拿出來,在遍歷到i的時(shí)候,就會(huì)形成hei str.setLength(str.length() - 1); } }
測(cè)試樣例
public static void main(String[] args) { //he, him, lot, a //初始化Tire樹 Trie trie = new Trie(); trie.init(); //構(gòu)建Tire樹,只有以下單詞才是有效單詞 trie.insertStr("he".toCharArray()); trie.insertStr("him".toCharArray()); trie.insertStr("lot".toCharArray()); trie.insertStr("a".toCharArray()); //匹配字符串是否為有效單詞 List<String> strings = trie.findStrPrefix("h"); }
運(yùn)行結(jié)果
總結(jié)
到這里Trie樹就講完了,主要就是聚合前綴,通過樹的特性,按照鏈路進(jìn)行訪問,同時(shí)標(biāo)志尾節(jié)點(diǎn),標(biāo)志到當(dāng)前節(jié)點(diǎn)是一個(gè)完整的字符串。
以上就是詳解Java中字典樹(Trie樹)的圖解與實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Java字典樹的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring中的@RestControllerAdvice注解使用方法解析
這篇文章主要介紹了Spring中的@RestControllerAdvice注解使用方法解析,@RestControllerAdvice是Controller的增強(qiáng) 常用于全局異常的捕獲處理 和請(qǐng)求參數(shù)的增強(qiáng),需要的朋友可以參考下2024-01-01Spring注解驅(qū)動(dòng)之ApplicationListener異步處理事件說明
這篇文章主要介紹了Spring注解驅(qū)動(dòng)之ApplicationListener異步處理事件說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09Spring Validation方法實(shí)現(xiàn)原理分析
這篇文章主要介紹了Spring Validation實(shí)現(xiàn)原理分析,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07java尋找迷宮路徑的簡(jiǎn)單實(shí)現(xiàn)示例
這篇文章主要介紹了java尋找迷宮路徑的簡(jiǎn)單實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10IDEA插件Statistic統(tǒng)計(jì)代碼快速分辨爛項(xiàng)目
這篇文章主要為大家介紹了使用IDEA插件Statistic來統(tǒng)計(jì)項(xiàng)目代碼,幫助大家快速識(shí)別出爛項(xiàng)目,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-01-01Springboot+Mybatis-plus不使用SQL語句進(jìn)行多表添加操作及問題小結(jié)
這篇文章主要介紹了在Springboot+Mybatis-plus不使用SQL語句進(jìn)行多表添加操作,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04Java import導(dǎo)入及訪問控制權(quán)限修飾符原理解析
這篇文章主要介紹了Java import導(dǎo)入及訪問控制權(quán)限修飾符過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11Java兩種方法計(jì)算出階乘尾部連續(xù)0的個(gè)數(shù)
這篇文章主要介紹了Java兩種方法計(jì)算出階乘尾部連續(xù)0的個(gè)數(shù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03