【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 <= numsi <= 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    // 取消选中

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


相关推荐
microrain34 分钟前
从数据烟囱到融合中心:SagooIoT统一数据处理平台的设计实践
物联网·golang·开源·sagooiot
mifengxing2 小时前
LeetCode 41.缺失的第一个正数|Hard题O(n)+O(1)最优解法深度解析
java·算法·leetcode·排序算法
DLYSB_4 小时前
API 网关流量洪峰与突发 CC 攻击:我用 Go 写了个“现场物理防御哨兵”,把故障响应压缩到秒级
开发语言·后端·golang·报警灯
hanlin037 小时前
动态规划专练:力扣第121、122题
笔记·算法·leetcode
To_OC7 小时前
LC 74 搜索二维矩阵:换皮的二分查找,我居然一开始没看出来
javascript·算法·leetcode
玖玥拾7 小时前
LeetCode 189 轮转数组
算法·leetcode
朋克洛德的码农7 小时前
反射是什么?
golang
Tisfy10 小时前
LeetCode 3345.最小可整除数位乘积 I:暴力枚举(从n开始尝试)
数学·算法·leetcode·题解·枚举
青山木11 小时前
Hot 100 --- 在排序数组中查找元素的第一个和最后一个位置
java·数据结构·算法·leetcode
mifengxing12 小时前
LeetCode 238 除自身以外数组的乘积|无除法O(n)时间+O(1)空间双解法
java·算法·leetcode