1. 前缀统计问题与Trie树解法
字符串前缀匹配是算法竞赛和实际开发中的常见需求。想象你正在开发一个搜索引擎的自动补全功能,当用户输入"app"时,系统需要快速找出所有以"app"开头的候选词(如"apple"、"application"等)。这类问题本质上就是前缀统计问题。
给定n个字符串和m个查询,每个查询给出一个字符串T,要求统计n个字符串中有多少是T的前缀。例如:
- 字符串集:["a", "app", "apple", "application"]
- 查询"apple"的前缀匹配结果为3("a", "app", "apple")
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Trie树数据结构解析
2.1 Trie树基础结构
Trie树(前缀树)是一种专门处理字符串匹配的树形数据结构。它的核心思想是利用字符串的公共前缀来减少查询时间。每个节点代表一个字符,从根节点到某一节点的路径构成一个字符串。
标准Trie节点通常包含:
- 子节点指针数组(长度取决于字符集,如26个小写字母)
- 结束标记(标记是否有字符串在此节点结束)
cpp复制struct TrieNode {
TrieNode* children[26];
bool isEnd;
};
2.2 改进的计数Trie节点
在本题中,我们需要处理重复字符串的情况。传统bool型结束标记无法统计重复次数,因此改进为int型计数器:
cpp复制int trie[N][26]; // 子节点数组
int ed[N]; // 结束计数器
int tot = 1; // 节点总数(根节点为1)
这种设计使得:
- 插入重复字符串时,ed[p]++即可
- 查询时可以累加所有经过节点的ed值
3. 算法实现细节
3.1 Trie插入操作
插入操作将字符串逐字符插入Trie树:
cpp复制void insert(string s) {
int p = 1; // 从根节点开始
for(char c : s) {
int ch = c - 'a';
if(!trie[p][ch])
trie[p][ch] = ++tot; // 新建节点
p = trie[p][ch]; // 移动到子节点
}
ed[p]++; // 在终点增加计数
}
时间复杂度:O(L),L为字符串长度
注意:初始时tot=1,表示根节点编号为1。这是为了避免memset初始化整个数组,提高效率。
3.2 前缀查询操作
查询时沿Trie路径遍历,累加路径上的所有结束计数:
cpp复制int search(string t) {
int p = 1, ans = 0;
for(char c : t) {
int ch = c - 'a';
if(!trie[p][ch]) break; // 无后续路径
p = trie[p][ch];
ans += ed[p]; // 累加当前节点的计数
}
return ans;
}
关键点:
- 遇到不存在的分支立即终止
- 每个节点的ed值代表以该位置结尾的字符串数量
4. 复杂度分析与优化
4.1 时间复杂度
- 建树:O(N*Lavg),N为字符串数量,Lavg为平均长度
- 查询:O(M*Lavg),M为查询次数
- 总体:O((N+M)*Lavg)
4.2 空间优化技巧
原始实现使用静态数组可能浪费空间。可以考虑:
- 动态分配节点:
cpp复制struct Node {
unordered_map<char, Node*> children;
int count = 0;
};
- 数组大小估算:
- 题目通常给出总字符数限制(如1e6)
- 每个字符需要一个节点,tot最大值≈总字符数
5. 边界情况与测试用例
5.1 常见边界情况
- 空字符串处理:
- 插入""时应在根节点设置ed[1]++
- 查询""时应返回ed[1]
- 重复字符串:
cpp复制insert("a");
insert("a");
search("a"); // 应返回2
- 非匹配路径:
cpp复制insert("appl
