LeetCode算法日记 - Day 56: 全排列II、话号码的字母组合

目录

[1. 全排列II](#1. 全排列II)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#1.3 代码实现)

[2. 电话号码的字母组合](#2. 电话号码的字母组合)

[2.1 题目解析](#2.1 题目解析)

[2.2 解法](#2.2 解法)

[2.3 代码实现](#2.3 代码实现)


1. 全排列II

https://leetcode.cn/problems/permutations-ii/

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

复制代码
输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

复制代码
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

1.1 题目解析

题目本质

在长度 ≤ 8 的数组里,从左到右"选位填数",但元素可能重复;我们需要枚举所有长度为 n 的排列,同时去掉因相同元素换位造成的重复结果

常规解法

朴素回溯(DFS):每层从剩余元素中任选一个放入路径 path,直到放满加入答案。

问题分析

朴素回溯对含重复元素的数组会把"相同数字在同一层互换位置"的分支也枚举出来,产生大量重复结果与无用搜索;虽然可以用集合去重,但代价高(每层/全局去重会引入额外哈希开销,且实现繁琐)。

思路转折

要想不重不漏且高效 ,必须在搜索时剪枝,避免生成重复分支。关键做法:

**i)**先对 nums 排序,使相同数字相邻;

ii)同一层 的枚举里,不同层的相同元素会被标记为 true,而只有同层的才会被标记为 false 。如果当前数字 nums[i] 与前一个数字 nums[i-1] 相等,且前一个相同数字本层还没被使用(!used[i-1]),------这能保证"同层相同值只让排序后出现的第一个出场",杜绝同构分支。

1.2 解法

算法思想(简短总结)

• 排序:nums 从小到大,使相同元素相邻。

• 回溯:path 记录当前排列;used[i] 标记下标 i 是否已被使用。

• 同层去重剪枝:当 i>0 && nums[i]==nums[i-1] && !used[i-1] 时跳过当前 i,仅允许同层的"第一个相同值"被选。

• 递归结束:当 path.size()==n 时加入答案。

**i)**对 nums 调用 Arrays.sort(nums)。

**ii)**准备结构:List<List<Integer>> res、List<Integer> path、boolean[] used。

**iii)**定义 dfs(depth):

  • 若 depth==n:将 path 拷贝加入 res,返回。

  • 循环 i=0..n-1:

    • 若 used[i] 为真,跳过;

    • 若 i>0 && nums[i]==nums[i-1] && !used[i-1],剪枝跳过;

    • 否则选择:used[i]=true,path.add(nums[i]),递归到 depth+1;回溯撤销选择。

**iv)**返回 res。

易错点

  • 忘记先排序,导致"同层去重"条件失效。

  • 将剪枝条件误写成 used[i-1](应是 !used[i-1]),或把"同层"与"跨层"混淆。

  • 到达叶子未 return,虽然不致错,但会多跑无用循环。

  • 冗余分支:if(vis[i]) continue; 后不要再写 else continue;。

1.3 代码实现

java 复制代码
import java.util.*;

class Solution {
    private List<List<Integer>> res;
    private List<Integer> path;
    private boolean[] used;

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);                 // 1) 排序,使相同元素相邻
        int n = nums.length;
        res = new ArrayList<>();
        path = new ArrayList<>();
        used = new boolean[n];
        dfs(nums, 0);
        return res;
    }

    private void dfs(int[] nums, int depth) {
        if (depth == nums.length) {        // 2) 叶子:收集答案
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;         // 已用过,跳过

            // 3) 同层去重:相同元素只允许第一个进入本层分支
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;

            // 4) 选择
            used[i] = true;
            path.add(nums[i]);

            // 5) 递归下一层
            dfs(nums, depth + 1);

            // 6) 回溯
            path.remove(path.size() - 1);
            used[i] = false;
        }
    }
}

复杂度分析

  • 时间复杂度:最坏情况下为生成所有排列的复杂度 O(n · n!),剪枝能显著减少含重复元素时的无效分支。

  • 空间复杂度:O(n) 递归栈与标记数组,答案集额外空间按输出规模计。

2. 电话号码的字母组合

https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/

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

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 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'] 的一个数字。

2.1 题目解析

题目本质

把一串数字(2--9)按电话键映射成"按位选字母"的所有组合,本质是一个固定长度的笛卡尔积/按位回溯问题。

常规解法

逐位遍历,每一位从映射的字母集中任选一个,深度优先把路径补满后收集为一个字符串。

问题分析:

  • 若直接用多重循环,会随着输入长度变化而改变循环层数,不可扩展;且写死层数容易出错。

  • 用回溯可以自然处理任意位数(这里 ≤4),并且路径构建/撤销的代价低。

  • 复杂度上,假设每位平均有 b 个字母、长度为 n,则解的规模与搜索复杂度为 O(bⁿ),这里 b≈3~4,n≤4,规模很小。

思路转折:

要写得简洁、稳定、易维护

  • 用常量表 KEYS[0..9] 存映射;

  • 递归参数用"当前位置 pos";

  • 路径用可变字符串(StringBuilder/StringBuffer),到叶子一次性 toString() 收集;

  • 空输入直接返回空列表,避免额外分支。

2.2 解法

算法思想

• 排列模型:第 pos 位从 KEYS[digits[pos]-'0'] 中依次选一个字母。

• 递归推进:选中一个字母 → 递归到 pos+1;当 pos==n 时加入答案。

• 回溯撤销:从路径尾部删去刚加入的字母,返回上一层继续尝试其他字母。

i)digits 为空,返回空列表。

ii) 初始化常量映射表 KEYS

**iii)**准备结果列表 res 与路径 path(可变字符串)。

**iv)**调用 dfs(digits, 0)。

**v)**在 dfs 中:

  • 若 pos == digits.length():把 path.toString() 加入结果并返回。

  • 取本位数字 idx = digits.charAt(pos) - '0',得到字母串 letters = KEYS[idx]。

  • 遍历 letters:依次 append 当前字母 → 递归到下一位 → 回溯 delete 最后一个字母。

**vi)**返回结果列表。

易错点

  • 到达叶子时把同一个可变对象直接加入结果,导致后续修改污染结果;应 toString() 拷贝。

  • 使用 + 拼接字符串构建路径,会产生大量中间对象;应使用 StringBuilder/StringBuffer。

2.3 代码实现

java 复制代码
import java.util.*;

class Solution {
    // 电话键映射
    private static final String[] KEYS = {
        "",     // 0
        "",     // 1
        "abc",  // 2
        "def",  // 3
        "ghi",  // 4
        "jkl",  // 5
        "mno",  // 6
        "pqrs", // 7
        "tuv",  // 8
        "wxyz"  // 9
    };

    private List<String> res = new ArrayList<>();
    private StringBuilder path = new StringBuilder();

    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) return res; // 空输入
        dfs(digits, 0);
        return res;
    }

    private void dfs(String digits, int pos) {
        if (pos == digits.length()) {          // 叶子:收集结果
            res.add(path.toString());
            return;
        }
        int idx = digits.charAt(pos) - '0';    // 当前位对应的按键
        String letters = KEYS[idx];

        for (int i = 0; i < letters.length(); i++) {
            path.append(letters.charAt(i));    // 选择一个字母
            dfs(digits, pos + 1);              // 递归下一位
            path.deleteCharAt(path.length() - 1); // 回溯撤销
        }
    }
}

复杂度分析(时间+空间)

  • 时间复杂度 :令输入长度为 n、每位分支数最大为 b(b≤4),则 O(bⁿ);本题 n≤4,规模很小。

  • 空间复杂度:递归深度 O(n),路径临时空间 O(n),结果集按输出规模计(最多 3⁴=81 或 4⁴=256 级别)。

相关推荐
未知陨落2 小时前
LeetCode:74.数组中的第K个最大元素
算法·leetcode
电子_咸鱼2 小时前
LeetCode-hot100——验证二叉搜索树
开发语言·数据结构·c++·算法·leetcode·深度优先
潼心1412o2 小时前
数据结构(长期更新)第1讲:算法复杂度
数据结构
Imxyk2 小时前
Codeforces Round 1052 (Div. 2) C. Wrong Binary Searchong Binary Search
c语言·c++·算法
Yunfeng Peng2 小时前
1- 十大排序算法(选择排序、冒泡排序、插入排序)
java·算法·排序算法
多看书少吃饭2 小时前
前端实现抽烟识别:从算法到可视化
前端·算法
MMjeaty3 小时前
特殊矩阵的压缩存储
算法·矩阵
断剑zou天涯3 小时前
【算法笔记】二叉树递归解题套路及其应用
java·笔记·算法
微笑尅乐10 小时前
力扣350.两个数组的交集II
java·算法·leetcode·动态规划