c++

result.push_back(path)
result.pop_back()
vector used(99,0);会被当做函数声明;vector used{99,0};会创建一个长度为 2 的 used,内容是 {99,0};所以要vector used = vector(99, 0);
vector used(nums.size(), false);
result.push_back(path);不需要另外拷贝。push_back(或 emplace_back)都是拷贝(或移动)当前传入的元素到容器内部
cout没法直接输出数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Solution sol;                        // 创建一个 Solution 对象
vector<int> nums = {1, 2, 3}; // 准备输入
auto ans = sol.permute(nums); // 调用成员函数 permute

// 打印结果
for (const auto& perm : ans) {//ans 是一个 vector<vector<int>>,如果写成 for (auto perm : ans),每次循环都会拷贝一个 vector<int>(可能很大),开销不小。用 & 改为引用后,就不会拷贝,直接在原容器里访问元素。加上 const,编译器会阻止你在循环体里意外地修改 perm 的内容
//所以,const auto& 是一种 既高效(无拷贝)又安全(只读)的迭代方式,在只想读取容器元素时非常常见。
cout << "[ ";
for (int x : perm) {
cout << x << " ";
}
cout << "]\n";
}
return 0;

java

感觉和c++语法很相似,但是定义有比较大的区别。Java:所有的对象(除了基本类型)都是通过 new 放到堆(heap)上,JVM 负责垃圾回收。c++写 Solution sol; 或 vector v;,它就是一个栈(stack)上或编译器决定的内存区域里“自动”分配的对象,在作用域结束时立即析构(调用析构函数),内存自动释放。写 new Solution、new vector,才会到堆(free store)上去分配,需要手动 delete(或者用智能指针)来销毁
result.add(path)
path.remove(path.size()-1)
// Java 使用 List 接口和 ArrayList 实现,C++ 使用 vector
List<List> result = new ArrayList<>();
List path = new ArrayList<>();
boolean[] used = new boolean[nums.length];//也有直接用[]的,应该是根据需要调用的算法决定的
Solution sol = new Solution();//这里的sol其实是个指针,引用分配了的内存,但是c++就是在栈上创建了一个实体。写Solution* sol 才是指针,指向 new 出来的对象;也可以写 Solution& sol = *ptr; 来得到一个引用。
int[] nums = {1, 2, 3};
path.size() == nums.length啊?因为path是list,nums是int[]
copy:result.add(new ArrayList<>(path));

List<List> permutations = sol.permute(nums);
System.out.println(permutations);
可以直接输出二维数组

python

from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:在 Python 的类方法里,self 并不是一个关键字,而是一个约定俗成的名字,用来表示「方法正在作用的那个对象实例」——也就是你调用 permute 的那个 Solution 对象本身。sol = Solution() sol.permute([1,2,3])被转换为Solution.permute(sol, [1,2,3])

result: List[List[int]] = []
used = [False] * len(nums) # 标记数组
path: List[int] = [] # 当前构造的排列
len(path)
result.append(path[:])
result.append(path.copy()),不然所有对 path 的后续修改,都会反映在 result 里之前所有保存进去的那一份上。
path.pop()
for i in range(len(nums)):
for j in nums:
可以一维数据一整行一次性输出 for row in matrix: print(row)
也可以逐元素打印

1
2
3
for row in matrix:
# 将每个元素转成字符串,用空格连接
print(" ".join(map(str, row)))

或者循环两次。都是直接引用原数组的,不存在深拷贝
enumerate() 是一个内置函数,用来把一个可迭代对象(比如列表)“打包”成一个索引—元素对的迭代器
for i, row in enumerate(matrix):就相当于在每一次循环里同时拿到:i:当前元素在 matrix 中的下标(从 0 开始);row:matrix[i] 对应的那一行(或那一个元素)

go

func permute(nums []int) [][]int {}
var result [][]int
path := make([]int, 0, len(nums))
used := make([]bool, len(nums))
nums := []int{1, 2, 3}
permutations := permute(nums)
len(path)
res = append(res, tmp)
提前创建一个copy数组,避免后续对 path 的修改,反映在 result 里之前所有保存进去的那一份上。
path = path[:len(path)-1]
for i := range nums {} //for i := 0; i < len(matrix); i++{}
fmt.Println(permutations)
可以一整行一次性输出for _, row := range matrix { fmt.Println(row)}
或者两次循环。 _, 是空白占位符,表示我们不需要这个变量。range和enumerate是一样的。但是range是必须存在的,不能像python一样直接取row

1
2
3
4
5
6
7
for i, row := range matrix {//可以直接拿到
fmt.Printf("Row %d: ", i)
for _, val := range row {
fmt.Printf("%d ", val)
}
fmt.Println()
}

题目精讲

全排列

给定一个不含重复数字的数组 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]]

我的思路

回溯算法,有递归。跳出条件一定是排到最后一个数了。此时需要把这个数加到单层列表的末尾,把完整了的列表append到双层列表里。
关键是1.怎么找到那个没有被排过的数 2.怎么加到列表末尾。3.全局变量和局部变量怎么区分
在搜索之后 1.建立一个used字典表 2.必须把构建中的解传到递归函数里 3.另开一函数,全部当做局部参数,但是传进去的都是&。不需要return
3.记得回退,为了循环 4.回溯算法最好另外写,void

在有了思路以后写出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {

int total=nums.size();
if (total=1) {
int i=0;
while (i<used.size())
{if(used[i]=0) hhh=hhh.append(nums.append(used[i]));return hhh;}
else {
while (i<used.size())
{if(used[i]=0) {used[i]=1;hhh=permute(nums.append(used[i]));used[i]=0;}
}

}}}
private:
vector<vector<int>> hhh;
vector<int> used;
};

这里面的大多数语法其实是和python相似的,和c++差距很大,除了定义和函数调用是c++的其他都不是。
问题:
if (total = 1)
这里你用了赋值运算符 =,而不是比较运算符 ==,导致 total 会被改成 1,而且条件永远为真。

used 没有初始化
你在类成员里声明了 vector used;,但从未为它分配大小,后面访问 used[i] 会越界。

nums.append(…) 并不存在
vector 没有 append 方法,你想做的是往路径里 push 元素,而不是往原数组里。然后需要拆成两步

递归返回值和累积结果混乱
你在递归时写了 hhh = permute(…),但 permute 是返回 vector<vector>,而且全排列的每次递归都应向同一个结果容器里 “push” 一个新的排列。

逻辑不完整
终止条件、回溯时的恢复操作等都不符合标准的回溯框架。
gpt版本

空指针
栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {

int total=nums.size();
int i=0;
if (total==1) {

while (i<nums.size())
{if(used[i]==0){
nums.push_back(nums[i]);
hhh.push_back(nums);
nums.pop_back();
} i++;
}return hhh;
}
else {
while (i<nums.size())
{if(used[i]==0) {
used[i]=1;
nums.push_back(nums[i]);
permute(nums);
nums.pop_back();
used[i]=0;
}
i++;
}}
if(nums.size()<total) return hhh;
else cout<<hhh[0][0];
return hhh;


}
private:
vector<vector<int>> hhh;
vector<int> used{6,0};
};

nums的使用完全错误,是初始数组,不能在这个上面增减
nums有很多要改成aaa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {

int total=nums.size()-aaa.size();
int i=0;
if (total==1) {

while (i<nums.size())
{if(used[i]==0){
aaa.push_back(nums[i]);
hhh.push_back(aaa);
aaa.pop_back();
} i++;
}return hhh;
}
else {
while (i<nums.size())
{if(used[i]==0) {
used[i]=1;
aaa.push_back(nums[i]);
permute(nums);
aaa.pop_back();
used[i]=0;
}
i++;
}}
// if(aaa.size()<total) return hhh;
// else cout<<hhh[0][0];力扣只需要返回hhh就可以了,不需要输出
return hhh;


}
private:
vector<vector<int>> hhh;
vector<int> used = vector<int>(99, 0);
vector<int> aaa;
};

终于通过了的单函数版本,但是写的很抽象。一个是,利用私有变量做全局变量很不对,最好还是写两个函数。

缺点

  1. 全局/成员状态污染,缺乏重入性

hhh、used、aaa 都是类的私有成员,递归和多次调用之间互相复用,既没在入口清空,也没在结束后重置。

如果同一个 Solution 对象被多次调用,或在中途抛异常/提前返回,都会残留脏数据,结果不可预测。

  1. used 数组长度硬编码且不匹配,这个是没有设两个函数的副作用,不知道size应该多大

写成 vector used = vector(99,0);,与实际 nums.size() 没有任何关系。

若 nums.size()>99,就越界访问;若 <99,又会在无效位置(i>=nums.size() 时)把无关标记当作“未用”处理,生成错误结果。

递归调用只依赖全局副作用,不使用返回值

permute(nums); 递归后既不捕获其返回值,也不传入任何子问题状态,完全靠外部 hhh 累加。

这种“副作用+忽略返回”混淆了控制流,极易遗漏分支、出错、难以调试。

  1. 终止条件和控制流混乱,这个是直接拿原函数递归的副作用,本来不需要return hhh的递归结果,但是因为递归调用本身有返回值,所以必须返回。

用 total = nums.size() - aaa.size() 判断“只剩 1 个待选元素”,可读性差,比直接 if (aaa.size()==nums.size()) 难以理解。

if (total==1)… return hhh; else… 之后又有一个无条件 return hhh;,while、if、return 嵌套过深,控制流极不直观。

  1. 回溯状态管理不规范,撤销状态时,很容易没有将状态恢复到初始状态。

标准回溯要在「选择→递归→撤销」三步里完整管理好局部状态,而这段代码把“当前路径” aaa 和“标记” used 当成员,一层一层地 push/pop,既不清晰,也容易在撤销时漏掉或重复操作。

  1. 接口签名与实现不解耦

严格来说,这段代码的 permute 接口是正确的,但实现里却又私下依赖成员 aaa 传递状态,导致接口含糊——读者很难仅凭 permute(nums) 看出它到底走了哪些分支、用了哪些状态。

  1. 没有考虑空输入或重复调用

如果传入空 nums、或多次调用同一对象,hhh、aaa 都不清零,直接复用上一次结果,会返回错误输出。
used 长度写死 99、aaa 一开始是空、hhh 一开始也空,但都没有在 permute 入口显式初始化或说明,代码可维护性极差。

  1. 性能次优

虽然回溯本身是指数级,但这里还不断对成员变量做 push/pop、反复检查 used[i]==0、全局累加 hhh,在大规模 nums 下开销更大。

反正就是非常不应该用成员变量

成员变量

成员变量(也就是类里的非静态数据成员)本质上是用来描述“对象的长期状态”或“对象的属性”的。成员变量适合保存 “对整个对象都有意义,并且需要在多次调用间保持” 的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
vector<bool> used(n, false); // 标记每个元素是否已选
vector<int> path; // 当前排列路径
vector<vector<int>> result; // 存放所有排列结果

backtrack(nums, used, path, result);
return result;
}

private:
void backtrack(const vector<int>& nums,
vector<bool>& used,
vector<int>& path,
vector<vector<int>>& result) {
if (path.size() == nums.size()) {
// 找到一个完整排列,加入结果
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (used[i]) continue; // 跳过已选元素
// 选择 nums[i]
used[i] = true;
path.push_back(nums[i]);

backtrack(nums, used, path, result);

// 撤销选择
path.pop_back();
used[i] = false;
}
}
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
)

func permute(nums []int) [][]int {
var res [][]int
path := make([]int, 0, len(nums))
used := make([]bool, len(nums))

var backtrack func()
backtrack = func() {
if len(path) == len(nums) {
tmp := make([]int, len(path))
copy(tmp, path)
res = append(res, tmp)
return
}
for i := range nums {
if used[i] {
continue
}
// 选择 nums[i]
used[i] = true
path = append(path, nums[i])

backtrack()

// 撤销选择
path = path[:len(path)-1]
used[i] = false
}
}

backtrack()
return res
}

func main() {
nums := []int{1, 2, 3}
permutations := permute(nums)
fmt.Println(permutations)
// 输出: [[1 2 3] [1 3 2] [2 1 3] [2 3 1] [3 1 2] [3 2 1]]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.ArrayList;
import java.util.List;

public class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
boolean[] used = new boolean[nums.length];
List<Integer> path = new ArrayList<>();
backtrack(nums, used, path, result);
return result;
}

private void backtrack(int[] nums,
boolean[] used,
List<Integer> path,
List<List<Integer>> result) {
if (path.size() == nums.length) {
// 找到一个完整排列,拷贝添加到结果
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
// 选择 nums[i]
used[i] = true;
path.add(nums[i]);

backtrack(nums, used, path, result);

// 撤销选择
path.remove(path.size() - 1);
used[i] = false;
}
}

// 测试
public static void main(String[] args) {
Solution sol = new Solution();
int[] nums = {1, 2, 3};
List<List<Integer>> permutations = sol.permute(nums);
System.out.println(permutations);
}
}