【LeetCode】46. 全排列

46. 全排列

题目

【LeetCode】46. 全排列

给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。你可以按任意顺序顺序返回答案。

示例 1:

输入:nums = 1,2,3

输出:\[1,2,3,1,3,2,2,1,3,2,3,1,3,1,2,3,2,1]

示例 2:

输入:nums = 0,1

输出:\[0,1,1,0]

示例 3:

输入:nums = 1

输出:\[1]

提示:

  • 1 <= nums.length <= 6
  • -10 <= numsi <= 10
  • nums 中的所有整数 互不相同

解法一:回溯法(使用标记数组)

解题思路

这种方法通过递归构建排列,使用标记数组记录元素是否已被使用,核心步骤包括:

  1. 从空序列开始,尝试添加未使用的元素
  2. 当序列长度等于数组长度时,记录当前排列
  3. 回溯:移除最后添加的元素,标记为未使用,尝试其他元素
java 复制代码
import java.util.*;

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> current = new ArrayList<>();
        boolean[] used = new boolean[nums.length]; // 标记元素是否已使用
        
        backtrack(nums, used, current, result);
        return result;
    }
    
    private void backtrack(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> result) {
        // 终止条件:当前排列长度等于数组长度
        if (current.size() == nums.length) {
            result.add(new ArrayList<>(current)); // 添加副本到结果集
            return;
        }
        
        // 遍历所有元素,尝试使用未被使用的元素
        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                // 选择当前元素
                used[i] = true;
                current.add(nums[i]);
                
                // 递归处理剩余元素
                backtrack(nums, used, current, result);
                
                // 回溯:撤销选择
                current.remove(current.size() - 1);
                used[i] = false;
            }
        }
    }
}

算法解析

nums = [1,2,3] 为例:

  1. 初始状态:current = []used = [false, false, false]
  2. 第一层递归:尝试添加1、2、3中的一个,标记为已使用
  3. 第二层递归:从剩余元素中选择一个添加
  4. 第三层递归:添加最后一个剩余元素,此时序列长度为3,添加到结果
  5. 回溯过程:逐层移除最后添加的元素,标记为未使用,尝试其他可能性

该方法的时间复杂度为 O(n×n!),需要遍历n!个排列,每个排列需要O(n)时间复制;空间复杂度为 O(n),主要是递归栈和标记数组的开销。

解法二:邻位交换法(无标记数组)

解题思路

这种方法通过交换元素位置来生成排列,无需标记数组,核心步骤包括:

  1. 固定当前位置的元素(通过与后续元素交换)
  2. 递归处理剩余位置的元素
  3. 回溯:交换回元素,恢复数组原状,尝试其他交换组合
java 复制代码
import java.util.*;

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        for (int num : nums) {
            list.add(num);
        }
        
        backtrackSwap(list, 0, result);
        return result;
    }
    
    private void backtrackSwap(List<Integer> list, int index, List<List<Integer>> result) {
        // 终止条件:当前索引达到列表末尾
        if (index == list.size()) {
            result.add(new ArrayList<>(list));
            return;
        }
        
        // 从当前索引开始,与后续每个元素交换
        for (int i = index; i < list.size(); i++) {
            // 交换当前索引与i位置的元素
            Collections.swap(list, index, i);
            
            // 递归处理下一个位置
            backtrackSwap(list, index + 1, result);
            
            // 回溯:交换回来恢复原状
            Collections.swap(list, index, i);
        }
    }
}

算法解析

nums = [1,2,3] 为例:

  1. 初始状态:list = [1,2,3]index = 0
  2. 第一层递归:将index=0分别与i=0、1、2交换,得到1,2,32,1,33,2,1
  3. 第二层递归:对每种情况,处理index=1的位置,继续交换
  4. 第三层递归:处理index=2的位置,完成排列并添加到结果
  5. 回溯过程:逐层交换回元素,恢复数组状态,尝试其他交换组合

该方法的时间复杂度同样为 O(n×n!),空间复杂度为 O(n),但省去了标记数组的空间,实际空间效率更高。

两种方法对比

特性 标准回溯法(标记数组) 邻位交换法(无标记)
核心操作 标记数组+添加/删除元素 元素交换
空间效率 稍低(需要标记数组) 更高(无标记数组)
直观性 更容易理解 稍难理解(依赖交换逻辑)
适用场景 所有可迭代序列 主要适用于数组/列表

两种方法本质上都是深度优先搜索(DFS),通过递归探索所有可能的排列组合,是解决全排列问题的经典方法。

相关推荐
IT_陈寒3 小时前
用了Proxy才发现以前的JavaScript白写了
前端·人工智能·后端
唐青枫4 小时前
别把 ArrayList 当成会自动管理内存的 List:Zig 动态数组从入门到实战
后端
鸿蒙开发4 小时前
我为 HarmonyOS 做了一个统一大模型 SDK:@hmkit/ai 正式开源
后端
猪是念来过倒4 小时前
Semaphore 与 RateLimiter:并发控制双雄详解
后端
掘金者阿豪4 小时前
异构数据同步最怕什么?不是同步慢,而是数据对不上
后端
古法安卓5 小时前
Android-日志系统源码解析
android·java·android studio
地平线开发者5 小时前
【模型轻量化专题】衡量模型轻量性的指标
算法
程序员cxuan5 小时前
Claude 官方的学习教程,太强了。
人工智能·后端·程序员
老孙讲技术5 小时前
把工地遮挡和掉线接进项目部:setMessageCallback 订 alarm 与 deviceStatus
后端·物联网·音视频开发
老孙讲技术5 小时前
关店后有人进门,监控值班群却没响:用 setMessageCallback 订 human,再用 aiHuman 打开人形
后端·物联网·音视频开发