270 · 电话号码的字母组合II(Trie)

链接:

题解:电话号码的字母组合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:建树时就把每个单词转成唯一的数字串,查询时直接沿数字串走,没有分支爆炸。

相关推荐
CQU_JIAKE4 分钟前
10.31[A]
算法·深度优先
corey936 分钟前
Linux 学习 - 第十四周作业
linux·运维·学习
zwc32104320558 分钟前
Day 5:threshold与inRange阈值分割
c++·图像处理·人工智能·opencv·算法·计算机视觉
云-缩影11 分钟前
实际项目中怎么选排序算法:从面试题到工程落地
java·算法·排序算法
可乐_不渴13 分钟前
# C语言文件操作全解析:从 fopen 到 fclose,一文吃透文件 IO
linux·服务器·c语言
Pniubi23 分钟前
力扣46全排列(回溯(递归))
算法·leetcode·职场和发展
草莓熊Lotso29 分钟前
【C++ 基于微服务的即时通讯系统】网关基建:cpp-httplib 与 WebSocketpp 从入门到实战
linux·开发语言·数据库·c++·redis·微服务·云原生
Ruiery9 小时前
Linux 6.6内核内存管理深度解析(十六):mmap — 用户态“申请内存“的真相
linux·运维·服务器
欧米欧9 小时前
Linux系统篇(七),环境变量的底层溯源,核心获取机制与安全应用
linux·运维·服务器
Ruiery9 小时前
Linux 6.6内核内存管理深度解析(十八):KASLR 内核地址空间随机化
linux·运维·服务器·安全