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

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


相关推荐
liuyao_xianhui5 小时前
优选算法_栈_删除字符中的所有相邻重复项_C++
开发语言·数据结构·c++·python·算法·leetcode·链表
老虎06275 小时前
LeetCode热题100 刷题笔记(第三天)链表 「两数相加」
笔记·leetcode·链表
会编程的土豆5 小时前
【leetcode hot 100】二叉树
算法·leetcode
Mr_Xuhhh6 小时前
LeetCode 热题 100 刷题笔记:数组与排列的经典解法(续)
算法·leetcode·职场和发展
Mr_Xuhhh7 小时前
LeetCode 热题 100 刷题笔记:高频面试题详解(215 & 347)
算法·leetcode·排序算法
童话ing7 小时前
【LeetCode】239.滑动窗口最大值
数据结构·算法·leetcode·golang
不会写DN8 小时前
Go 中最主流 JWT 库 jwt -go
开发语言·后端·golang
_日拱一卒8 小时前
LeetCode:和为K的子数组
算法·leetcode·职场和发展
Mr_Xuhhh8 小时前
LeetCode 热题 100 刷题笔记:数组与排列的经典解法
数据结构·算法·leetcode