【题型总结】字典树

字典树数据结构及其在字符串处理中的应用

字典树

理论基础

  • Trie 树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构,是一种非典型的多叉树模型

  • 其核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」。

    IMG_1659.PNG

  • 应用:一次建树,多次查询

    • 在纯算法领域,前缀树算是一种较为常用的数据结构。
    • 不过如果在工程中,不考虑前缀匹配的话,基本上使用 hash 就能满足。如果考虑前缀匹配的话,工程也不会使用 Trie 。一方面是字符集大小不好确定,另外,对于个别的超长字符 Trie 会进一步变深。
  • 题型:根据字符串的前缀进行查找

  • 模板:添加字符串至字典树,查询时返回次数

    • 二维数组

      • 使用二维数组 trie[]来存储我们所有的单词字符。
      • 使用 index 来自增记录我们到底用了多少个格子(相当于给被用到格子进行编号)。
      • 使用 count[]数组记录某个格子被「被标记为结尾的次数」(当 idx 编号的格子被标记了 n 次,则有 count[idx]=n)。
      class Trie {
          int N = 100009; // N为节点个数,直接设置为十万级
          int[][] trie;
          int[] count;
          int index;
      
          public Trie() {
              trie = new int[N][26];
              count = new int[N];
              index = 0;
          }
          
          public void insert(String s) {// 将字符串str添加进字典树
              int p = 0;
              for (int i = 0; i < s.length(); i++) {
                  int u = s.charAt(i) - 'a';
                  if (trie[p][u] == 0) trie[p][u] = ++index;// 创建结点并赋予编号index
                  p = trie[p][u];// 走到下一个结点
              }
              count[p]++;// 计数
          }
          
          public int search(String s) {// 返回当前字符串出现的次数
              int p = 0;
              for (int i = 0; i < s.length(); i++) {
                  int u = s.charAt(i) - 'a';
                  if (trie[p][u] == 0) return false;//若当前结点不存在,那么直接返回0
                  p = trie[p][u];
              }
              return count[p];
          }    
      }
      
      
      • 复杂度
        • 时间复杂度: O ( l e n ) O(len) O(len) l e n len len为入参字符串长度
        • 空间复杂度: O ( n k ) O(nk) O(nk) n n n为节点数量, k k k为字符集大小
    • 动态扩点

      建立 TrieNode 结构节点。

      class Trie {
          class TrieNode {
              int cnt;
              TrieNode[] tns = new TrieNode[26];
          }
      
          TrieNode root;
          public Trie() {
              root = new TrieNode();
          }
      
          public void insert(String s) {
              TrieNode p = root;
              for(int i = 0; i < s.length(); i++) {
                  int u = s.charAt(i) - 'a';
                  if (p.tns[u] == null) p.tns[u] = new TrieNode();//创建结点
                  p = p.tns[u]; 
              }
              p.cnt++;
          }
      
          public int search(String s) {
              TrieNode p = root;
              for(int i = 0; i < s.length(); i++) {
                  int u = s.charAt(i) - 'a';
                  if (p.tns[u] == null) return 0;
                  p = p.tns[u]; 
              }
              return p.cnt;
          }
      }
      
      • 复杂度
        • 时间复杂度: O ( l e n ) O(len) O(len) l e n len len为入参字符串长度
        • 空间复杂度: O ( n k ) O(nk) O(nk) n n n为节点数量, k k k为字符集大小
    • 静态数组

      减小空间复杂度,并避免垃圾回收

      class Trie {
          // 以下 static 成员独一份,被创建的多个 Trie 共用
          static int N = 100009; // 直接设置为十万级
          static int[][] trie = new int[N][26];
          static int[] count = new int[N];
          static int index = 0;
      
          // 在构造方法中完成重置 static 成员数组的操作
          // 这样做的目的是为减少 new 操作(无论有多少测试数据,上述 static 成员只会被 new 一次)
          public Trie() {
              for (int row = index; row >= 0; row--) {
                  Arrays.fill(trie[row], 0);
              }
              Arrays.fill(count, 0);
              index = 0;
          }
          
          public void insert(String s) {
              int p = 0;
              for (int i = 0; i < s.length(); i++) {
                  int u = s.charAt(i) - 'a';
                  if (trie[p][u] == 0) trie[p][u] = ++index;
                  p = trie[p][u];
              }
              count[p]++;
          }
          
          public int search(String s) {
              int p = 0;
              for (int i = 0; i < s.length(); i++) {
                  int u = s.charAt(i) - 'a';
                  if (trie[p][u] == 0) return 0;
                  p = trie[p][u];
              }
              return count[p];
          }
      }
      
      
      • 复杂度
        • 时间复杂度: O ( l e n ) O(len) O(len) l e n len len为入参字符串长度
        • 空间复杂度: O ( n k ) O(nk) O(nk) n n n为节点数量, k k k为字符集大小

相关题目

1.实现 Trie (前缀树)【LC208】

A trie (pronounced as “try”) or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker.

Implement the Trie class:

  • Trie() Initializes the trie object.
  • void insert(String word) Inserts the string word into the trie.
  • boolean search(String word) Returns true if the string word is in the trie (i.e., was inserted before), and false otherwise.
  • boolean startsWith(String prefix) Returns true if there is a previously inserted string word that has the prefix prefix, and false otherwise.

Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false
  • 思路:

    • insert:首先从根出发,如果子节点存在,那么沿着指针,移动至子节点;如果子节点不存在,那么创建一个新的子节点;处理到最后一个字符时,计数。

    • search:首先从根出发,如果子节点存在,那么沿着指针,移动至子节点;如果子节点不存在,那么直接返回false;处理到最后一个字符时,如果该字符对应的数量大于0,那么返回true。

      如果数量为0,则为前缀

    • startsWith:首先从根出发,如果子节点存在,那么沿着指针,移动至子节点;如果子节点不存在,那么直接返回false;处理到最后一个字符时,返回true。

二维数组实现
class Trie {
    int[][] tire;
    int[] count;
    int N = 100001;
    int index;
    public Trie() {
        tire = new int[N][26];
        count = new int[N];
        index = 0;
    }
    
    public void insert(String word) {
        int p = 0;
        for (int i = 0; i < word.length(); i++){
            int u = word.charAt(i) - 'a';
            if (tire[p][u] == 0) tire[p][u] = ++index;
            p = tire[p][u];
        }
        count[p]++;
    }
    
    public boolean search(String word) {
        int p = 0;
        for (int i = 0; i < word.length(); i++){
            int u = word.charAt(i) - 'a';
            if (tire[p][u] == 0) return false;
            p = tire[p][u];
        }
        return count[p] > 0;
    }
    
    public boolean startsWith(String prefix) {
        int p = 0;
        for (int i = 0; i < prefix.length(); i++){
            int u = prefix.charAt(i) - 'a';
            if (tire[p][u] == 0) return false;
            p = tire[p][u];
        }
        return true;
    }
}

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * boolean param_2 = obj.search(word);
 * boolean param_3 = obj.startsWith(prefix);
 */
  • 复杂度
    • 时间复杂度: O ( l e n ) O(len) O(len) l e n len len为入参字符串长度
    • 空间复杂度: O ( n k ) O(nk) O(nk) n n n为节点数量, k k k为字符集大小
静态数组实现
class Trie {
    // 以下 static 成员独一份,被创建的多个 Trie 共用
    static int N = 100009; // 直接设置为十万级
    static int[][] trie = new int[N][26];
    static int[] count = new int[N];
    static int index = 0;

    // 在构造方法中完成重置 static 成员数组的操作
    // 这样做的目的是为减少 new 操作(无论有多少测试数据,上述 static 成员只会被 new 一次)
    public Trie() {
        for (int row = index; row >= 0; row--) {
            Arrays.fill(trie[row], 0);
        }
        Arrays.fill(count, 0);
        index = 0;
    }
    
    public void insert(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) trie[p][u] = ++index;
            p = trie[p][u];
        }
        count[p]++;
    }
    
    public boolean search(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return count[p] != 0;
    }
    
    public boolean startsWith(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return true;
    }
}


作者:宫水三叶
链接:https://leetcode.cn/problems/implement-trie-prefix-tree/solutions/721110/gong-shui-san-xie-yi-ti-shuang-jie-er-we-esm9/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 复杂度
    • 时间复杂度: O ( l e n ) O(len) O(len) l e n len len为入参字符串长度
    • 空间复杂度: O ( n k ) O(nk) O(nk) n n n为节点数量, k k k为字符集大小
动态扩点实现:TrieNode
class Trie {
    class TrieNode{
       int cnt;
       TrieNode[] next = new TrieNode[26];
    }
    TrieNode root;
    public Trie(){
        root = new TrieNode();
    }
    public void insert(String word) {
        TrieNode p = root;
        for (int i = 0; i < word.length(); i++){
            int u = word.charAt(i) - 'a';
            if (p.next[u] == null)  p.next[u] = new TrieNode();
            p = p.next[u];
        }
        p.cnt++;
    }
    
    public boolean search(String word) {
        TrieNode p = root;
        for (int i = 0; i < word.length(); i++){
            int u = word.charAt(i) - 'a';
            if (p.next[u] == null)  return false;
            p = p.next[u];
        }
        return p.cnt > 0;
    }
    
    public boolean startsWith(String prefix) {
        TrieNode p = root;
        for (int i = 0; i < prefix.length(); i++){
            int u = prefix.charAt(i) - 'a';
            if (p.next[u] == null) return false;
            p = p.next[u];
        }
        return true;
    }
}

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * boolean param_2 = obj.search(word);
 * boolean param_3 = obj.startsWith(prefix);
 */
  • 复杂度
    • 时间复杂度: O ( l e n ) O(len) O(len) l e n len len为入参字符串长度
    • 空间复杂度: O ( n k ) O(nk) O(nk) n n n为节点数量, k k k为字符集大小

2.统计异或值在范围内的数对有多少【LC1803】

Given a (0-indexed) integer array nums and two integers low and high, return the number of nice pairs.

A nice pair is a pair (i, j) where 0 <= i < j < nums.length and low <= (nums[i] XOR nums[j]) <= high.

字典树
  • 思路:

    • 首先使用字典树存储数组中元素的二进制形式,由于 n u m s [ i ] ≤ 2 ∗ 1 0 4 nums[i] \le 2*10^4 nums[i]2104,因此用15位二进制就可以表示;

    • 由容斥原理可得,异或值在 [ l o w , h i g h ] [low,high] [low,high]之间的对数=异或值为 h i g h high high的对数-异或值为 l o w − 1 low-1 low1的对数

    • 然后求出 n u m s [ i ] nums[i] nums[i]异或 n u m s [ 0 , i − 1 ] nums[0,i-1] nums[0,i1]小于等于 t a r g e t target target的数量,再将 n u m s [ i ] nums[i] nums[i]加入字典树中

      具体步骤:从高位开始枚举,符合则计数,不符合直接return

      • 如果 t a r g e t target target的第 j j j位为1,那么与之前数字第 j j j位异或结果可以为1也可以为0
      • 如果 t a r g e t target target的第 j j j位为1,那么与之前数字第 j j j位异或结果只能为0
  • 二维数组实现

    class Solution {
        int[][] trie;
        int[] cnt;
        int idx;
        public int countPairs(int[] nums, int low, int high) {
            trie = new int[nums.length * 16][2];
            cnt = new int[nums.length * 16];
            return get(nums, high) - get(nums, low - 1);
        }
        int get(int[] nums, int high) {
            idx = 0;
            for (int i = 0; i < trie.length; i++) trie[i][0] = trie[i][1] = cnt[i] = 0;
            int ans = 0;
            for (int i = 0; i < nums.length; i++) {
                ans += query(nums[i], high);
                add(nums[i]); 
            }
            return ans;
        }
        void add(int x) {
            int p = 0;
            for (int i = 14; i >= 0; i--) {
                int u = (x >> i)  & 1;
                if (trie[p][u] == 0) trie[p][u] = ++idx;
                p = trie[p][u]; //移动到下一个结点 
                cnt[p]++; // 个数增加,cnt[x]代表x结点出现的次数
            }
        }
        int query(int x, int high) {
            int sum = 0, p = 0;
            for  (int i = 14; i >= 0; i--) {
                int u = (x >> i) & 1;
                if (((high >> i) & 1) == 1) { //high当前i位为1, 那么x与以前数当前i位的异或可以位1或者0
                    sum += cnt[trie[p][u]];//加上与x异或后当前i位为0的数量
                    if (trie[p][u ^ 1] == 0) return sum; //没有结点可以继续走下去,直接返回
                    p = trie[p][u ^ 1]; //继续往异或的结点走下去
                } else { //high当前i位为0, x与以前数异或的第i为必须为0
                    if (trie[p][u] == 0) return sum; //没有结点走下去
                    p = trie[p][u]; //寻找与x的第i位相同的进制,异或结果为0
                }
            }
            sum += cnt[p]; //加上走到最后的结点数
            return sum;
        }
    }
    
    作者:Tizzi
    链接:https://leetcode.cn/problems/count-pairs-with-xor-in-a-range/solutions/2045650/javac-zi-dian-shu-fu-zi-dian-shu-mo-ban-566um/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
    • 复杂度
      • 时间复杂度: O ( n l o g C ) O(nlogC) O(nlogC)
      • 空间复杂度: O ( n l o g C ) O(nlogC) O(nlogC) n n n为节点数量, C C C为字符集大小
  • TrieNode

    class Solution {
        class TrieNode{
            TrieNode[] next = new TrieNode[2];
            int cnt;
        }    
        TrieNode root;
        public int countPairs(int[] nums, int low, int high) {
            return get(nums, high) - get(nums, low - 1);
        }
        public int get(int[] nums, int t){
            root = new TrieNode();
            int ans = 0;
            for (int i = 0; i < nums.length; i++){
                ans += search(nums[i], t);
                add(nums[i]);
            }
            return ans;
        }
        public void add(int num){
            TrieNode p = root;
            for (int i = 14; i >= 0; i--){ // 高位至低位
                int u = (num >> i) & 1;
                if (p.next[u] == null) p.next[u] = new TrieNode();
                p = p.next[u];
                p.cnt++;
            }
            
    
        }
        public int search(int num, int t){
            int count = 0;
            TrieNode p = root;
            for (int i = 14; i >= 0; i--){
                int u = (num >> i) & 1;
                if (((t >> i) & 1) == 1){// t的第i位为1
                    if (p.next[u] != null) count += p.next[u].cnt;// 结果为0 之后节点为任意值均符合条件 直接加cnt
                    if (p.next[u ^ 1] == null) return count;// 之后没有节点可以走了 直接返回结果
                    p = p.next[u ^ 1];// t的第i位为0
                }else{
                    if (p.next[u] == null) return count;
                    p = p.next[u];
                }
            }
            count += p.cnt;
            return count;
        }
    }
    
    
    • 复杂度
      • 时间复杂度: O ( n l o g C ) O(nlogC) O(nlogC)
      • 空间复杂度: O ( n l o g C ) O(nlogC) O(nlogC) n n n为节点数量, C C C为字符集大小
*哈希表
  • 思路:

    • 基于异或性质 x ⊕ y = t x \oplus y = t xy=t等价于$y = t \oplus x $,可以统计nums中每个数的出现次数,并记录在哈希表cnt
    • 然后遍历cnt的每一个键 x x x,那么 c n t [ x ] ∗ c n t [ x ⊕ t ] cnt[x]*cnt[x\oplus t] cnt[x]cnt[xt],累加结果除以2即为数组中任意两数异或结果为 y y y的对数
    • 那么对区间 [ l o w , h i g h ] [low,high] [low,high]的每个数都这样统计即可得最终结果。
  • 实现[超时]

    class Solution {
        public int countPairs(int[] nums, int low, int high) {
            Map<Integer, Integer> numToCount = new HashMap<>();
            for (int num : nums){
                numToCount.put(num, numToCount.getOrDefault(num, 0) + 1);
            }
            int count = 0;
            for (int i = low; i <= high; i++){
                for (var node: numToCount.entrySet()){
                    count += node.getValue() * numToCount.getOrDefault(node.getKey() ^ i,0);
                }
            }
            return count / 2;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ∗ C ) O(n*C) O(nC) n n n为数组长度, C C C h i g h − l o w high-low highlow
      • 空间复杂度: O ( n ) O(n) O(n)
  • 优化:

    • [ 0 , t ] [0,t] [0,t]分成若干区间,计算每个区间的答案[没咋懂]

      • 若右数第m位(注意m>1)是1, 则可划分出一组只需考虑第m位及以左;
      • 特别地, 若右边从第1位开始连续m位是1, 则有一组, 只需考虑第m+1位及以左. (例如若 x=10011, 最后一组忽略右2位) 统一来看, 则可通过 x+1 的 第m位(m>=1)为条件进行判断, 计算异或时记得减一

      image-20230106191115687

    • 基于容斥原理,把 [ l o w , h i g h ] [low,high] [low,high]转化为计算 [ 0 , h i g h ] [0,high] [0,high] [ 0 , l o w − 1 ] [0,low-1] [0,low1]相减的结果

    class Solution {
        public int countPairs(int[] nums, int low, int high) {
            int ans = 0;
            var cnt = new HashMap<Integer, Integer>();
            for (int x : nums) cnt.put(x, cnt.getOrDefault(x, 0) + 1);
            for (++high; high > 0; high >>= 1, low >>= 1) {
                var nxt = new HashMap<Integer, Integer>();
                for (var e : cnt.entrySet()) {
                    int x = e.getKey(), c = e.getValue();
                    if ((high & 1) == 1) ans += c * cnt.getOrDefault(x ^ (high - 1), 0);
                    if ((low & 1) == 1)  ans -= c * cnt.getOrDefault(x ^ (low - 1), 0);
                    nxt.put(x >> 1, nxt.getOrDefault(x >> 1, 0) + c);
                }
                cnt = nxt;
            }
            return ans / 2;
        }
    }
    
    作者:灵茶山艾府
    链接:https://leetcode.cn/problems/count-pairs-with-xor-in-a-range/solutions/2045560/bu-hui-zi-dian-shu-zhi-yong-ha-xi-biao-y-p2pu/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
    • 复杂度
      • 时间复杂度: O ( n ) O(n) O(n),严格来说为 O ( n + n l o g U n ) O(n+nlog{\frac{U}{n}}) O(n+nlognU), n n n为数组长度, U U U为数组中的最大值
      • 空间复杂度: O ( n ) O(n) O(n)

3.数组中两个数的最大异或值【LC421】

Given an integer array nums, return the maximum result of nums[i] XOR nums[j], where 0 <= i <= j < n.

字典树
  • 思路

    • 首先使用字典树存储数组中元素的二进制形式,由于 n u m s [ i ] ≤ 2 31 − 1 nums[i] \le 2^{31}-1 nums[i]2311,因此需要用32位二进制表示就;

    • 然后枚举 n u m s [ i ] nums[i] nums[i],并将 n u m s [ 0 , i − 1 ] nums[0,i-1] nums[0,i1]作为 n u m s [ j ] nums[j] nums[j]放入字典树中,找到使异或值最大的 n u m s [ j ] nums[j] nums[j]返回,然后更新异或值

      • 如果将 n u m s [ 0 , i ] nums[0,i] nums[0,i]放入字典树,那么当 i = 0 i=0 i=0时,只能返回自身,异或最大值为0
    • 如何找到使异或值最大的 n u m s [ j ] nums[j] nums[j]

      从字典树的根节点开始遍历,并从最高位往低位查询,优先让每一位的异或结果为1,即优先匹配与之不同的二进制位【局部最优】,这样才能使最终的异或结果最大【全局最优】,

  • 实现

    class Solution {
        class TrieNode{
            TrieNode[] next = new TrieNode[2];
        }
        TrieNode root = new TrieNode();
        public int findMaximumXOR(int[] nums) {
            TrieNode root;
            int res = 0;
            for (int num : nums){
                add(num);
                res = Math.max(search(num) ^ num, res);
                
            }
            return res;
        }
        public void add(int x){
            TrieNode p = root;
            for (int i = 31; i >= 0; i--){
                int u = (x >> i) & 1;
                if (p.next[u] == null) p.next[u] = new TrieNode();
                p = p.next[u];
            }
        } 
        public int search(int x){
            int res = 0;
            TrieNode p = root;
            for (int i = 31; i >= 0; i--){
                int u = (x >> i) & 1, v = 1 - u; 
                if (p.next[v] != null) {
                    res |= (v << i);
                    p = p.next[v];
                }else {
                    res |= (u << i);
                    p = p.next[u];
                }         
            }
            return res;
        }
    } 
    
    • 复杂度
      • 时间复杂度: O ( n ) O(n) O(n)
      • 空间复杂度: O ( n ) O(n) O(n)
  • 实现:字典树直接返回每个数nums[i]nums[0,i-1]的最大异或结果

    2023/2/28

    class Solution {
        class TireNode{
            TireNode[] next = new TireNode[2];
            int cnt;
        }
        TireNode root;
        public void insert(int num){
            TireNode p = root;
            for (int i = 31; i >= 0; i--){
                int u =((num >> i) & 1);
                if (p.next[u] == null) p.next[u] = new TireNode();
                p = p.next[u]; 
            }
        }
        public int searchMaxXOR(int num){
            int res = 0;
            TireNode p = root;
            for (int i = 31; i >= 0; i--){
                int u = ((num >> i) & 1);
                if (p.next[1 - u] != null) {
                    p = p.next[1 - u];
                    res |= (1 << i);
                }else{
                   p = p.next[u];
                }
            }
            return res;
        }
        public int findMaximumXOR(int[] nums) {
            int res = 0;
            root = new TireNode();
            insert(nums[0]);
            for (int i = 1; i < nums.length; i++){
                res = Math.max(res, searchMaxXOR(nums[i]));
                insert(nums[i]);
            }
            return res;
        }
    }
    

删除子文件夹【LC1233】

Given a list of folders folder, return the folders after removing all sub-folders in those folders. You may return the answer in any order.

If a folder[i] is located within another folder[j], it is called a sub-folder of it.

The format of a path is one or more concatenated strings of the form: '/' followed by one or more lowercase English letters.

  • For example, "/leetcode" and "/leetcode/problems" are valid paths while an empty string and "/" are not.
排序+双指针
  • 思路:

    将字符串按照字典顺序排序,将所有位于一个根目录下的文件夹视为一组,使用滑动窗口找到所有的根目录,放入结果集。

  • 实现

    排序后,左指针指向的目录为根目录,如果右指针对应的目录包含根目录+'/',那么证明它们位于一个根目录下

    class Solution {
        public List<String> removeSubfolders(String[] folder) {
            Arrays.sort(folder);
            List<String> res = new ArrayList<>();
            int l = 0, n = folder.length;
            while (l < n){
                int lenL = folder[l].length();
                int r = l;
                while(r + 1 < n && folder[r + 1].length() > lenL && folder[r + 1].substring(0,lenL).equals(folder[l]) 
                    && folder[r + 1].charAt(lenL) == '/'){
                    r++;
                }
                res.add(folder[l]);
                l = r + 1;
            }
            return res;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n l o g n ) O(nlog n) O(nlogn)
      • 空间复杂度: O ( 1 ) O(1) O(1)
排序+去重
  • 实现:换一种写法,枚举判断每一个目录是否位于前一个根目录下,如果是,那么跳过这个目录;如果不是,那么这个目录为新的根目录,放入结果中

    class Solution {
        public List<String> removeSubfolders(String[] folder) {
            Arrays.sort(folder);
            List<String> res = new ArrayList<>();
            int l = 0, n = folder.length;
            while (l < n){
                int lenL = folder[l].length();
                int r = l;
                while(r + 1 < n && folder[r + 1].length() > lenL && folder[r + 1].substring(0,lenL).equals(folder[l]) 
                    && folder[r + 1].charAt(lenL) == '/'){
                    r++;
                }
                res.add(folder[l]);
                l = r + 1;
            }
            return res;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ∗ l ∗ l o g n ) O(n*l*log n) O(nllogn) n n n是数组的长度, l l l是文件夹的平均长度,排序所需要的时间复杂度为 O ( n ∗ l ∗ l o g n ) O(n*l*log n) O(nllogn)
      • 空间复杂度: O ( l ) O(l) O(l),截取子串需要的空间
字典树:全部加入再判断
  • 思路:

    使用字典树存储所有的目录,并使用属性isEnd记录是否是一个目录的末尾。然后对于每一个目录判断其所有前缀是否是根目录(前缀的末尾isEnd属性是true,并且下一个字符串是’/')。如果是,那么该目录是子目录,跳过;如果不是,那么该目录是根目录,加入结果集

  • 实现

    • 字典树
      • 可能包含27个字符:26个小写字母+‘/’
      • insert:将所有字符串加入字典树中
      • search:搜索每个字符串的前缀字符串是否是根目录,是,则返回true;否,返回false,加入结果集。
    class Solution {
        class Tire{
            class TireNode{
                TireNode[] next = new TireNode[27];
                boolean isEnd;
            }
            TireNode root;
            public Tire(){
                root = new TireNode();
            }
            public void insert(String s){
                TireNode p = root;
                for (char c : s.toCharArray()){
                    int u = c == '/' ? 26 : c - 'a';
                    if (p.next[u] == null) p.next[u] = new TireNode();
                    p = p.next[u];
                }
                p.isEnd = true;
            }
            public boolean search(String s){
                TireNode p = root;
                for (char c : s.toCharArray()){                               
                    if (p.isEnd && c == '/' ) return true;// 是根目录
                    int u = c == '/' ? 26 : c - 'a';
                    p = p.next[u];
                }
                return false;// 不是根目录
            }
        }
        public List<String> removeSubfolders(String[] folder) {
            List<String> res = new ArrayList<>();
            Tire tire = new Tire();
            for (String s : folder){
                tire.insert(s);// 加入字典树
            }
            for (String s : folder){
                if (!tire.search(s)){// 判断前缀字符串是否是根目录
                    res.add(s);
                }
            }
            return res;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ∗ l ) O(n*l) O(nl) n n n是数组的长度, l l l是文件夹的平均长度
      • 空间复杂度: O ( n ∗ l ) O(n*l) O(nl)
字典树:边加入边判断
  • 思路:边加入边判断

    将数组进行排序,保证根目录一定出现在子目录之前,边加入目录,一边判断目录是否包含之前的根目录,在insert中直接返回true或者false

  • 实现

    class Solution {
        class Tire{
            class TireNode{
                TireNode[] next = new TireNode[27];
                boolean isEnd;
            }
            TireNode root;
            public Tire(){
                root = new TireNode();
            }
            public boolean insert(String s){
                TireNode p = root;
                for (char c : s.toCharArray()){
                    if (p.isEnd && c == '/') return false;// 子目录 add不成功
                    int u = c == '/' ? 26 : c - 'a';
                    if (p.next[u] == null) p.next[u] = new TireNode();
                    p = p.next[u];
                }
                p.isEnd = true;
                return true; // 新的根目录 add成功
            }
           
        }
        public List<String> removeSubfolders(String[] folder) {
            Arrays.sort(folder);
            List<String> res = new ArrayList<>();
            Tire tire = new Tire();
            for (String s : folder){
                if(tire.insert(s)){
                    res.add(s);
                }
            }
            return res;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ∗ l ∗ l o g n ) O(n*l*log n) O(nllogn) n n n是数组的长度, l l l是文件夹的平均长度,排序所需要的时间复杂度为 O ( n ∗ l ∗ l o g n ) O(n*l*log n) O(nllogn)insert所需要的时间复杂度为 O ( n ∗ l ) O(n*l) O(nl)
      • 空间复杂度: O ( n ∗ l ) O(n*l) O(nl)

单词替换【LC648】

在英语中,我们有一个叫做 词根(root) 的概念,可以词根后面添加其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)。例如,词根an,跟随着单词 other(其他),可以形成新的单词 another(另一个)。

现在,给定一个由许多词根组成的词典 dictionary 和一个用空格分隔单词形成的句子 sentence。你需要将句子中的所有继承词词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。

你需要输出替换之后的句子。

  • 思路:使用字典树存储dictionary中的词根,然后使用split函数分割sentence,使用insearch函数查找每一个单词是否有词根出现,如果有则加最短词根添加至结果集;否则将原字符串添加至结果集

  • 实现

    • 构建字典树数据结构,并编写insert方法和search方法
      • insert方法同其他题
      • search方法返回最短前缀,如果不存在则返回空字符串
    class Solution {
        class TireNode{
            TireNode[] next = new TireNode[26];
            int cnt;
        }
        TireNode root;
        public String replaceWords(List<String> dictionary, String sentence) {
            root = new TireNode();
            for (String s : dictionary){
                insert(s);
            }
            StringBuilder sb = new StringBuilder();
            String[] words = sentence.split(" ");
            for (String word : words){
                String r = search(word);
                if (r != ""){
                    sb.append(r + " ");
                }else{
                    sb.append(word + " ");
                }
            }
            sb.deleteCharAt(sb.length() - 1);
            return sb.toString();
        }
        public void insert(String s){
            TireNode p = root;
            for (int i = 0; i < s.length(); i++){
                int u = s.charAt(i) - 'a';
                if (p.next[u] == null) p.next[u] = new TireNode();
                p = p.next[u];
            }
            p.cnt++;
        }
        public String search(String s){
            TireNode p = root;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < s.length(); i++){
                int u = s.charAt(i) - 'a';
                if (p.next[u] == null) return "";
                sb.append(s.charAt(i));         
                p = p.next[u];
                if (p.cnt > 0) return sb.toString();
            }
            return "";
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( d + n ) O(d+n) O(d+n) d d d是dictionary的总长度, n n n是sentence的长度, l l l是文件夹的平均长度
      • 空间复杂度: O ( d + s ) O(d+s) O(d+s)

实现一个魔法字典【LC676】

设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于你构建的字典中。

实现 MagicDictionary 类:

  • MagicDictionary() 初始化对象
  • void buildDict(String[] dictionary) 使用字符串数组 dictionary 设定该数据结构,dictionary 中的字符串互不相同
  • bool search(String searchWord) 给定一个字符串 searchWord ,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 true ;否则,返回 false
  • 思路:

    将字典字符串存储在字典树中,字典树结构与其他字典树相同。search函数使用dfs搜索前缀数的每一条路径,由于要求需要改变一个字符,因此需要记录改变的字符数目,当最后一个字符是字典树某个单词的末尾并且只修改了一个字符时返回true

  • 实现

    class MagicDictionary {
        class TireNode{
            TireNode[] next = new TireNode[26];
            int cnt = 0;
        }
        TireNode root;
        /** Initialize your data structure here. */
        public MagicDictionary() {
            root = new TireNode();
        }
        public void insert(String s){
            TireNode p = root;
            for (int i = 0; i < s.length(); i++){
                int u = s.charAt(i) - 'a';
                if (p.next[u] == null) p.next[u] = new TireNode();
                p = p.next[u];
            }
            p.cnt++;
        }
        public void buildDict(String[] dictionary) {
            for (String s : dictionary){
                insert(s);
            }
        }
        
        public boolean search(String s) {
            return dfs(s, root, 0, 0);
        }
        public boolean dfs(String s, TireNode p, int i, int edit){
            if (p == null) return false;
            if (p.cnt > 0 && i == s.length() && edit == 1 ) return true;
            if (i < s.length() && edit <= 1){
                boolean found = false;
                for (int j = 0; j < 26 && !found; j++){
                    int next = j == s.charAt(i) - 'a' ? edit : edit + 1;
                    found =  dfs(s, p.next[j], i + 1, next);
                }
                return found;
            }
            return false;
        }
    }
    
    /**
     * Your MagicDictionary object will be instantiated and called as such:
     * MagicDictionary obj = new MagicDictionary();
     * obj.buildDict(dictionary);
     * boolean param_2 = obj.search(searchWord);
     */
    
    • 复杂度
      • 时间复杂度: O ( d + n ) O(d+n) O(d+n) d d d是dictionary的总字符长度, n n n是searchWord的平均长度
      • 空间复杂度: O ( d ) O(d) O(d)

单词的压缩编码【LC820】

单词数组 words有效编码 由任意助记字符串 s 和下标数组 indices 组成,且满足:

  • words.length == indices.length
  • 助记字符串 s'#' 字符结尾
  • 对于每个下标 indices[i]s 的一个从 indices[i] 开始、到下一个 '#' 字符结束(但不包括 '#')的 子字符串 恰好与 words[i] 相等

给你一个单词数组 words ,返回成功对 words 进行编码的最小助记字符串 s 的长度 。

  • 思路:

    • 由题意可知,如果一个字符串是另一个字符串的后缀字符串,那么可以通过较长字符串的下标偏移得到另一个字符串,因此我们可以使用前缀数存储所有字符串的反转形式,就相当于存储了每个字符串的后缀。
    • 最终的长度即为从根节点到每个叶子节点的路径长度
  • 实现

    class Solution {
        class TireNode{
            boolean isChild;
            TireNode[] next = new TireNode[26];
        }
        TireNode root;
        public void insert(String  s){
            TireNode p = root;
            for (int i = s.length() - 1; i >= 0; i--){
                int u = s.charAt(i) - 'a';
                if (p.next[u] == null) {
                    p.next[u] = new TireNode();          
                }
                p.isChild = false;
                p = p.next[u];
            }
        }
        public int minimumLengthEncoding(String[] words) {
            root = new TireNode();
            for (String word : words){
                insert(word);    
            }
            return dfs(root, 1);
        }
        public int dfs (TireNode p, int len){
            boolean isLeaf = true;
            int res = 0;
            for (TireNode u : p.next){
                if (u != null){
                    isLeaf = false;
                    res += dfs(u, len + 1);
                }
            }
            if (isLeaf) {
                res += len;
            }
            return res;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( d ) O(d) O(d) d d d是words的总字符长度
      • 空间复杂度: O ( d ) O(d) O(d)

键值映射【LC677】

实现一个 MapSum 类,支持两个方法,insertsum

  • MapSum() 初始化 MapSum 对象
  • void insert(String key, int val) 插入 key-val 键值对,字符串表示键 key ,整数表示值 val 。如果键 key 已经存在,那么原来的键值对将被替代成新的键值对。
  • int sum(string prefix) 返回所有以该前缀 prefix 开头的键 key 的值的总和。
  • 思路:

    使用字典树存储每个key

    • insert在每个key的末尾字符记录相应的val
    • sum先找到前缀的末尾节点,然后从该节点进行dfs,搜索以该前缀开头的每一个字符串的val值总和
  • 实现

    class MapSum {
        class TireNode{
            TireNode[] next = new TireNode[26];
            int val = 0;
        }
        TireNode root;
        /** Initialize your data structure here. */
        public MapSum() {
            root = new TireNode();
        }
        
        public void insert(String key, int val) {
            TireNode p = root;
            for (char c : key.toCharArray()){
                int u = c - 'a';
                if (p.next[u] == null) p.next[u] = new TireNode();
                p = p.next[u];
            }
            p.val = val;
        }
        
        public int sum(String prefix) {
            int res = 0;
            TireNode p = root;
            for (char c : prefix.toCharArray()){
                int u = c - 'a';
                if (p.next[u] == null) return res;
                p = p.next[u];
            }
            return dfs(p);
    
        }
        public int dfs(TireNode p){
            if (p == null) return 0;
            int sum = p.val;
            for (int i = 0; i < 26; i++){
                sum +=  dfs(p.next[i]);
            }
            return sum;
        }
    }
    
    /**
     * Your MapSum object will be instantiated and called as such:
     * MapSum obj = new MapSum();
     * obj.insert(key,val);
     * int param_2 = obj.sum(prefix);
     */
    
    • 复杂度
      • 时间复杂度:insert的时间复杂度为 O ( n ) O(n) O(n) n n n为字符串的长度,sum的时间复杂度为 O ( d ) O(d) O(d) d d d为字典树的大小
      • 空间复杂度: O ( d ) O(d) O(d)

字符流【LC1032】

设计一个算法:接收一个字符流,并检查这些字符的后缀是否是字符串数组 words 中的一个字符串。

例如,words = ["abc", "xyz"] 且字符流中逐个依次加入 4 个字符 'a''x''y''z' ,你所设计的算法应当可以检测到 "axyz" 的后缀 "xyz"words 中的字符串 "xyz" 匹配。

按下述要求实现 StreamChecker 类:

  • StreamChecker(String[] words) :构造函数,用字符串数组 words 初始化数据结构。
  • boolean query(char letter):从字符流中接收一个新字符,如果字符流中的任一非空后缀能匹配 words 中的某一字符串,返回 true ;否则,返回 false
  • 思路:【字典树】

    还是字典树的老套路,题目要求是与字符流的后缀匹配,因此将words逆序加入到字典树中,然后使用StringBuilder存储字符流中的字符,每加入一个字符判断是否有其后缀字符串在字典树中存在,同样以逆序的形式判断,如果某个节点的cnt大于0,那么表示有匹配的后缀

  • 实现

    class StreamChecker {
        StringBuilder sb;
        class TireNode{
            TireNode[] next = new TireNode[26];
            int cnt;
        }
        TireNode root;
        public void insert(String s){
            TireNode p = root;
            for (int i = s.length() - 1; i >= 0; i--){
                int u = s.charAt(i) - 'a';
                if (p.next[u] == null)  p.next[u] = new TireNode();
                p = p.next[u];
            }
            p.cnt++;
        }
        public StreamChecker(String[] words) {
            root = new TireNode();
            sb = new StringBuilder();
            for (String word : words){
                insert(word);
            }
        }
        
        public boolean query(char letter) {
            sb.append(letter);
            TireNode p = root;
            for (int i = sb.length() - 1; i >= 0; i--){
                int u = sb.charAt(i) - 'a';
                if (p.next[u] == null) return false;
                p = p.next[u];
                if (p.cnt > 0) return true;
            }
            return p.cnt > 0;
        }
    }
    
    /**
     * Your StreamChecker object will be instantiated and called as such:
     * StreamChecker obj = new StreamChecker(words);
     * boolean param_1 = obj.query(letter);
     */
    
    • 复杂度
      • 时间复杂度:insert的时间复杂度为 O ( n ) O(n) O(n) n n n为字符串的长度,StreamChecker的时间复杂度为 O ( m n ) O(mn) O(mn) m m m为字符串的大小,query的时间复杂度为 O ( d ) O(d) O(d) d d d为字典树的大小
      • 空间复杂度: O ( d ) O(d) O(d)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值