电话号码的字母组合
- https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
描述
-
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
-
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
1 2(abc) 3(def) 4(ghi) 5(jkl) 6(mno) 7(pqrs) 8(tuv) 9(wxyz) * 0 #
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
提示
- 0 <= digits.length <= 4
- digits[i] 是范围 [‘2’, ‘9’] 的一个数字
Typescript 版算法实现
1 )方案 1: 深度优先遍历
function letterCombinations(digits: string): string[] {
// 对输入做处理,如果小于1返回空
if (digits.length < 1) return []
// 建立电话号码键盘字典映射
const dict: string[] = ['', '', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
// 如果只给了一个按键,直接把按键内容取出来并按单个字符分组
if (digits.length < 2) return dict[digits].split('')
// 把输入字符串按单字符分隔变成数组,234 => [2,3,4]
const num: string[] = digits.split('')
// 保存键盘映射后的字母内容,如 23 => ['abc','def']
const code: string[] = []
num.forEach(item => {
if (!dict[item]) return
code.push(dict[item])
})
// 这里arr可以是数组和字符串的集合
const comb = (arr: any[]) => {
// 临时变量用来保存前两个组合的结果
const tmp: string[] = []
// 最外层的循环是遍历第一个元素,里层的循环是遍历第二个元素
for (let i = 0, ilen = arr[0].length; i < ilen; i++) {
for (let j = 0, jlen = arr[1].length; j < jlen; j++) {
tmp.push(`${arr[0][i]}${arr[1][j]}`)
}
}
// 从数组中索引为0开始删除,一共删除2个元素,将arr数组的前两项用 tmp 数组替代
arr.splice(0, 2, tmp)
if (arr.length > 1) {
comb(arr)
// 这是递归后形成的最后一个结果数组进行返回,这个数组只有一个元素
return arr[0]
}
return tmp
}
return comb(code)
};
- 按照题目要求,可以任意输入符合要求的数值型字符串来获取最终的组合结果
- 我们并不知道用户会输入多少字符,比如:2,3;2,3,4;2,3,4,5;2,3,4,5,6,…
- 所以并不适合使用n层循环来暴力枚举,需要的是两两组合
- 两两组合完事后,将前两个替换成最新的一个来与后面的一个再次进行两两组合
- 直到最终只有一个数组的时候结束,这里用的递归的思路来处理的
- 同时,我们字符串和数组都可以按下标来进行访问,这一点要注意
- 这里一个重要的数组api是 splice,即这行:
arr.splice(0, 2, tmp) - 这里arr是个any[]类型的数组,里面的数据结构是字符串和数组混合结构的数组
- 这里考察的是组合关系
- 这个dict字典的设计是比较合适的,前两个都是
'', 为了和数字与下标的转换进行匹配 - 但是重新审视这个算法,两个for循环嵌套,还是递归,这里时间复杂度较高
- 时间复杂度为 O(N + N + N + log(N) * K * L^2)
- 其中 N 为输入字符串长度,K 为有效按键数量,L 为按键平均长度
- 空间复杂度为 O(N + N + K * L + log(N) * K * L)
- 构建了 num 数组和 code 数组,它们各自的空间复杂度为 O(N)
- 递归调用时,使用了 tmp 临时数组,其空间复杂度为 O(K * L),其中 K 为数组长度,L 为数组平均长度
- 递归调用的层数为 log(N),每层都有一个 tmp 数组,因此总体递归的空间复杂度为 O(log(N) * K * L)
2)方案2: 回溯
// 定义 letterCombinations 方法,接收一个字符串参数 digits,返回一个字符串数组
function letterCombinations(digits: string): string[] {
// 如果 digits 为空字符串,直接返回空数组
if (!digits) return [];
// 定义电话号码对应的字母组合映射
const phoneMap: { [key: string]: string } = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
};
// 定义回溯函数,接收一个索引参数
const backtrack = (index: number): void => {
// 如果索引等于 digits 的长度,将当前组合加入结果数组
if (index === digits.length) {
combinations.push(combination.join(''));
} else {
// 获取当前数字对应的字母组合
const digit = digits[index];
// 遍历当前数字对应的字母组合
for (const letter of phoneMap[digit]) {
// 将当前字母加入组合
combination.push(letter);
// 递归调用回溯函数,增加索引
backtrack(index + 1);
// 回溯,将最后加入的字母移除,尝试其他字母
combination.pop();
}
}
};
// 初始化组合和结果数组
const combination: string[] = [];
const combinations: string[] = [];
// 调用回溯函数,从索引 0 开始
backtrack(0);
// 返回最终的结果数组
return combinations;
}
- 使用哈希表存储每个数字对应的所有可能的字母,然后进行回溯操作
- 回溯过程中维护一个字符串,表示已有的字母排列
- 如果未遍历完电话号码的所有数字,则已有的字母排列是不完整的
- 该字符串初始为空。每次取电话号码的一位数字
- 从哈希表中获得该数字对应的所有可能的字母,并将其中的一个字母插入到已有的字母排列后面
- 然后继续处理电话号码的后一位数字,直到处理完电话号码中的所有数字
- 即得到一个完整的字母排列。然后进行回退操作,遍历其余的字母排列
- 回溯算法用于寻找所有的可行解,如果发现一个解不可行,则会舍弃不可行的解
- 在这道题中,由于每个数字对应的每个字母都可能进入字母组合,因此不存在不可行的解,直接穷举所有的解即可
- 时间复杂度:O(3m×4n3^m × 4^n3m×4n)
- 空间复杂度: O(m + n) 线性
- m 是对应三个字母的数字个数,n 是对应四个字母的数字个数
3 )方案 3: 广度优先遍历
// 定义 letterCombinations 方法,接收一个字符串参数 digits,返回一个字符串数组
function letterCombinations(digits: string): string[] {
// 如果 digits 为空字符串,直接返回空数组
if (!digits) return [];
// 定义电话号码对应的字母组合数组
const phone: string[] = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'];
// 初始化队列,将空字符串作为初始元素
let queue: string[] = [''];
// 遍历输入的数字字符串
for (const digit of digits) {
// 获取当前队列的长度
const queueLength = queue.length;
// 遍历当前队列中的元素
for (let i = 0; i < queueLength; i++) {
// 从队列中取出一个元素
const tmp = queue.shift()!;
// 遍历当前数字对应的字母组合, 这里字符串 2 的 Unicode 编码是 50, 减去后对应上述数组的索引
for (const letter of phone[digit.charCodeAt(0) - 50]) {
// 将当前元素和字母组合的每个字母拼接,然后加入队列
queue.push(tmp + letter);
}
}
}
// 返回最终的结果数组
return queue;
}
- 使用队列,先将输入的 digits 中第一个数字对应的每一个字母入队
- 然后将出队的元素与第二个数字对应的每一个字母组合后入队…直到遍历到 digits 的结尾
- 最后队列中的元素就是所求结果
- 总结下来就是:利用队列,从当前出发,逐步构造解
- 时间复杂度:O(3m×4n3^m × 4^n3m×4n)
- 空间复杂度:O(3m×4n3^m × 4^n3m×4n)
- m 是对应三个字母的数字个数,n 是对应四个字母的数字个数
Python3 版算法实现
1 )方案1: 深度优先
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
# 对输入做处理,如果小于1返回空
if len(digits) < 1:
return []
# 建立电话号码键盘字典映射
dict = ['', '', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
# 如果只给了一个按键,直接把按键内容取出来并按单个字符分组
if len(digits) < 2:
return list(dict[int(digits)])
# 把输入字符串按单字符分隔变成数组,234 => [2,3,4]
num = list(digits)
# 保存键盘映射后的字母内容,如 23 => ['abc','def']
code = []
for item in num:
if not dict[int(item)]:
continue
code.append(dict[int(item)])
# 这里arr可以是数组和字符串的集合
def comb(arr):
# 临时变量用来保存前两个组合的结果
tmp = []
# 最外层的循环是遍历第一个元素,里层的循环是遍历第二个元素
for i in range(len(arr[0])):
for j in range(len(arr[1])):
tmp.append(arr[0][i] + arr[1][j])
# 从数组中索引为0开始删除,一共删除2个元素,将arr数组的前两项用 tmp 数组替代
arr.pop(0)
arr.pop(0)
arr.insert(0, tmp)
if len(arr) > 1:
comb(arr)
# 这是递归后形成的最后一个结果数组进行返回,这个数组只有一个元素
return arr[0]
return tmp
return comb(code)
2 )方案2: 回溯
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
# 如果 digits 为空字符串,直接返回空列表
if not digits:
return []
# 定义电话号码对应的字母组合映射
phone_map = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
# 定义回溯函数
def backtrack(index, combination):
# 如果索引等于 digits 的长度,将当前组合加入结果列表
if index == len(digits):
combinations.append("".join(combination))
else:
# 获取当前数字对应的字母组合
digit = digits[index]
# 遍历当前数字对应的字母组合
for letter in phone_map[digit]:
# 将当前字母加入组合
combination.append(letter)
# 递归调用回溯函数,增加索引
backtrack(index + 1, combination)
# 回溯,将最后加入的字母移除,尝试其他字母
combination.pop()
# 初始化组合和结果列表
combination = []
combinations = []
# 调用回溯函数,从索引 0 开始
backtrack(0, combination)
# 返回最终的结果列表
return combinations
3 )方案3: 广度优先
from collections import deque
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
# 如果 digits 为空字符串,直接返回空列表
if not digits:
return []
# 定义电话号码对应的字母组合数组
phone = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
# 初始化队列,将空字符串作为初始元素
queue = deque([''])
# 遍历输入的数字字符串
for digit in digits:
# 获取当前队列的长度
queue_length = len(queue)
# 遍历当前队列中的元素
for _ in range(queue_length):
# 从队列中取出一个元素
tmp = queue.popleft()
# 遍历当前数字对应的字母组合
for letter in phone[ord(digit) - 50]:
# 将当前元素和字母组合的每个字母拼接,然后加入队列
queue.append(tmp + letter)
# 返回最终的结果列表
return list(queue)
Golang 版算法实现
1 )方案1: 深度优先
import (
"strings"
)
func letterCombinations(digits string) []string {
// 对输入做处理,如果小于1返回空
if len(digits) < 1 {
return []string{}
}
// 建立电话号码键盘字典映射
dict := []string{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}
var result []string // 最终结果集
// 如果只给了一个按键,直接把按键内容取出来并按单个字符分组
if len(digits) < 2 {
return strings.Split(dict[digits[0]-'0'], "")
}
// 把输入字符串按单字符分隔变成数组,234 => [2,3,4]
num := strings.Split(digits, "")
// 保存键盘映射后的字母内容,如 23 => ['abc','def']
code := [][]string{}
for _, item := range num {
if len(dict[item[0]-'0']) == 0 {
continue
}
code = append(code, []string{dict[item[0]-'0']})
}
// 递归函数,用于生成字母组合
var comb func(arr [][]string)
comb = func(arr [][]string) {
// 检查数组长度是否大于等于2
if len(arr) < 2 {
result = arr[0]
return
}
// 临时变量用于保存前两个组合的结果
tmp := []string{}
// 遍历第一个元素的每个字母与第二个元素的每个字母的组合
firstLen := len(arr[0])
for _, item := range arr[0] {
if firstLen == 1 {
for _, char1 := range item {
for _, char2 := range arr[1][0] {
tmp = append(tmp, string(char1)+string(char2))
}
}
} else {
for _, char2 := range arr[1][0] {
tmp = append(tmp, item+string(char2))
}
}
}
// 替换切片的前两个元素
arr = append([][]string{tmp}, arr[2:]...)
// 继续递归处理剩余的元素组合
if len(arr[0]) > 0 {
comb(arr)
}
}
// 调用递归函数生成结果
comb(code)
return result
}
2 )方案2: 回溯
import "strings"
// 定义 letterCombinations 函数,接收一个字符串参数 digits,返回一个字符串数组
func letterCombinations(digits string) []string {
// 如果 digits 为空字符串,直接返回空数组
if len(digits) == 0 {
return []string{}
}
// 定义电话号码对应的字母组合映射
phoneMap := map[string]string{
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
// 初始化结果数组
var combinations []string
// 定义回溯函数
var backtrack func(index int, combination string)
backtrack = func(index int, combination string) {
// 如果索引等于 digits 的长度,将当前组合加入结果数组
if index == len(digits) {
combinations = append(combinations, combination)
return
}
// 获取当前数字对应的字母组合
digit := string(digits[index])
letters := phoneMap[digit]
// 遍历当前数字对应的字母组合
for _, letter := range letters {
// 将当前字母加入组合
comb := combination + string(letter)
// 递归调用回溯函数,增加索引
backtrack(index+1, comb)
}
}
// 调用回溯函数,从索引 0 开始
backtrack(0, "")
// 返回最终的结果数组
return combinations
}
3 )方案3: 广度优先
func letterCombinations(digits string) []string {
// 如果 digits 为空字符串,直接返回空数组
if digits == "" {
return []string{}
}
// 定义电话号码对应的字母组合数组
phone := []string{"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}
// 初始化队列,将空字符串作为初始元素
queue := []string{""}
// 遍历输入的数字字符串
for _, digit := range digits {
// 获取当前队列的长度
queueLength := len(queue)
// 创建临时队列用于存储新的组合
tmpQueue := make([]string, 0)
// 遍历当前队列中的元素
for i := 0; i < queueLength; i++ {
// 从队列中取出一个元素
tmp := queue[i]
// 遍历当前数字对应的字母组合
for _, letter := range phone[digit-'2'] {
// 将当前元素和字母组合的每个字母拼接,然后加入临时队列
tmpQueue = append(tmpQueue, tmp+string(letter))
}
}
// 更新队列为新的组合
queue = tmpQueue
}
// 返回最终的结果数组
return queue
}
2383



被折叠的 条评论
为什么被折叠?



