

链接:
题解:电话号码的字母组合II
1、把dict中的字符映射为数字,建设以数字0,9为next的字典树,在创建的过程中需要记录路径数量,因为query满足的dict中的前缀就可以了
2.query查询的时候,将末尾的cur->count放入到结果里面
写法 B:数字 Trie
-
建 Trie :把每个单词转成数字串插入,
O(∑|dict|) -
每个查询 :沿着数字串走
|query|步,直接返回当前节点的count,O(|query|) -
总时间 :
O(∑|dict| + ∑|queries|)
这是严格线性 的,∑|dict| + ∑|queries| ≤ 10^5,非常快。
cpp
class Solution {
public:
struct TrieNode {
TrieNode* next[10];
int cnt; // 有多少个单词经过该节点
TrieNode() : cnt(0) {
for (int i = 0; i < 10; ++i) {
next[i] = nullptr;
}
}
};
int charToDigit(char c) {
if (c <= 'c') return 2; // a b c
if (c <= 'f') return 3; // d e f
if (c <= 'i') return 4; // g h i
if (c <= 'l') return 5; // j k l
if (c <= 'o') return 6; // m n o
if (c <= 's') return 7; // p q r s
if (c <= 'v') return 8; // t u v
return 9; // w x y z
}
void insert(TrieNode* root, const string& word) {
TrieNode* cur = root;
for (char c : word) {
int d = charToDigit(c);
if (!cur->next[d]) {
cur->next[d] = new TrieNode();
}
cur = cur->next[d];
cur->cnt++;
}
}
vector<int> letterCombinationsII(const vector<string>& queries,
const vector<string>& words) {
TrieNode* root = new TrieNode();
// 将字典中的单词转换成数字串,插入数字 Trie
for (const string& word : words) {
insert(root, word);
}
vector<int> result;
result.reserve(queries.size());
for (const string& query : queries) {
TrieNode* cur = root;
bool ok = true;
for (char c : query) {
int d = c - '0';
if (!cur->next[d]) {
ok = false;
break;
}
cur = cur->next[d];
}
if (ok) {
result.push_back(cur->cnt);
} else {
result.push_back(0);
}
}
return result;
}
};
cpp
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;
class Solution {
public:
struct Trie {
Trie() {
next.resize(26, nullptr);
end = false;
count = 0; // 以该节点为前缀的单词数量
}
vector<Trie*> next;
bool end;
int count;
};
void insert_trie(Trie* root, const string& word) {
Trie* cur = root;
for (char ch : word) {
int i = ch - 'a';
if (!cur->next[i]) {
cur->next[i] = new Trie;
}
cur = cur->next[i];
cur->count++; // 经过该节点,前缀计数 +1
}
cur->end = true;
}
// DFS:走到 query 末尾时,把当前节点的 count 累加(覆盖完整单词 + 部分前缀)
void dfs(int begin, Trie* root, const string& query,
const unordered_map<char, string>& mappings, int& count) {
if (begin == query.size()) {
count += root->count;
return;
}
char digit = query[begin];
auto it = mappings.find(digit);
if (it == mappings.end()) return;
for (char ch : it->second) {
int i = ch - 'a';
if (root->next[i]) {
dfs(begin + 1, root->next[i], query, mappings, count);
}
}
}
vector<int> letterCombinationsII(const vector<string>& queries,
const vector<string>& words) {
if (words.empty()) {
return vector<int>(queries.size(), 0);
}
Trie* root = new Trie;
for (const auto& word : words) {
insert_trie(root, word);
}
unordered_map<char, string> mappings = {
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
vector<int> result;
result.reserve(queries.size());
for (const auto& query : queries) {
int count = 0;
dfs(0, root, query, mappings, count);
result.push_back(count);
}
return result;
}
};
// ================== 测试代码 ==================
int main() {
Solution sol;
// ---------- 样例 1 ----------
{
vector<string> queries = {"2", "3", "4"};
vector<string> dict = {"a", "abc", "de", "fg"};
vector<int> expected = {2, 2, 0};
vector<int> got = sol.letterCombinationsII(queries, dict);
cout << "样例 1:" << endl;
cout << " query = [\"2\", \"3\", \"4\"]" << endl;
cout << " dict = [\"a\", \"abc\", \"de\", \"fg\"]" << endl;
cout << " expected= [2, 2, 0]" << endl;
cout << " got = [";
for (size_t i = 0; i < got.size(); ++i) {
cout << got[i] << (i + 1 < got.size() ? ", " : "");
}
cout << "]" << endl;
cout << " " << (got == expected ? "PASS" : "FAIL") << endl << endl;
}
// ---------- 自定义样例 2:部分前缀匹配 ----------
{
// "ad" 映射为 "23",所以 "2" 能匹配 "a"(完整) 和 "ad"(部分前缀)
vector<string> queries = {"2", "23"};
vector<string> dict = {"a", "ad"};
vector<int> expected = {2, 1};
vector<int> got = sol.letterCombinationsII(queries, dict);
cout << "样例 2 (部分前缀):" << endl;
cout << " query = [\"2\", \"23\"]" << endl;
cout << " dict = [\"a\", \"ad\"]" << endl;
cout << " expected= [2, 1]" << endl;
cout << " got = [";
for (size_t i = 0; i < got.size(); ++i) {
cout << got[i] << (i + 1 < got.size() ? ", " : "");
}
cout << "]" << endl;
cout << " " << (got == expected ? "PASS" : "FAIL") << endl << endl;
}
// ---------- 自定义样例 3:多条路径合并计数 ----------
{
// "23" 可表示 "ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"
// dict = ["ad", "ae", "xyz"] -> "23" 匹配 "ad","ae"
vector<string> queries = {"23", "9"};
vector<string> dict = {"ad", "ae", "xyz"};
vector<int> expected = {2, 1};
vector<int> got = sol.letterCombinationsII(queries, dict);
cout << "样例 3 (多路径):" << endl;
cout << " query = [\"23\", \"9\"]" << endl;
cout << " dict = [\"ad\", \"ae\", \"xyz\"]" << endl;
cout << " expected= [2, 1]" << endl;
cout << " got = [";
for (size_t i = 0; i < got.size(); ++i) {
cout << got[i] << (i + 1 < got.size() ? ", " : "");
}
cout << "]" << endl;
cout << " " << (got == expected ? "PASS" : "FAIL") << endl << endl;
}
// ---------- 自定义样例 4:空字典 ----------
{
vector<string> queries = {"2", "3"};
vector<string> dict = {};
vector<int> expected = {0, 0};
vector<int> got = sol.letterCombinationsII(queries, dict);
cout << "样例 4 (空字典):" << endl;
cout << " query = [\"2\", \"3\"]" << endl;
cout << " dict = []" << endl;
cout << " expected= [0, 0]" << endl;
cout << " got = [";
for (size_t i = 0; i < got.size(); ++i) {
cout << got[i] << (i + 1 < got.size() ? ", " : "");
}
cout << "]" << endl;
cout << " " << (got == expected ? "PASS" : "FAIL") << endl << endl;
}
return 0;
}
写法 A:字母 Trie + DFS(你现在的写法)
-
建 Trie :
O(∑|dict|) -
每个查询 :从根开始 DFS,每个数字位置最多尝试 4 个字母分支。
实际访问的节点数受 Trie 剪枝影响,但最坏情况 下,如果 Trie 很密集,访问节点数可以接近
O(4^|query|)。不过由于 Trie 的节点总数是
O(∑|dict|),所以每个查询访问的节点数上限是O(min(4^|query|, ∑|dict|))。 -
总时间 :
O(∑|dict| + ∑|queries| × min(4^|query|, ∑|dict|))
在最坏情况下(比如字典里单词很多、前缀高度重合),每个查询可能遍历大量 Trie 节点,总时间可能达到 O(∑|queries| × ∑|dict|),即 5×10^4 × 5×10^4 = 2.5×10^9,会超时。
对比总结
| 维度 | 字母 Trie + DFS | 数字 Trie |
|---|---|---|
| 建 Trie | `O(∑ | dict |
| 单次查询 | 最坏 `O(min(4^ | query |
| 总时间 | 最坏 `O(∑ | queries |
| 是否超时风险 | 数据大时可能超时 | 不会 |
为什么数字 Trie 更快?
关键在于:数字 Trie 把「字母映射」这一步提前到了建树阶段。
-
字母 Trie:查询时每个数字要展开成最多 4 个字母,DFS 要枚举所有可能的字母路径,路径数指数增长(虽然被 Trie 剪枝)。
-
数字 Trie:建树时就把每个单词转成唯一的数字串,查询时直接沿数字串走,没有分支爆炸。