【力扣Leetcode题解系列之0017— Letter Combinations of a Phone Number 电话号码的字母组合】

17. Letter Combinations of a Phone Number 电话号码的字母组合:多语言实现与分析

一、题目分析

给定一个仅包含数字 2 - 9 的字符串,返回这些数字在电话拨号键盘上所能代表的所有字母组合。1 不对应任何字母,并且结果的顺序可以是任意的。

二、常用解法

回溯法

  1. 思路
    • 回溯法是一种通过尝试所有可能的路径来找到所有解决方案的算法。在本题中,我们从左到右依次处理输入字符串中的每个数字。
    • 对于每个数字,我们知道它对应的一组字母。我们尝试选择其中一个字母,然后递归地处理下一个数字,直到处理完所有数字,此时得到一个完整的字母组合,将其加入结果集。
    • 递归完成后,需要回溯到上一步,尝试该数字对应的其他字母,以此遍历所有可能的组合。
  2. 优点:回溯法能够清晰地模拟出所有可能的组合情况,逻辑直观,易于理解和实现。对于这类需要穷举所有可能情况的问题,回溯法是一种非常自然的解决思路。

递归法(类似回溯的递归实现)

  1. 思路
    • 同样是基于递归的思想,但实现方式略有不同。对于输入字符串的第一个数字,我们遍历其对应的所有字母。对于每个字母,我们递归地处理剩余的数字字符串,得到后续的所有组合,然后将当前字母与这些组合依次拼接,得到以当前字母开头的所有完整组合,加入结果集。
    • 这种方法本质上也是在穷举所有可能的组合,与回溯法类似,但在实现结构上可能更加紧凑。
  2. 优点:代码结构相对简洁,通过递归调用自身处理子问题,充分利用了函数调用栈来保存中间状态,减少了显式的状态管理代码。

使用内置函数(如Python的product

  1. 思路
    • 在Python中,可以利用itertools.product函数来计算多个可迭代对象的笛卡尔积。对于本题,每个数字对应的字母集合就是一个可迭代对象。
    • 我们将输入字符串中每个数字对应的字母集合提取出来,然后使用product函数计算它们的笛卡尔积,得到所有可能的字母组合。最后将这些组合转换为字符串形式并返回。
  2. 优点:利用内置函数能够快速实现功能,代码简洁明了,避免了手动编写复杂的嵌套循环或递归逻辑。同时,内置函数通常经过优化,性能可能较好。

循环法

  1. 思路
    • 初始化一个结果数组res,其中包含一个空字符串,表示初始状态。然后依次遍历输入字符串中的每个数字。
    • 对于每个数字,我们根据其对应的字母集合,将结果数组res中的每个字符串与字母集合中的每个字母进行拼接,生成新的结果数组。这样,在遍历完所有数字后,res中就包含了所有可能的字母组合。
  2. 优点:通过简单的循环和字符串拼接操作实现,逻辑相对直观,避免了递归带来的函数调用开销和栈空间消耗,在某些情况下可能具有较好的性能。

三、多语言实现

Python实现

  1. 回溯法
class Solution:
    def letterCombinations(self, digits):
        kvmaps = {'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'}
        res = []
        self.dfs(digits, 0, res, '', kvmaps)
        return res

    def dfs(self, string, index, res, path, kvmaps):
        if index == len(string):
            if path:
                res.append(path)
            return
        for j in kvmaps[string[index]]:
            self.dfs(string, index + 1, res, path + j, kvmaps)
  1. 内置函数product
from itertools import product


class Solution:
    def letterCombinations(self, digits):
        if not digits:
            return []
        kvmaps = {'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'}
        answer = []
        for each in product(*[kvmaps[key] for key in digits]):
            answer.append(''.join(each))
        return answer
  1. 循环法
class Solution:
    def letterCombinations(self, digits):
        if digits == "": return []
        d = {'2': "abc", '3': "def", '4': "ghi", '5': "jkl", '6': "mno", '7': "pqrs", '8': "tuv", '9': "wxyz"}
        res = ['']
        for e in digits:
            res = [w + c for c in d[e] for w in res]
        return res

Java实现

  1. 回溯法
import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<String> letterCombinations(String digits) {
        List<String> res = new ArrayList<>();
        if (digits == null || digits.length() == 0) {
            return res;
        }
        String[] kvmaps = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        backtrack(digits, 0, new StringBuilder(), res, kvmaps);
        return res;
    }

    private void backtrack(String digits, int index, StringBuilder path, List<String> res, String[] kvmaps) {
        if (index == digits.length()) {
            res.add(path.toString());
            return;
        }
        int digit = digits.charAt(index) - '0';
        String letters = kvmaps[digit];
        for (int i = 0; i < letters.length(); i++) {
            path.append(letters.charAt(i));
            backtrack(digits, index + 1, path, res, kvmaps);
            path.deleteCharAt(path.length() - 1);
        }
    }
}
  1. 递归法
import java.util.ArrayList;
import java.util.List;

class Solution {
    private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) {
            return new ArrayList<>();
        }
        return combine(digits, 0);
    }

    private List<String> combine(String digits, int index) {
        if (index == digits.length()) {
            return new ArrayList<>();
        }
        List<String> result = new ArrayList<>();
        String letters = KEYS[digits.charAt(index) - '0'];
        List<String> subCombinations = combine(digits, index + 1);
        if (subCombinations.isEmpty()) {
            subCombinations.add("");
        }
        for (char letter : letters.toCharArray()) {
            for (String subCombination : subCombinations) {
                result.add(letter + subCombination);
            }
        }
        return result;
    }
}

C++实现

  1. 回溯法
#include <iostream>
#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    vector<string> letterCombinations(string digits) {
        vector<string> res;
        if (digits.empty()) {
            return res;
        }
        string kvmaps[] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        string path;
        backtrack(digits, 0, path, res, kvmaps);
        return res;
    }

private:
    void backtrack(const string& digits, int index, string& path, vector<string>& res, const string kvmaps[]) {
        if (index == digits.size()) {
            res.push_back(path);
            return;
        }
        int digit = digits[index] - '0';
        const string& letters = kvmaps[digit];
        for (char letter : letters) {
            path.push_back(letter);
            backtrack(digits, index + 1, path, res, kvmaps);
            path.pop_back();
        }
    }
};
  1. 循环法
#include <iostream>
#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    vector<string> letterCombinations(string digits) {
        if (digits.empty()) {
            return {};
        }
        vector<string> res = {""};
        string kvmaps[] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        for (char digit : digits) {
            int num = digit - '0';
            vector<string> temp;
            for (string str : res) {
                for (char c : kvmaps[num]) {
                    temp.push_back(str + c);
                }
            }
            res = temp;
        }
        return res;
    }
};

Go实现

  1. 回溯法
package main

import "fmt"

func letterCombinations(digits string) []string {
    if digits == "" {
        return []string{}
    }
    kvmaps := map[byte]string{'2': "abc", '3': "def", '4': "ghi", '5': "jkl", '6': "mno", '7': "pqrs", '8': "tuv", '9': "wxyz"}
    var res []string
    var path []byte
    var dfs func(int)
    dfs = func(index int) {
        if index == len(digits) {
            res = append(res, string(path))
            return
        }
        for _, c := range kvmaps[digits[index]] {
            path = append(path, byte(c))
            dfs(index + 1)
            path = path[:len(path) - 1]
        }
    }
    dfs(0)
    return res
}
  1. 循环法
package main

import "fmt"

func letterCombinations(digits string) []string {
    if digits == "" {
        return []string{}
    }
    kvmaps := map[byte]string{'2': "abc", '3': "def", '4': "ghi", '5': "jkl", '6': "mno", '7': "pqrs", '8': "tuv", '9': "wxyz"}
    res := []string{""}
    for _, digit := range digits {
        var temp []string
        for _, str := range res {
            for _, c := range kvmaps[byte(digit)] {
                temp = append(temp, str + string(c))
            }
        }
        res = temp
    }
    return res
}

四、算法复杂性分析

时间复杂度

  1. 回溯法和递归法:假设输入数字字符串的长度为 (n),每个数字平均对应 (m) 个字母(在本题中 (m) 最大为 4)。对于每个数字,都有 (m) 种选择,总共有 (n) 个数字,所以时间复杂度为 (O(m^n))。这是因为在递归或回溯过程中,每一层递归都有 (m) 个分支,一共有 (n) 层递归。
  2. 使用内置函数productproduct函数计算笛卡尔积的时间复杂度也是 (O(m^n)),因为它本质上也是在生成所有可能的组合,与回溯法的时间复杂度相同。
  3. 循环法:同样假设输入数字字符串长度为 (n),每个数字平均对应 (m) 个字母。外层循环执行 (n) 次,每次内层循环生成新的结果数组时,操作次数与当前结果数组的大小和当前数字对应的字母数有关。第一次循环时,结果数组大小为 1,第二次为 (m),第三次为 (m^2),以此类推。总的操作次数为 (m + m^2 + \cdots + m^n),这是一个等比数列求和,其时间复杂度也为 (O(m^n))。

空间复杂度

  1. 回溯法和递归法:空间复杂度主要由递归调用栈的深度和存储结果的空间决定。递归调用栈的深度为 (n),存储结果的空间在最坏情况下为 (O(m^n))(所有可能的组合都要存储),所以总的空间复杂度为 (O(n + m^n))。通常情况下,由于结果集占用空间较大,空间复杂度可近似看作 (O(m^n))。
  2. 使用内置函数product:空间复杂度主要为存储结果的空间,在最坏情况下为 (O(m^n)),所以空间复杂度为 (O(m^n))。
  3. 循环法:空间复杂度同样主要为存储结果的空间,在最坏情况下为 (O(m^n)),所以空间复杂度为 (O(m^n))。

五、实现的关键点和难度

关键点

  1. 数字到字母的映射:准确建立数字与字母的映射关系是解题的基础,无论是使用数组、哈希表还是其他数据结构来存储这种映射,都要确保其正确性。
  2. 递归或循环逻辑:对于回溯法和递归法,要清晰地理解递归的过程和边界条件,确保在处理完所有数字后能够正确生成组合并回溯。对于循环法,要正确处理每一步循环中结果数组的更新,确保所有可能的组合都被生成。
  3. 结果处理:在生成组合的过程中,要正确处理结果的存储和返回。注意题目中对于输入为空字符串时返回空列表的要求,避免出现边界情况的错误。

难度

  1. 递归理解:回溯法和递归法的核心是递归,对于不熟悉递归思想的人来说,理解递归的调用过程、状态保存和回溯机制可能有一定难度。特别是在处理复杂的嵌套递归时,需要清晰的逻辑思维来确保程序的正确性。
  2. 边界条件处理:正确处理输入为空字符串的情况是一个容易出错的点。在各种解法中都需要特别注意这个边界条件,确保返回结果符合题目要求。同时,在循环法中,要注意结果数组的初始化和更新,避免在边界情况下出现数组越界等错误。

六、扩展及难度加深题目

扩展题目1:支持更多特殊字符的映射

假设除了数字 2 - 9 对应字母外,还增加了一些特殊字符的映射,如 * 对应一组特殊符号,# 对应另一组字符等。要求实现一个函数,能够处理包含这些特殊字符的输入字符串,生成所有可能的组合。这需要扩展数字到字符的映射表,并相应调整生成组合的逻辑。

扩展题目2:带权重的字母组合

为每个数字对应的字母赋予权重,要求找出所有字母组合,并按照组合的权重之和进行排序返回。这需要在生成组合的过程中计算每个组合的权重,然后使用合适的排序算法对结果进行排序,增加了算法的复杂性。

难度加深题目1:动态更新映射关系的字母组合

假设数字到字母的映射关系会动态变化,例如在程序运行过程中,某些数字的映射会根据特定条件改变。要求实现一个函数,能够在映射关系动态更新的情况下,高效地生成字母组合。这需要设计一种数据结构来动态维护映射关系,并在映射关系改变时快速调整生成组合的算法。

难度加深题目2:多维映射的字母组合

假设输入不仅是一维的数字字符串,而是一个二维数组,每个元素可能是数字或其他表示映射关系的符号,并且不同维度之间的映射关系存在依赖。例如,第一维数字决定第二维数字的映射表。要求实现一个函数,能够处理这种复杂的多维映射关系,生成所有可能的字母组合。这需要深入理解多维数据结构的处理方式,并设计复杂的递归或循环逻辑来遍历和生成组合。

七、应用场合

  1. 密码生成与破解:在密码学中,可能需要生成所有可能的字符组合来进行暴力破解(当然,这在合法合规的安全测试场景下)。电话号码的字母组合问题类似于这种生成所有可能组合的场景,可以作为基础模型进行扩展和应用。
  2. 排列组合问题建模:在数学、计算机科学等领域,许多排列组合问题都可以抽象成类似电话号码字母组合的形式。通过解决此类问题,可以为解决更复杂的排列组合问题提供思路和方法。
  3. 搜索与匹配算法:在搜索引擎、文本匹配等应用中,有时需要根据用户输入的部分信息生成所有可能的完整信息组合,然后进行匹配。电话号码字母组合问题的解法可以作为这类搜索与匹配算法的一部分,用于生成可能的匹配项。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值