手撕代码之前缀树(Trie)

1. Trie:非典型多叉树的奥秘

1.1 为什么说Trie是非典型多叉树?

Trie的"非典型性"体现在其结构设计哲学上:

  • 空间换时间:每个节点固定26个子节点指针(对应26个字母)
  • 隐式存储:字符信息通过指针数组索引隐式存储(非显式存储字符)
  • 路径即信息:从根节点到当前节点的路径构成存储的字符串

1.2 结点结构深度解析

class TrieNode {
    // 存储当前节点是否构成完整单词
    private boolean isEnd;  
    // 子节点指针数组(a-z映射到0-25索引)
    private TrieNode[] children;
    
    public TrieNode() {
        isEnd = false;
        children = new TrieNode[26];  // 固定26个子节点槽位
    }
}
关键设计考量:
  1. 字母映射:通过字符-'a’得到0-25的索引值
  2. 空间预分配:即使某些字母未被使用仍然保留空间
  3. 状态标记:isEnd标记路径是否构成完整单词

2. Trie的四大核心应用场景

2.1 自动补全系统

  • 输入"app"时提示"apple", "application"等
  • 实现原理:DFS遍历所有可能分支

2.2 拼写检查

  • 判断单词是否存在于词典
  • 时间复杂度:O(L) L为单词长度

2.3 IP路由

  • 最长前缀匹配查找
  • 示例:路由表存储IP段与端口的映射

2.4 输入法预测

  • 基于用户输入序列预测候选词
  • 可结合词频统计进行优化

3. 手撕Trie三连击

3.1 插入操作

public void insert(String word) {
    TrieNode node = root;
    for (char ch : word.toCharArray()) {
        int index = ch - 'a';
        if (node.children[index] == null) {
            node.children[index] = new TrieNode();
        }
        node = node.children[index];
    }
    node.isEnd = true;  // 标记单词终点
}
时间复杂度分析:
  • 最佳:O(L) 需要插入新分支
  • 最差:O(L) 路径已存在

3.2 搜索操作

public boolean search(String word) {
    TrieNode node = searchPrefix(word);
    return node != null && node.isEnd;
}

private TrieNode searchPrefix(String prefix) {
    TrieNode node = root;
    for (char ch : prefix.toCharArray()) {
        int index = ch - 'a';
        if (node.children[index] == null) {
            return null;
        }
        node = node.children[index];
    }
    return node;
}
失败场景分析:
  1. 路径不存在(提前终止)
  2. 路径存在但未标记isEnd

3.3 前缀搜索

public boolean startsWith(String prefix) {
    return searchPrefix(prefix) != null;
}
与完全搜索的区别:
  • 不检查isEnd标记
  • 只需验证路径存在性

4. Trie的时空博弈论

4.1 时间复杂度对比

操作哈希表平衡树Trie
插入O(L)O(LlogN)O(L)
查找O(L)O(LlogN)O(L)
前缀查找不支持不支持O(L)

4.2 空间复杂度对比

  • 哈希表:O(NL) 存储所有字符串
  • Trie:最差O(NL) 最佳O(L)(共享前缀)

4.3 适用场景选择指南

  • ✅ 推荐使用:高频前缀查询、需要自动补全
  • ❌ 不推荐:随机字符串集合、内存敏感场景

5. Trie优化之道

5.1 压缩Trie

  • 合并单分支路径
  • 示例:将a->p->p->l->e合并为"apple"节点

5.2 双数组Trie

  • 使用base和check两个数组存储状态
  • 空间利用率提升30%-50%

5.3 动态节点分配

class TrieNode {
    Map<Character, TrieNode> children;  // 改用HashMap
    boolean isEnd;
}

6. 实战:实现敏感词过滤器

public class SensitiveFilter {
    private TrieNode root = new TrieNode();
    
    public void addWord(String word) { /* 标准插入逻辑 */ }
    
    public String filter(String text) {
        StringBuilder result = new StringBuilder();
        int n = text.length();
        for (int i = 0; i < n; ) {
            TrieNode node = root;
            int j = i;
            while (j < n && node.children.containsKey(text.charAt(j))) {
                node = node.children.get(text.charAt(j));
                j++;
            }
            if (node.isEnd) {
                result.append("***");
                i = j;
            } else {
                result.append(text.charAt(i));
                i++;
            }
        }
        return result.toString();
    }
}

7. Trie的局限性

  1. 内存消耗:每个节点需要固定空间开销
  2. 初始化成本:需要预建完整字典树
  3. 更新代价:动态增删可能导致结构重组
  4. 字母表限制:需要预先确定字符范围
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值