算法学习笔记:28.前缀树——从原理到实战,涵盖 LeetCode 与考研 408 例题

前缀树(Trie,又称字典树或单词查找树)是一种专为字符串处理设计的树形数据结构,由 Edward Fredkin 于 1960 年提出。它通过共享字符串的前缀来高效存储和检索字符串集合,在 autocomplete(自动补全)、拼写检查、IP 路由等场景中有着广泛应用。


前缀树的基本概念与结构

定义与特点

前缀树是一种多叉树,每个节点代表一个字符串的前缀,其特点如下:

  • 节点结构:每个节点包含一个布尔值isEnd(标记该节点是否为某个字符串的结尾)和若干个子节点(通常对应 26 个英文字母,也可扩展到其他字符)。
  • 根节点:为空节点,不存储任何字符,是所有字符串的起点。
  • 前缀共享:具有相同前缀的字符串会共享前缀部分的节点,显著减少存储空间。
  • 高效操作:插入、查找、前缀匹配的时间复杂度均为O(L),其中L是字符串的长度,与字符串总数无关。

结构图示

以下是存储字符串 "apple"、"app"、"banana" 的前缀树结构:

前缀树的核心操作

插入操作

思路:从根节点开始,逐个字符处理字符串:

1. 对于当前字符,若节点的子节点中不存在该字符,则创建新节点。

2. 移动到对应子节点,继续处理下一个字符。

3. 字符串处理完毕后,将最后一个节点标记为结尾(`isEnd = true`)。

Java 代码

class TrieNode {

    public TrieNode[] children; // 子节点(假设只包含小写字母)

    public boolean isEnd; // 是否为单词结尾

    public TrieNode() {

        children = new TrieNode[26]; // 26个小写字母

        isEnd = false;

    }

}

public class Trie {

    private TrieNode root;

    public Trie() {

        root = new TrieNode(); // 根节点为空

    }

// 插入字符串

    public void insert(String word) {

        TrieNode node = root;

        for (int i = 0; i < word.length(); i++) {

            char c = word.charAt(i);

            int index = c - 'a'; // 计算字符索引(0-25)

            if (node.children[index] == null) {

                node.children[index] = new TrieNode(); // 新建节点

            }

            node = node.children[index]; // 移动到子节点

        }

        node.isEnd = true; // 标记单词结尾

    }

}

查找操作

思路:从根节点开始遍历字符串的每个字符:

  1. 若当前字符的子节点不存在,返回false。
  2. 移动到对应子节点,继续处理下一个字符。
  3. 字符串处理完毕后,返回最后一个节点的isEnd(确保是完整单词)。

Java 代码

// 查找字符串是否存在于前缀树中

public boolean search(String word) {

    TrieNode node = root;

    for (int i = 0; i < word.length(); i++) {

        char c = word.charAt(i);

        int index = c - 'a';

        if (node.children[index] == null) {

            return false; // 字符不存在,查找失败

        }

        node = node.children[index];

    }

    return node.isEnd; // 必须是单词结尾

}

2.3前缀匹配操作
思路:与查找类似,但无需检查最后一个节点是否为结尾,只需确认前缀存在:


// 检查是否存在以prefix为前缀的字符串

public boolean startsWith(String prefix) {

    TrieNode node = root;

    for (int i = 0; i < prefix.length(); i++) {

        char c = prefix.charAt(i);

        int index = c - 'a';

        if (node.children[index] == null) {

            return false;

        }

        node = node.children[index];

    }

    return true; // 前缀存在

}

LeetCode 例题实战

例题 1:208. 实现 Trie (前缀树)(中等)

题目描述:实现一个 Trie 类,包含 insert、search 和 startsWith 方法。

示例

Trie trie = new Trie();

trie.insert("apple");

trie.search("apple"); // 返回 True

trie.search("app"); // 返回 False

trie.startsWith("app"); // 返回 True

trie.insert("app");

trie.search("app"); // 返回 True

解题思路

直接实现前缀树的三个核心操作,如上文所述。

代码实现
class Trie {

    private class TrieNode {

        TrieNode[] children;

        boolean isEnd;

        public TrieNode() {

            children = new TrieNode[26];

            isEnd = false;

        }

    }

    private TrieNode root;

    public Trie() {

        root = new TrieNode();

    }

    public void insert(String word) {

        TrieNode node = root;

        for (char c : word.toCharArray()) {

            int index = c - 'a';

            if (node.children[index] == null) {

                node.children[index] = new TrieNode();

            }

            node = node.children[index];

        }

        node.isEnd = true;

    }

    public boolean search(String word) {

        TrieNode node = root;

        for (char c : word.toCharArray()) {

            int index = c - 'a';

            if (node.children[index] == null) {

                return false;

            }

            node = node.children[index];

        }

        return node.isEnd;

    }

    public boolean startsWith(String prefix) {

        TrieNode node = root;

        for (char c : prefix.toCharArray()) {

            int index = c - 'a';

            if (node.children[index] == null) {

                return false;

            }

            node = node.children[index];

        }

        return true;

    }

}
复杂度分析
  • 时间复杂度:插入、查找、前缀匹配均为O(L),L是字符串长度。
  • 空间复杂度:O(L*N),N是插入的字符串数量,L是平均长度(每个字符可能创建新节点)。

例题 2:211. 添加与搜索单词 - 数据结构设计(中等)

题目描述:设计一个支持以下两种操作的数据结构:

void addWord(word)

bool search(word)

search 可以搜索文字或正则表达式字符串,字符串只包含字母.或a-z。.可以表示任何一个字母。

示例

addWord("bad")

addWord("dad")

addWord("mad")

search("pad") → false

search("bad") → true

search(".ad") → true

search("b..") → true

解题思路
  1. 数据结构:使用前缀树存储单词,节点结构与基础 Trie 相同。
  2. 搜索操作:对于.通配符,需要递归检查当前节点的所有非空子节点,其他字符则正常匹配。
搜索过程

(搜索 ".ad")

代码实现
class WordDictionary {

    private class TrieNode {

        TrieNode[] children;

        boolean isEnd;

        public TrieNode() {

            children = new TrieNode[26];

            isEnd = false;

        }

    }

    private TrieNode root;

    public WordDictionary() {

        root = new TrieNode();

    }

    public void addWord(String word) {

        TrieNode node = root;

        for (char c : word.toCharArray()) {

            int index = c - 'a';

            if (node.children[index] == null) {

                node.children[index] = new TrieNode();

            }

            node = node.children[index];

        }

        node.isEnd = true;

    }

    public boolean search(String word) {

        return searchHelper(word, 0, root);

    }

// 递归辅助函数:从index位置开始搜索,当前节点为node

    private boolean searchHelper(String word, int index, TrieNode node) {

        if (index == word.length()) {

            return node.isEnd; // 已到结尾,检查是否为单词

        }

        char c = word.charAt(index);

        if (c == '.') {

// 通配符:检查所有非空子节点

            for (TrieNode child : node.children) {

                if (child != null && searchHelper(word, index + 1, child)) {

                    return true;

                }

            }

            return false;

        } else {

// 普通字符:匹配对应子节点

            int childIndex = c - 'a';

            TrieNode child = node.children[childIndex];

            return child != null && searchHelper(word, index + 1, child);

        }

    }

}
复杂度分析
  • 时间复杂度
    • 插入:O(L),L是单词长度。
    • 搜索:最坏O(26^L)(全是通配符,需检查所有可能路径),平均O(L)。
  • 空间复杂度:O(L*N),与基础 Trie 相同。

考研 408 例题解析

例题 1:概念辨析题(选择题)

题目:关于前缀树(Trie),下列说法错误的是( )。

A. 前缀树的插入和查找时间复杂度均为O(L),L是字符串长度

B. 前缀树适合存储大量具有相同前缀的字符串

C. 前缀树的空间复杂度通常优于哈希表

D. 前缀树可以高效支持前缀匹配操作

答案:C

解析

  • A 正确:前缀树的操作时间与字符串长度线性相关。
  • B 正确:相同前缀的字符串共享节点,节省空间。
  • C 错误:对于前缀差异大的字符串,前缀树可能产生大量节点,空间复杂度劣于哈希表。
  • D 正确:前缀匹配是前缀树的核心功能,效率远高于哈希表(需遍历所有键)。

例题 2:算法设计题(408 高频考点)

题目:设计一个前缀树,统计每个字符串的出现次数,并实现一个方法getFrequency(String word),返回单词的出现次数。

解题思路
  1. 节点扩展:在 TrieNode 中增加count字段,记录以该节点为结尾的字符串的出现次数。
  2. 插入操作:插入时,若单词已存在,只需递增count;否则创建节点并初始化count = 1。
  3. 查询频率:查找单词路径,若存在则返回count,否则返回 0。
代码实现
class CountTrie {

    private class TrieNode {

        TrieNode[] children;

        int count; // 出现次数

        public TrieNode() {

            children = new TrieNode[26];

            count = 0;

        }

    }

    private TrieNode root;

    public CountTrie() {

        root = new TrieNode();

    }

    public void insert(String word) {

        TrieNode node = root;

        for (char c : word.toCharArray()) {

            int index = c - 'a';

            if (node.children[index] == null) {

                node.children[index] = new TrieNode();

            }

            node = node.children[index];

        }

        node.count++; // 递增计数

    }

    public int getFrequency(String word) {

        TrieNode node = root;

        for (char c : word.toCharArray()) {

            int index = c - 'a';

            if (node.children[index] == null) {

                return 0; // 单词不存在

            }

            node = node.children[index];

        }

        return node.count;

    }

}
复杂度分析
  • 时间复杂度:插入和查询均为O(L),与基础前缀树一致。
  • 空间复杂度:O(L*N),额外存储count字段不影响量级。

前缀树的扩展与应用

实际应用场景

  • 搜索引擎自动补全:输入前缀时,快速返回所有匹配的搜索词(如 Google 搜索提示)。
  • 拼写检查:检查单词是否存在于词典中,或推荐相似单词(如 Word 文档的拼写纠错)。
  • IP 路由表:最长前缀匹配算法用于路由选择(将 IP 地址视为字符串)。
  • 词频统计:如例题 2,统计文本中单词出现的次数。

与其他数据结构的对比

数据结构

插入时间

查找时间

前缀匹配

空间效率(前缀重复时)

前缀树

O(L)

O(L)

高效支持

优(共享前缀)

哈希表

O(L)

O(L)

不支持

劣(存储完整字符串)

红黑树

O(LlogN)

O(LlogN)

需遍历

中(存储完整字符串)

考研 408 备考要点

  • 核心考点:前缀树的结构、插入 / 查找操作、时间与空间复杂度分析。
  • 重点掌握
  1. 前缀树与哈希表的优缺点对比(适用场景)。
  2. 前缀树在前缀匹配问题中的不可替代性。
  3. 带扩展功能的前缀树设计(如计数、通配符匹配)。
  • 常见错误
    • 忽略isEnd标记,导致混淆前缀和完整单词。
    • 处理通配符时未考虑所有可能的子节点,导致漏匹配。

总结

前缀树是一种专为字符串处理优化的数据结构,其核心优势在于前缀共享和高效的字符串操作。本文通过 LeetCode 例题(208 题和 211 题)展示了基础前缀树和带通配符的扩展应用,通过考研 408 例题解析了概念辨析和功能扩展思路,并结合 SVG 图示直观呈现了树的结构与操作过程。

掌握前缀树的关键在于:

  1. 理解其节点结构与前缀共享机制。
  2. 熟练实现插入、查找、前缀匹配等核心操作。
  3. 能够根据问题需求扩展前缀树功能(如计数、通配符)。

在考研备考中,需重点关注前缀树与其他数据结构的对比,以及其在实际场景中的应用,这不仅有助于应对理论题,也能提升算法设计能力。

希望本文能够帮助读者更深入地理解前缀树算法,并在实际项目中发挥其优势。谢谢阅读!


希望这份博客能够帮助到你。如果有其他需要修改或添加的地方,请随时告诉我。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆企鹅仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值