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

链接:

题解:电话号码的字母组合II

1、把dict中的字符映射为数字,建设以数字0,9为next的字典树,在创建的过程中需要记录路径数量,因为query满足的dict中的前缀就可以了

2.query查询的时候,将末尾的cur->count放入到结果里面

写法 B:数字 Trie

  • 建 Trie :把每个单词转成数字串插入,O(∑|dict|)

  • 每个查询 :沿着数字串走 |query| 步,直接返回当前节点的 countO(|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(你现在的写法)

  • 建 TrieO(∑|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:建树时就把每个单词转成唯一的数字串,查询时直接沿数字串走,没有分支爆炸

相关推荐
zly350039 分钟前
VMware Converter Standalone 物理机转化为虚拟机后源1硬盘变成了2个硬盘(2个硬盘文件)虚拟机无法启动。
linux·运维·服务器
2401_839080541 小时前
C++常见八股
数据结构·c++·算法
青山是哪个青山1 小时前
LeetCode 188:买卖股票的最佳时机 IV
算法
a187927218312 小时前
【算法】树(二):队列与祖先——BFS 骨架三配件、短路透传与合流
算法·leetcode·二叉树··bfs·算法讲解·树遍历
syagain_zsx2 小时前
算法基础篇 · 03 枚举(C++ 题解)
c++·算法·二进制·枚举
GeW2 小时前
从内核到数据:Linux与工业数据库如何支撑高端制造
linux
weixin_307779132 小时前
C++代码实现MATLAB中的ode23tb函数功能
开发语言·c++·算法·matlab
我是章汕呐3 小时前
地级市能源消耗量及消耗强度数据【2006-2023年】平衡面板
人工智能·经验分享·算法·回归
小小的木头人3 小时前
Linux systemd Service 与 Timer 定时任务原理及实战
linux