每日一题
2026.11.1
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 中的所有整数 互不相同
总体思路
本题采用回溯算法(DFS)遍历所有可能的组合。维护一个当前路径 tmp,每次从数组中挑选一个数加入路径。
为了防止重复,每次挑选前遍历 tmp 查重:若该数字未在路径中,则将其加入 tmp 并递归进入下一层;若已存在,则跳过。
当 tmp 长度等于原数组长度时,说明生成了一个完整的全排列,将其深拷贝后存入结果集 res。
递归结束后,将 tmp 最后一个元素弹出(回溯),恢复现场,继续尝试其他分支。该方法逻辑直观,省去了额外的 visited 标记数组。
- 时间复杂度:O(n * n!)
一共有 n! 个结果。
在这个特定版本的代码中,每次往深走一层,都要遍历 tmp 来查重(O(n)),所以总复杂度比使用 visited 数组的方法要高一点点。但由于全排列本身的复杂度极高(阶乘级别),常数项的差异在实际中影响不算太大。 - 空间复杂度:O(n)
递归调用栈的深度最大为 n。tmp 数组的长度最大也为 n。不计算最终返回结果 res 的占用空间。
代码
golang
go
package main
import "fmt"
func permute(nums []int) [][]int {
res := [][]int{}
tmp := []int{}
// 定义闭包 dfs
var dfs func()
dfs = func() {
// 1. 终止条件:路径长度等于数组长度,说明找到了一个全排列
if len(tmp) == len(nums) {
// 深拷贝:防止后续 tmp 被修改影响已存入的结果
tmpCopy := make([]int, len(tmp))
copy(tmpCopy, tmp)
res = append(res, tmpCopy)
return
}
// 2. 遍历所有可用的数字
for i := 0; i < len(nums); i++ {
// 检查 nums[i] 是否已经在 tmp 中
found := false
for j := 0; j < len(tmp); j++ {
if nums[i] == tmp[j] {
found = true
break
}
}
// 如果没找到,说明可用
if !found {
tmp = append(tmp, nums[i]) // 做选择
dfs() // 递归
tmp = tmp[:len(tmp)-1] // 撤销选择(回溯)
}
}
}
dfs()
return res
}
func main() {
nums := []int{1, 2, 3}
fmt.Println(permute(nums))
}
///!!or!!
func main() {
// 使用 bufio 提高读取速度
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords) // 按空格分割输入
// 处理多组输入直到 EOF
for {
if !scanner.Scan() {
break
}
var n int
fmt.Sscanf(scanner.Text(), "%d", &n)
nums := make([]int, n)
for i := 0; i < n; i++ {
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &nums[i])
}
}
// 获取结果并打印
res := permute(nums)
for _, p := range res {
for i, v := range p {
if i > 0 {
fmt.Print(" ")
}
fmt.Print(v)
}
fmt.Println()
}
}
}
78. 子集
题目
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = 1,2,3
输出:\[,1,2,1,2,3,1,3,2,3,1,2,3]
示例 2:
输入:nums = 0
输出:\[,0]
提示:
1 <= nums.length <= 10
-10 <= numsi <= 10
nums 中的所有元素 互不相同
总体思路

- 时间复杂度:O(n * 2^n)。一共有 2^n 个子集,每个子集需要用 O(n) 的时间进行 copy 拷贝到结果集里。
- 空间复杂度:O(n)。不计结果集的空间,递归栈深度和 tmp 的最大长度均为 n。如果算上结果集,总空间为 O(n * 2^n)。
代码
golang
go
package main
import "fmt"
func subsets(nums []int) [][]int {
res := [][]int{}
tmp := []int{}
// 定义闭包 dfs,pathLen 表示当前正在考虑 nums 的哪个数字
var dfs func(pathLen int)
dfs = func(pathLen int) {
// 1. 终止条件:所有数字都已经做过决定了
if pathLen == len(nums) {
// 【大坑注意】必须深拷贝,否则最后 res 里全是空切片
tmpCopy := make([]int, len(tmp))
copy(tmpCopy, tmp)
res = append(res, tmpCopy)
return
}
// 2. 分支一:选择当前数字
tmp = append(tmp, nums[pathLen]) // 把数字加进来
dfs(pathLen + 1) // 递归,去决定下一个数字
tmp = tmp[:len(tmp)-1] // 回溯,把刚才加进来的数字弹出去(撤销选择)
// 3. 分支二:不选择当前数字
dfs(pathLen + 1) // 直接递归,去决定下一个数字
}
dfs(0)
return res
}
func main() {
fmt.Println(subsets([]int{1, 2, 3}))
}
//!!!!!!!
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords)
for {
if !scanner.Scan() {
break
}
var n int
fmt.Sscanf(scanner.Text(), "%d", &n)
nums := make([]int, n)
for i := 0; i < n; i++ {
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &nums[i])
}
}
res := subsets(nums)
for _, subset := range res {
for i, v := range subset {
if i > 0 {
fmt.Print(" ")
}
fmt.Print(v)
}
fmt.Println() // 每个子集占一行
}
}
}
39. 组合总和
题目
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = 2,3,6,7, target = 7
输出:\[2,2,3,7]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = 2,3,5, target = 8
输出: \[2,2,2,2,2,3,3,3,5]
示例 3:
输入: candidates = 2, target = 1
输出: \[\]
提示:
1 <= candidates.length <= 30
2 <= candidatesi <= 40
candidates 的所有元素 互不相同
1 <= target <= 40
总体思路
这道题的核心要求是:从给定的数组 candidates 中找出所有和为 target 的组合,每个数字可以被无限次重复选取,且组合不能重复(例如 2, 2, 3 和 2, 3, 2 视为同一个)。
🧠 总体思路
为了解决"组合去重"和"数字可重复选"两个问题,我们需要在递归时引入一个 start 参数。
- 组合去重:每次从下标 start 开始遍历,这保证了我们选出的数字组合是有序的(比如选了 2 之后,只能选 2 或 2 之后的数,不能回头选前面的数),从而避免出现 2, 3, 2 这种重复组合。
- 数字可重复:在递归进入下一层时,传入的 start 是当前下标 i,而不是 i + 1。这样下一层依然可以从当前数字开始选,实现无限次重复。
- 剪枝优化:如果当前累加和已经超过 target(即 target - candidatesi < 0),直接停止当前分支的搜索。
- 时间复杂度:O(S),其中 S 是所有可行解的长度之和。由于是无限重复选取,解的数量会非常庞大(如果包含 1,可能是指数级甚至阶乘级,称为"组合爆炸"),最坏情况是指数级别。
- 空间复杂度:O(target)。递归树的最大深度取决于剩余目标值 target 能够被最小的数字减多少次。比如最小是 1,那么深度就是 target。不计算结果集 res 所占的空间。
代码
golang
go
package main
import "fmt"
func combinationSum(candidates []int, target int) [][]int {
res := [][]int{}
tmp := []int{}
var dfs func(start, remain int)
dfs = func(start, remain int) {
// 1. 终止条件:如果剩余目标值为 0,说明找到了一个合法组合
if remain == 0 {
tmpCopy := make([]int, len(tmp))
copy(tmpCopy, tmp) // 深拷贝
res = append(res, tmpCopy)
return
}
// 2. 遍历当前可选的数字
for i := start; i < len(candidates); i++ {
// 剪枝:如果当前数字比剩余目标值还大,直接跳过(因为数组全是正数,再加只会更大)
if candidates[i] > remain {
continue
}
// 做选择
tmp = append(tmp, candidates[i])
// 递归:注意这里传的是 i,而不是 i+1。因为数字可以重复使用
dfs(i, remain - candidates[i])
// 撤销选择(回溯)
tmp = tmp[:len(tmp)-1]
}
}
dfs(0, target)
return res
}
func main() {
candidates := []int{2, 3, 6, 7}
target := 7
fmt.Println(combinationSum(candidates, target))
// 输出: [[2 2 3] [7]]
}
///or!!!!!!!
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords)
for {
if !scanner.Scan() {
break
}
var n, target int
fmt.Sscanf(scanner.Text(), "%d", &n)
scanner.Scan()
fmt.Sscanf(scanner.Text(), "%d", &target)
candidates := make([]int, n)
for i := 0; i < n; i++ {
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &candidates[i])
}
}
res := combinationSum(candidates, target)
for _, combination := range res {
for i, v := range combination {
if i > 0 {
fmt.Print(" ")
}
fmt.Print(v)
}
fmt.Println()
}
}
}