【Day48】46. 全排列

文章目录

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 <= nums[i] <= 10

nums 中的所有整数 互不相同


思路:

给定一个不含重复数字的数组,我们需要求出它的所有全排列

全排列问题本质上是枚举所有可能的排列组合 ,并且每个元素只能使用一次,非常适合用回溯算法解决。

回溯算法的核心逻辑可以概括为:
一步步做选择 → 递归往下走 → 走到底后回退一步 → 换一个选择再试


可以把构造全排列的过程,想象成给 n 个位置依次填数字的游戏:

  1. 从第一个位置开始,依次尝试所有还没有用过的数字
  2. 选中一个数字后,将它放入当前路径,并标记为已使用,避免重复选择
  3. 递归进入下一个位置,继续选择数字
  4. 当所有位置都填满时,说明得到了一个完整排列,将其保存到结果集中
  5. 回退(回溯):撤销上一步的选择,取消标记,尝试其他可能的数字

这就是回溯的灵魂:
选择 → 递归 → 撤销选择


需要维护的三个关键变量:

  1. result:最终结果集合,用于存储所有完整的全排列
  2. path:当前路径,保存正在构造中的排列
  3. used:布尔标记数组,记录哪些数字已经被使用,防止重复选取

本质:

在递归的每一层 中,选择一个尚未使用的数字,加入当前路径,不断向下构造,直到形成完整排列。


例如 nums = [1,2,3]

整个回溯过程可以用一棵递归树直观表示:

复制代码
[]
├── [1]
│   ├── [1,2]
│   │   └── [1,2,3]
│   └── [1,3]
│       └── [1,3,2]
├── [2]
│   ├── [2,1]
│   │   └── [2,1,3]
│   └── [2,3]
│       └── [2,3,1]
└── [3]
    ├── [3,1]
    │   └── [3,1,2]
    └── [3,2]
        └── [3,2,1]

每一条从根节点到叶子节点的路径,都是一个合法的全排列


代码实现(Go):

go 复制代码
package main

import "fmt"

// permute 函数:返回nums的全排列
// nums: 输入的不含重复数字的数组
// 返回值: 存储所有全排列的二维切片
func permute(nums []int) [][]int {
	// 1. 定义最终结果集合,用于存储所有完整的排列
	var result [][]int

	// 2. 定义当前路径path,保存正在拼接的排列
	var path []int

	// 3. 定义used数组,标记nums中对应下标的数字是否被使用过
	// used[i] = true 表示nums[i]已经在path中,不能重复选择
	used := make([]bool, len(nums))

	// 4. 定义回溯函数(闭包),无参数无返回值
	// 在 Go 中递归闭包需要先用 var 声明函数变量,再进行赋值。因为函数体内部要调用自身时,必须保证这个变量在作用域中已经存在。如果直接使用 := 定义匿名函数,函数体执行时变量还未完成绑定,会导致编译错误。
	var backtrack func()

	backtrack = func() {
		// 递归终止条件:当前路径长度等于nums长度,说明已经形成一个完整排列
		if len(path) == len(nums) {
			// Go 中的切片是引用类型,path 在回溯过程中会不断被修改(追加、回撤)
			// 如果不复制直接将 path 存入结果集,结果集中保存的只是指向 path 底层数组的引用
			// 后续回溯修改 path 时,已经存入结果集的数据会同步被覆盖,导致最终输出错误
			// 因此必须通过 make+copy 创建一个新切片,复制当前 path 的内容再保存,保证结果不会被修改
			temp := make([]int, len(path))
			copy(temp, path)
			// 将复制后的排列加入结果集
			result = append(result, temp)
			return
		}

		// 遍历所有数字,尝试每一个可选的数字
		for i := 0; i < len(nums); i++ {
			// 如果当前数字已经被使用,跳过
			if used[i] {
				continue
			}

			// ------------------- 做选择 -------------------
			// 标记该数字已使用
			used[i] = true
			// 将数字加入当前路径
			path = append(path, nums[i])

			// 递归:继续选择下一个数字,构造排列的下一位
			backtrack()

			// ------------------- 撤销选择(回溯) -------------------
			// 从路径中移除最后一个数字
			path = path[:len(path)-1]
			// 取消标记,该数字可以被后续选择使用
			used[i] = false
		}
	}

	// 启动回溯
	backtrack()

	// 返回所有全排列结果
	return result
}

// main函数:程序入口,用于测试输入输出
func main() {
	nums := []int{1, 2, 3}
	res := permute(nums)

	fmt.Println(res) // [[1 2 3] [1 3 2] [2 1 3] [2 3 1] [3 1 2] [3 2 1]]
}

时间复杂度: O ( n × n ! ) O(n \times n!) O(n×n!)

  • 一共 n ! n! n! 种排列
  • 每种排列需要构造长度为 n n n 的路径
  • 所以总复杂度为 O ( n × n ! ) O(n \times n!) O(n×n!)

空间复杂度: O ( n ) O(n) O(n)

  • 递归调用栈深度:最多 n n n 层(数组长度)
  • pathused 数组空间都是 O ( n ) O(n) O(n)
  • 不计入结果存储的额外空间,复杂度为 O ( n ) O(n) O(n)

回溯的核心

go 复制代码
used[i] = true     // 选这个数
path = append(...) // 放进path
backtrack()        // 继续选下一个数
path = pop()       // 把数退回来
used[i] = false    // 取消选中

这就是标准回溯模板,所有排列组合问题都通用


相关推荐
khalil10201 小时前
代码随想录算法训练营Day-31贪心算法 | 56. 合并区间、738. 单调递增的数字、968. 监控二叉树
数据结构·c++·算法·leetcode·贪心算法·二叉树·递归
im_AMBER4 小时前
Leetcode 160 最小覆盖子串 | 串联所有单词的子串
开发语言·javascript·数据结构·算法·leetcode
帅小伙―苏6 小时前
力扣483找到字符串中所有字母异位词
算法·leetcode
smj2302_796826526 小时前
解决leetcode第3906题统计网格路径中好整数的数目
python·算法·leetcode
KobeSacre6 小时前
leetcode 树
算法·leetcode·职场和发展
大大杰哥7 小时前
leetcode hot100(1) 哈希
leetcode
叹一曲当时只道是寻常7 小时前
memos-cli 安装与使用教程:将 Memos 笔记同步到本地并支持 AI 语义搜索
人工智能·笔记·golang
Engineer邓祥浩7 小时前
LeetCode 热题 100 - 第1题:两数之和
算法·leetcode·职场和发展
阿Y加油吧8 小时前
算法二刷复盘:LeetCode 79 单词搜索 & 131 分割回文串(Java 回溯精讲)
java·算法·leetcode
6Hzlia8 小时前
【Hot 100 刷题计划】 LeetCode 101. 对称二叉树 | C++ DFS 极简递归模板
c++·leetcode·深度优先