算法:(十)前缀树

10.1 前缀树基础

面试题62:实现前缀树

题目:请设计实现一颗前缀树Trie,它有如下操作。

  • 函数insert,在前缀树中添加一个字符串。
  • 函数search,查找字符串。如果前缀树中包含该字符串,则返回true;否则返回false。
  • 函数startWith,查找字符串前缀。如果前缀树中包含以该前缀开头的字符串,则返回true,否则返回false。
public class Trie {
    private TrieNode root;
    public Trie(){
        root = new TrieNode();
    }

    public void insert(String word){
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            if(root.children[c - 'a'] == null){
                root.children[c - 'a'] = new TrieNode();
            }
            node = root.children[c - 'a'];
        }
        node.isEnd = true;
    }

    public boolean search(String word){
        TrieNode node = root;
        for(char c : word.toCharArray()){
            if(node.children[c - 'a'] == null){
                return false;
            }
            node = root.children[c - 'a'];
        }
        return node.isEnd;
    }

    public boolean startWith(String prefix){
        TrieNode node = root;
        for(char c : prefix.toCharArray()){
            if(node.children[c - 'a'] == null){
                return false;
            }
            node = root.children[c - 'a'];
        }
        return true;
    }
}

class TrieNode{
    // 前缀树的孩子是一个26长度的数组,代表26个字母
    // 如果数组的某个位置不是null,则标明该前缀中有该字母
    public TrieNode[] children;
    // 标记是否是单词结尾
    public boolean isEnd;
    public TrieNode(){
        children = new TrieNode[26];
    }
}

10.2 前缀树的应用

面试题63:替换单词

题目:英语中有一个概念叫词根。在词根后面加上若干字符就能拼出更长的单词。例如,“an"是一个词根,在它后面加上"other"就能得到另一个单词another。现在给定一个由词根组成的字典和一个英文句子,如果句子中的单词在字典中有它的词根,则用它的词根替换该单词;如果单词没有词根,则保留该单词。请输出替换后的句子。例如,如果词根字典包含字符串[“cat”, “bat”, “rat”],英文句子为"the cattle was rattled by the battery”,则替换之后的句子是"the cat was rat by the bat"。

public String replaceWords(List<String> prefixs, String sentence){
    TrieNode root = buildTrie(prefixs);
    String[] words = sentence.split(" ");
    for(int i = 0; i < words.length; i++){
        String word = words[i];
        words[i] = findPrefix(word, root);
    }
    return String.join(" ", words);
}
/**
 * 查找某个单词的前缀,找到则返回前缀,找不到则返回单词本身
 * @param word
 * @param root
 * @return
 */
private String findPrefix(String word, TrieNode root) {
    TrieNode node = root;
    StringBuilder prefix = new StringBuilder();
    for(char c : word.toCharArray()){
        if(node.children[c - 'a'] == null){
            return word;
        }
        node = node.children[c - 'a'];
        prefix.append(c);
        if(node.isEnd){
            return prefix.toString();
        }
    }
    return word;
}

private TrieNode buildTrie(List<String> prefixs) {
    TrieNode root = new TrieNode();
    for(String prefix : prefixs){
        TrieNode node = root;
        for(char c : prefix.toCharArray()){
            if(node.children[c - 'a'] == null){
                node.children[c - 'a'] = new TrieNode();
            }
            node = node.children[c - 'a'];
        }
        node.isEnd = true;
    }
    return root;
}

面试题64:神奇的字典

题目:请实现有如下两个操作的神奇字典。

  • 函数buildDict,输入单词数组来创建一个字典。
  • 函数search,输入一个单词,判断能够修改单词中的一个字符,使修改后的单词是字典中的一个单词。
public class MagicDictionary {
    private TrieNode root;
    public MagicDictionary(){
        root = new TrieNode();
    }

    public void buildDict(String[] words){
        for (String word : words) {
            TrieNode node = root;
            for (char c : word.toCharArray()) {
                if(node.children[c - 'a'] == null){
                    node.children[c - 'a'] = new TrieNode();
                }
                node = node.children[c - 'a'];
            }
            node.isEnd = true;
        }
    }

    public boolean search(String word){
        return dfs(word, root, 0, 0);
    }

    /**
     * 深度优先搜索,要保证找到修改一次能够满足条件的单词
     * @param word
     * @param node
     * @param depth 遍历深度
     * @param edit 修改次数,超过一次就会返回false
     * @return
     */
    private boolean dfs(String word, TrieNode node, int depth, int edit) {
        if(node == null){
            return false;
        }
        if(node.isEnd && depth == word.length() && edit == 1){
            return true;
        }
        // 如果修改次数大于1则会返回false。第一个条件要满足word.charAt(depth)是有效的
        if(depth < word.length() && edit <= 1){
            // 是否发现了满足条件的单词,发现之后就会跳出循环
            boolean found = false;
            for(int i = 0; i < 26 && !found; i++){
                if(word.charAt(depth) - 'a' == i){
                    found = dfs(word, node.children[i], depth + 1, edit);
                } else {
                    found = dfs(word, node.children[i], depth + 1, edit + 1);
                }
            }
            return found;
        }
        return false;
    }
}

面试题65:最短的单词编码

题目:输入一个包含n个单词的数组,可以把他们编码成一个字符串和n个下标。例如,单词数组[“time”, “me”, “bell”]可以编码成一个字符串"time#bell#",然后这些单词就可以通过下标[0, 2, 5]得到。输出最短的单词编码的长度,上个例子中输出10。

public int minimumLengthEncoding(String[] words){
    // 反向构建前缀树
    TrieNode root = new TrieNode();
    for (String word : words) {
        TrieNode node = root;
        for(int i = word.length() - 1; i >= 0; i--){
            if(node.children[word.charAt(i) - 'a'] == null){
                node.children[word.charAt(i) - 'a'] = new TrieNode();
            }
            node = node.children[word.charAt(i)];
        }
    }
    // 计算前缀树的深度之和
    int[] result = {0};
    getLength(root, 1, result);
    return  result[0];
}

/**
 * 深度优先遍历前缀树,计算前缀树的深度之和
 * @param root
 * @param depth
 * @param result
 */
private void getLength(TrieNode root, int depth, int[] result) {
    boolean isLeaf = true;
    for(int i = 0; i < 26; i++){
        if(root.children[i] != null){
            isLeaf = false;
            getLength(root.children[i], depth + 1, result);
        }
    }
    if(isLeaf){
        result[0] += depth;
    }
}

面试题66:单词之和

题目:请设计实现一个类型MapSum,它有如下两个操作。

  • 函数insert,输入一个字符串和一个整数,在该数据集合中添加一个字符串及其对应的值。如果数据集合中已经包含该字符串,则将该字符串对应的值替换成新值。
  • 函数sum,输入一个字符串,返回数据集合中所有以该字符串为前缀的字符串对应的值之和。
public class MapSum {
    class TrieNode{
        TrieNode[] children;
        boolean isEnd;
        int value;

        public TrieNode(){
            children = new TrieNode[26];
            isEnd = false;
            value = 0;
        }
    }
    private TrieNode root;

    public MapSum {
        root = new TrieNode();
    }

    
    /**
     * 插入时构建前缀树
     * @param word
     * @param value
     */
    public void insert(String word, int value){
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            if (node.children[c - 'a'] == null) {
                node.children[c - 'a'] = new TrieNode();
            }
            node = node.children[c - 'a'];
        }
        node.isEnd = true;
        node.value = value;
    }

    /**
     * 先找到满足条件的前缀,然后从前缀位置结点开始进行深度优先搜索
     * @param prefix
     * @return
     */
    public int sum(String prefix){
        TrieNode node = root;
        for (char c : prefix.toCharArray()) {
            if(node.children[c - 'a'] == null){
                return 0;
            }
            node = node.children[c - 'a'];
        }
        int[] sum = {0};
        getSum(node, sum);
        return sum[0];
    }

    private void getSum(TrieNode node, int[] sum) {
        if(node == null){
            return;
        }
        if(node.isEnd){
            sum[0] += node.value;
        }
        for(int i = 0; i < 26; i++){
            if(node.children[i] != null){
                getSum(node.children[i], sum);
            }
        }
    }
}

面试题67:最大的异或

题目:输入一个整数数组(每个数字都大于等于0),请计算其中任意两个数字的异或的最大值。例如[1,3,4,7]中,3和4的异或结果最大,异或结果为7。

/**
 * 因为bit位只有0或1
 * 所以构造新的前缀树结点,孩子结点数量为2
 */
class TrieNode{
    TrieNode[] children;

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

/**
 * 每个数字构造前缀树
 * @param nums
 * @return
 */
public TrieNode buildTrie(int[] nums){
    TrieNode root = new TrieNode();
    for (int num : nums) {
        TrieNode node = root;
        for(int i = 31; i > 0; i--){
            int bit = (num >> i) & 1;
            if(node.children[bit] == null){
                node.children[bit] = new TrieNode();
            }
            node = node.children[bit];
        }
    }
    return root;
}

/**
 * 从高位开始遍历,找与该位相反的数字,相反异或得1,能够保证得到较大的数字
 * @param nums
 * @return
 */
public int findMaxXor(int[] nums){
    TrieNode root = buildTrie(nums);
    int max = 0;
    for (int num : nums) {
        TrieNode node = root;
        int xor = 0;
        for(int i = 31; i > 0; i--){
            if(node.children[1 - ((num >> i) & 1)] != null){
                node = node.children[1 - ((num >> i) & 1)];
                xor = xor << 1 + 1;
            } else {
                node = node.children[(num >> i) & 1];
                xor = xor << 1;
            }
        }
        max = Math.max(max, xor);
    }
    return max;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值