前缀树的操作

前缀树是N叉树的一种形式,常用于存储字符串,树中每一个节点表示一个字符。
前缀树重要的存在价值是搜索速度,典型的利用空间换时间,时间复杂度为O(n),n是树的深度。

Trie

上图中存储了四个单词:am、bad、be、so,位于叶子节点,叶子节点一定为词,但词不一定位于叶子节点。除了存储词的节点外,其余节点称为前缀。如ba,在树中并不是一个词,但他是bad词的前缀,前缀的重要作用就是减少存储空间,具有相同前缀的不同单词,只需存储差异部分,大大减少了空间的存储。

我所喜欢的数据结构:

public class TrieNode {
    public boolean isWord;
    public HashMap<Character, TrieNode> nexts = new HashMap<Character, TrieNode>();
}

Trie常见的操作:

插入

    private void insert(String word, TrieNode root) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            if (node.nexts.get(word.charAt(i)) == null) {//没有前缀
                node.nexts.put(word.charAt(i), new TrieNode());
            }
            node = node.nexts.get(word.charAt(i));//切记切换到子节点,否则全部存在了一层
        }
        node.isWord = true;//标记单词存储结束,其余节点为前缀
    }

查找单词

    private boolean search(String word, TrieNode root) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            if (node.nexts.get(word.charAt(i)) == null) {
                return false;//中途查找失败,就失败
            }
            node = node.nexts.get(word.charAt(i));//切换到下一层
        }
        return node.isWord;//查找到了但不一定是单词,有可能是其他单词的前缀,因此需要判断
    }

查找前缀

    public boolean startsWith(String prefix) {
        char[] chars=prefix.toCharArray();
        TrieNode node=root;
        for(int i=0;i<chars.length;i++){
            if(node.nexts.get(chars[i])==null){
                return false;//没查完就到顶了,失败
            }else{
                node=node.nexts.get(chars[i]);//查找下一层
            }
        }
        return true;//查找到了一定是前缀,与查找单词不同
    }

Map Sum Pairs

https://leetcode.com/explore/learn/card/trie/148/practical-application-i/1058/

这个题目实在是翻译不出来啊!题意:插入多个单词(apple、app)给每一个词赋值apple=3、app=2,当输入前缀ap时,返回以ap为前缀的所有单词值的和。

Example:
Input: insert("apple", 3), Output: Null
Input: sum("ap"), Output: 3
Input: insert("app", 2), Output: Null
Input: sum("ap"), Output: 5

    public int sum(String prefix) {
        char[] chars=prefix.toCharArray();
        TrieNode node=root;
        for(int i=0;i<chars.length;i++){
            if(node.nexts.get(chars[i])==null){//前缀找到一半就夭折了
                return 0;
            }else{
                node=node.nexts.get(chars[i]);//去下一层寻找
            }
        }
            
        return value(node);//找到了前缀的最后一个节点
    }
    
    private int value(TrieNode root){
        TrieNode node=root;
        int sum=0;
        if(node==null){
            return 0;
        }
        if(node.isWord){//此节点本身就是单词,求和
            sum+=node.value;
        }
        if(node.nexts.keySet().size()>0){//节点还有子节点
            for(Character key :node.nexts.keySet()){
                sum+=value(node.nexts.get(key));//递归求和
            }
        }
        return sum;
    }

替换

一段文字内替换所有以
Trie中存储的单词
的单词

Example
Input: dict = ["cat", "bat", "rat"]
sentence = "the cattle was rattled by the battery"
Output: "the cat was rat by the bat"

步骤:

  • 构造Trie
  • 将句子拆分成单词
  • 将拆分的单词一个个的去Trie中查找,找到便替换
    private String replaceWords(TrieNode root,String[] words){
        StringBuilder stringBuilder=new StringBuilder();
        for(int i=0;i<words.length;i++){
            stringBuilder.append(replaceWords(root,words[i])).append(" ");//将拆分的单词一个个的去Trie中查找,找到便替换
        }
        stringBuilder.deleteCharAt(stringBuilder.length()-1);
        return stringBuilder.toString();
    }
    
    private String replaceWords(TrieNode root,String word){
        TrieNode node=root;
        StringBuilder stringBuilder=new StringBuilder();
        for(int i=0;i<word.length();i++){
            if(node.nexts.get(word.charAt(i))==null){//没有此单词的前缀
                return word;
            }else{
                stringBuilder.append(word.charAt(i));
                if(node.nexts.get(word.charAt(i)).isWord){//找到了单词的替换
                    return stringBuilder.toString();
                }else{
                    node=node.nexts.get(word.charAt(i));//查找下一层
                }
            }
        }
        return stringBuilder.toString();
    }

模糊查找

addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true

步骤:

  • 构建字典
  • 每个字符在Trie中查找,碰到模糊词汇使用递归
    public boolean search(String word) {
        return search(word,root);
    }
    
    public boolean search(String word,TrieNode root){
        if(word.length()==0){//每个字符都找完了,看最后一个节点是不是单词
            return root.isWord;
        }
        
        char c=word.charAt(0);
        if(c!='.'){//非模糊
            if(root.nexts.get(c)==null){//字符不匹配查找失败
                return false;
            }else{//字符匹配,递归看子节点
                return search(word.substring(1),root.nexts.get(c));
            }
        }else{//模糊
            for(char ch :root.nexts.keySet()){//跳过模糊字符,查找下一字符
                if(search(word.substring(1),root.nexts.get(ch)))//注意成功返回,失败一次不代表全部失败
                    return true;
            }
            return false;//全部失败
        }
        
    }

求最大异或值

在给定的数组中两两项异或,找出最大的异或值。

性质:
有 a ^ b = x
则 a ^ x = b , b ^ x = a

一个数的大小如何判断?从高位向低位走,先遇到不为0的数最大(1000 、0100),若高位相同继续向低位走(1000 、 1100)。

思路:

  • 将整数转成二进制存入Trie
  • 由于求最大值,所以从高位向低位存储
  • 循环数组
  • 假设项为a,存在于Trie中最大的异或值为x,a、Trie已知
  • 若想x最大,则x的每一位都为1,但不可能
  • 用a的每一位去异或1(性质),求得b的这一位,若b的这一位在Trie中,则最大的x这一位确实存在,否则下一位
  • 所有位都走完,求得a在Trie中的最大异或值。
  • 循环数组完毕,找出最大的异或值

由于存储的节点只有0、1所以修改TrieNode结构

class TrieNode{
    TrieNode[] children;
    
    public TrieNode(){
        children = new TrieNode[2];
    }
}

构造Trie

private void buildTrie(int[] nums,TrieNode root){
        for(int num:nums){
            TrieNode node=root;
            for(int i=31;i>=0;i--){
                int bit=(num >> i)&1;//与1是为了只求一位
                if(node.children[bit]==null){
                    node.children[bit]=new TrieNode();
                }
                node=node.children[bit];
            }
        }
}

遍历查找最大异或值

public int findMaximumXOR(int[] nums,TrieNode root) {
        int max=0;
        for(int num :nums){
            TrieNode node=root;
            int res=0;
            for(int i=31;i>=0;i--){
                int bit=(num >> i)&1;
                
                if(node.children[bit^1]!=null){//找到b的这一位
                    res+=1<<i;//累加x的这一位
                    node=node.children[bit^1];//去b的下一位找最大值
                }else{//没找到b的这一位,如思路中的x的每一位不可能都为1
                    node=node.children[bit];
                }
//由于是从高位开始查找的,所以先找到的1,一定是最大的
            }
            max=Math.max(res,max);
        }
        return max;
}

矩阵中查找单词

给定矩阵,判断输入的单词是否在矩阵中。

[
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]
矩阵中字符可以横向纵向组合成单词

思路:

  • 构建要查找单词的Trie
  • 遍历矩阵中的每一项
  • 让每一项上下左右构成单词后去Trie中查找(递归)
    public List<String> findWords(char[][] board, String[] words) {
        Trie trie=new Trie();
        for(String s: words){
            trie.insert(s);
        }
        
        int y=board.length;
        int x=board[0].length;
        
        boolean[][] visited=new boolean[y][x];//记录是否走过此路
        
        for(int i=0;i<x;i++){
            for(int j=0;j<y;j++){//遍历所有的点,这里还不是递归
                dfs(board,visited,"",i,j,trie);
            }
        }
        return new ArrayList<String>(res);
    }

    public void dfs(char[][] board,boolean[][] visited,String str,int x,int y,Trie trie){
        if(x<0||x>=board[0].length||y<0||y>=board.length) return;//所要查询的位置越界
        
        if(visited[y][x])return;//已经走过该路
        
        str+=board[y][x];//新字符串
        
        if(!trie.startWith(str)) return;//如果没有此前缀,不必浪费时间
        
        if(trie.search(str)){//在矩阵中找到该词,加入结果
            res.add(str);
        }
        
        visited[y][x]=true;//记录当前已走
        dfs(board,visited,str,x-1,y,trie);//横向查找
        dfs(board,visited,str,x+1,y,trie);
        dfs(board,visited,str,x,y-1,trie);//纵向查找
        dfs(board,visited,str,x,y+1,trie);
        visited[y][x]=false;//真个矩阵走完后,重置状态为下一次做准备
    }

回文

在给出的单词组中,找出可以组成回文的两个单词组。
LeetCode

Example 1:
Given words = ["bat", "tab", "cat"]
Return [[0, 1], [1, 0]]
The palindromes are ["battab", "tabbat"]
Example 2:
Given words = ["abcd", "dcba", "lls", "s", "sssll"]
Return [[0, 1], [1, 0], [3, 2], [2, 4]]
The palindromes are ["dcbaabcd", "abcddcba", "slls", "llssssll"]

 class TrieNode {
    HashMap<Character,TrieNode> children;
    int index;
    List<Integer> list;
    
    public TrieNode(){
        children=new HashMap<Character,TrieNode>();
        index=-1;
        list=new ArrayList<Integer>();
    }
}

    public List<List<Integer>> palindromePairs(String[] words) {
        List<List<Integer>> res=new ArrayList<List<Integer>>();
        TrieNode root=new TrieNode();
        for(int i=0;i<words.length;i++) addWord(root,words[i],i);
        for(int i=0;i<words.length;i++) search(words[i],i,root,res);
        return res;
    }
    
    private void addWord(TrieNode root,String word,int index){
        TrieNode node=root;
        for(int i=word.length()-1;i>=0;i--){//倒序插入,正序查找
            if(node.children.get(word.charAt(i))==null){
                node.children.put(word.charAt(i),new TrieNode());
            }
            if(isPalindrome(word,0,i)) node.list.add(index);//单词内部回文,node不仅仅属于此word,所以要添加到list
            node=node.children.get(word.charAt(i));
        }
        node.list.add(index);//此单词结束,不代表到达叶子节点
        node.index=index;//记录此节点为单词
    }
    
    private void search(String word,int index,TrieNode root,List<List<Integer>> res){
        for(int i=0;i<word.length();i++){
            if(root.index>=0&&root.index!=index&&isPalindrome(word,i,word.length()-1)){
//倒序插入,正序查找,找到的节点为单词且不是自己,证明到此节点root单词与单词前半部分回文,如果单词后半部分回文,则两单词一定回文
                res.add(Arrays.asList(index,root.index));
            }
            root=root.children.get(word.charAt(i));
            if(root==null){//查找结束
                return;
            }
        }
        
        for(int i:root.list){//最后一个节点一定是单词,同root.index>=0
            if(index==i) continue;//去除自己,同root.index!=index
            res.add(Arrays.asList(index,i));//由于是最后一个节点,所以没有后半部分
        }
//单词A、B,回文不一定AB回文且BA回文,只要其中一项即可,勿陷入盲区
        
    }
    
    private boolean isPalindrome(String word,int start,int end){
        while(start<end){
            if(word.charAt(start++)!=word.charAt(end--)) return false;
        }
        return true;
    }
    
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容