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

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


相关推荐
To_OC2 小时前
LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了
javascript·算法·leetcode
To_OC19 小时前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
To_OC3 天前
LC 49 字母异位词分组:想到哈希表很简单,选对 key 才是精髓
javascript·算法·leetcode
To_OC4 天前
LC 1 两数之和:面试第一道必考题,暴力解法直接被面试官 pass
javascript·算法·leetcode
何以解忧,唯有..10 天前
Go语言循环语句详解:for、range与循环控制
开发语言·算法·golang
踏着七彩祥云的小丑10 天前
Go学习第9天:并发编程 + 文件操作 + 正则表达式
学习·golang·正则表达式·go
想吃火锅100510 天前
【leetcode】121.买卖股票的最佳时机js/c++
算法·leetcode·职场和发展
JCGKS10 天前
Go `init` 函数:包初始化顺序到底是怎样的
golang·init·init执行顺序
何以解忧,唯有..10 天前
Go语言中的const:常量声明与iota枚举详解
java·开发语言·golang
凌波粒10 天前
LeetCode--491.递增子序列(回溯算法)
数据结构·算法·leetcode