【每日算法】LeetCode 17. 电话号码的字母组合

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 17. 电话号码的字母组合

1. 题目描述

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。

数字到字母的映射如下(与电话按键相同):

复制代码
2: "abc"
3: "def"
4: "ghi"
5: "jkl"
6: "mno"
7: "pqrs"
8: "tuv"
9: "wxyz"

注意:10 不包含任何字母。

示例 1:

复制代码
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

复制代码
输入:digits = ""
输出:[]

示例 3:

复制代码
输入:digits = "2"
输出:["a","b","c"]

2. 问题分析

2.1 问题本质

这是一个典型的组合问题,需要找出所有可能的字母排列组合。每个数字对应3-4个字母,我们需要从每个数字对应的字母集合中选取一个字母,然后将所有选出的字母按顺序组合起来。

2.2 关键特征

  1. 组合而非排列:顺序是固定的(按输入数字顺序),我们只需要在每个位置选择合适的字母
  2. 多重循环的变体:相当于多个for循环的嵌套,但循环层数由输入字符串长度决定
  3. 树形结构:可以看作一个树形遍历问题,每个节点代表一个数字,分支代表该数字对应的字母

2.3 前端视角

在前端开发中,类似的问题场景包括:

  • 动态表单生成(根据用户选择动态生成下一级选项)
  • 路由权限组合(不同权限组合对应不同的可访问路由)
  • 商品属性组合(如颜色、尺寸的不同组合)

3. 解题思路

3.1 核心思想:回溯算法

回溯算法是解决组合问题的经典方法,特别适合解决"所有可能"的问题。它通过探索所有可能的路径,并在遇到不可能的情况时回退到上一步。

3.2 算法步骤

  1. 建立数字到字母的映射表
  2. 如果输入字符串为空,直接返回空数组
  3. 初始化结果数组和当前路径
  4. 使用深度优先搜索(DFS)回溯:
    • 终止条件:当前路径长度等于输入数字字符串长度
    • 递归过程:获取当前数字对应的字母,遍历每个字母,将其加入路径,递归处理下一个数字,然后回溯(移除路径最后一个字母)

3.3 复杂度分析

  • 时间复杂度:O(3^m × 4^n),其中 m 是输入中对应3个字母的数字个数,n 是输入中对应4个字母的数字个数
  • 空间复杂度:O(m+n),递归调用栈的深度最大为输入数字的长度

3.4 最优解

回溯算法是这个问题的最优解,因为:

  1. 必须遍历所有可能的组合才能得到完整答案
  2. 避免了不必要的重复计算
  3. 空间复杂度相对较低,只存储当前路径和结果

4. 各思路代码实现

4.1 回溯算法(递归实现)

javascript 复制代码
/**
 * 方法一:经典回溯算法(递归)
 * @param {string} digits
 * @return {string[]}
 */
const letterCombinations = function(digits) {
    if (!digits || digits.length === 0) return [];
    
    // 数字到字母的映射
    const digitMap = {
        '2': 'abc',
        '3': 'def',
        '4': 'ghi',
        '5': 'jkl',
        '6': 'mno',
        '7': 'pqrs',
        '8': 'tuv',
        '9': 'wxyz'
    };
    
    const result = [];
    
    /**
     * 回溯函数
     * @param {number} index - 当前处理的数字索引
     * @param {string} current - 当前组合字符串
     */
    const backtrack = (index, current) => {
        // 终止条件:当前组合长度等于输入数字长度
        if (index === digits.length) {
            result.push(current);
            return;
        }
        
        // 获取当前数字对应的字母
        const digit = digits[index];
        const letters = digitMap[digit];
        
        // 遍历当前数字对应的所有字母
        for (let i = 0; i < letters.length; i++) {
            // 做出选择:添加当前字母到组合中
            backtrack(index + 1, current + letters[i]);
            // 回溯:在递归返回后,current不变,无需显式移除字符
        }
    };
    
    backtrack(0, '');
    return result;
};

4.2 迭代法(队列实现)

javascript 复制代码
/**
 * 方法二:迭代法(使用队列)
 * @param {string} digits
 * @return {string[]}
 */
const letterCombinationsQueue = function(digits) {
    if (!digits || digits.length === 0) return [];
    
    const digitMap = {
        '2': 'abc',
        '3': 'def',
        '4': 'ghi',
        '5': 'jkl',
        '6': 'mno',
        '7': 'pqrs',
        '8': 'tuv',
        '9': 'wxyz'
    };
    
    // 使用队列存储中间结果
    let queue = [''];
    
    for (let i = 0; i < digits.length; i++) {
        const digit = digits[i];
        const letters = digitMap[digit];
        const queueLength = queue.length;
        
        // 处理队列中现有的所有组合
        for (let j = 0; j < queueLength; j++) {
            const current = queue.shift(); // 取出队首元素
            
            // 为当前组合添加新的字母
            for (let k = 0; k < letters.length; k++) {
                queue.push(current + letters[k]);
            }
        }
    }
    
    return queue;
};

4.3 函数式编程实现

javascript 复制代码
/**
 * 方法三:函数式编程实现
 * @param {string} digits
 * @return {string[]}
 */
const letterCombinationsFP = function(digits) {
    if (!digits || digits.length === 0) return [];
    
    const digitMap = {
        '2': 'abc',
        '3': 'def',
        '4': 'ghi',
        '5': 'jkl',
        '6': 'mno',
        '7': 'pqrs',
        '8': 'tuv',
        '9': 'wxyz'
    };
    
    // 使用reduce逐步构建所有组合
    return digits.split('').reduce((acc, digit) => {
        const letters = digitMap[digit];
        if (acc.length === 0) {
            return letters.split('');
        }
        // 将现有组合与新的字母进行笛卡尔积
        return acc.flatMap(comb => 
            letters.split('').map(letter => comb + letter)
        );
    }, []);
};

5. 各实现思路的复杂度、优缺点对比

实现方法 时间复杂度 空间复杂度 优点 缺点 适用场景
回溯算法(递归) O(3^m × 4^n) O(m+n) 1. 思路清晰直观 2. 代码简洁 3. 标准解法,易于理解 1. 递归深度受栈大小限制 2. 字符串拼接产生新字符串 大多数场景,教学示例,中等长度输入
迭代法(队列) O(3^m × 4^n) O(3^m × 4^n) 1. 避免递归栈溢出 2. 适合大长度输入 3. 容易理解 1. 空间占用较大 2. 代码稍复杂 输入较长,担心递归深度限制
函数式编程 O(3^m × 4^n) O(3^m × 4^n) 1. 代码简洁优雅 2. 无副作用 3. 易于测试 1. 性能稍差 2. 可能产生中间数组 3. 理解需要函数式基础 函数式代码库,小规模输入,代码简洁性优先

6. 总结

6.1 算法核心要点

  1. 回溯算法是解决组合问题的利器,特别适合需要探索所有可能解的场景
  2. 递归与迭代可以相互转换,根据具体情况选择合适的方法
  3. 剪枝优化:虽然本题无法剪枝(需要所有组合),但在其他问题中可以通过条件判断提前终止无效分支

6.2 前端实际应用场景

场景一:动态表单生成器
javascript 复制代码
// 根据用户选择的选项动态生成下一级选项
// 例如:选择省份 -> 选择城市 -> 选择区域
function generateFormCombinations(selections) {
    // 类似电话号码组合,每个选择点对应多个选项
    // 生成所有可能的表单路径
}
场景二:路由权限组合
javascript 复制代码
// 用户可能有多种角色,每种角色对应不同的权限
// 计算用户可以访问的所有路由组合
function getAllowedRoutes(userRoles) {
    // 每个角色对应一组可访问路由
    // 组合所有角色的路由权限
}
场景三:商品SKU组合
javascript 复制代码
// 电商平台中,商品可能有多个属性(颜色、尺寸等)
// 生成所有可能的SKU组合
function generateProductSKUs(attributes) {
    // 例如:颜色:红、蓝;尺寸:S、M、L
    // 生成:红-S、红-M、红-L、蓝-S、蓝-M、蓝-L
}
场景四:国际化文案组合
javascript 复制代码
// 多语言网站中,根据语言和地区组合生成完整的文案路径
function getI18nPaths(languages, regions) {
    // 例如:语言:en, zh;地区:US, CN
    // 生成:en-US, en-CN, zh-US, zh-CN
}
相关推荐
Jerry12 小时前
LeetCode 459. 重复的子字符串
算法
成都渲染101云渲染666612 小时前
如何在3ds Max中实现更快、更高质量的渲染
前端·javascript·人工智能
闲猫14 小时前
Spring AI 对接Deepseek ChatModel 聊天对话
java·前端·spring
海石14 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石14 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
自信的未来16 小时前
JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
java·前端·json
我叫黑大帅16 小时前
MySQL 并发插入竞态问题:原子写入实践指南
后端·mysql·面试
全栈前端老曹16 小时前
【MongoDB】安全与权限管理 —— 用户认证、角色权限、SSL 加密
前端·javascript·数据库·安全·mongodb·nosql·ssl
Jerry16 小时前
LeetCode 151. 反转字符串中的单词
算法
zhangxingchao17 小时前
AI大模型核心八:从 Agent Skill、长文档 RAG 到知识库更新与训练策略
前端·人工智能·后端