0
点赞
收藏
分享

微信扫一扫

C++递归lambda出现的循环初始值捕获问题分析与解决


tags: C++ lambda Debug

问题

昨天开始刷回溯系列, 在用Python的时候一切良好, 但是到了C++这里, 我突然发现一模一样的代码竟然不出值了, 具体情况如下, 题目是全排列​​46. 全排列 - 力扣(LeetCode)​​,我的代码如下:

class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
int ns = nums.size(), i = 0;
vector<int> path{};
vector<bool> used(ns, false);
vector<vector<int>> ans{};
function<void(void)> f = [&](void) {
if (path.size() == ns) {
ans.emplace_back(path);
return;
}
for (i = 0; i < ns; i++) {
if (used[i]) continue;
used[i]=true;
path.emplace_back(nums[i]);
f();
path.pop_back();
used[i] = false;
}
};
f();
return ans;
}
};

我在本地调试加上了对一维和二维数组的输出操作符重载:

#include <iostream> #include <vector> #include <cstring> #include <functional> using namespace std; ostream& operator<<(ostream& os, const vector<int>& v) { for (auto& i : v) os << i << " "; os << endl; return os; } ostream& operator<<(ostream& os, const vector<vector<int>>& v) { for (auto& ll : v) { for (auto& i : ll) os << i << " "; os << "\n"; } os << endl; return os; } // 主函数 int main(int argc, char const* argv[]) { Solution s; vector<int> v1 = {1, 2, 3}; cout << s.permute(v1) << endl; return 0; }

乍一看完全没有问题, 代码逻辑也没出错, 但是运行结果就是不尽人意:

1 2 3

只出来了一个排列(就是其自身), 也就是说回溯的过程根本就没有进行, 这是因为什么呢?

我加上了几行输出, 与标准答案进行对比,才逐渐找到了问题所在:​​lambda​​的值捕获与循环的值初始化出了问题!

分析与调试

class Solution {
public:
// void f(void) {}
vector<vector<int>> permute(vector<int>& nums) {
int ns = nums.size(), i = 0;
cout << "func() i=" << i << endl;
vector<int> path{};
vector<vector<int>> ans{};
vector<bool> used(ns, false);
function<void(void)> f = [&]() {
cout << "lambda() first i=" << i << endl;
if (path.size() == ns) {
ans.emplace_back(path);
return;
}
for (i = 0; i < ns; ++i) {
cout << "loop first i=" << i << endl;
if (used[i]) continue;
path.emplace_back(nums[i]);
used[i] = true;
f();
cout << "after recur, path=" << path;
path.pop_back();
used[i] = false;
cout << "loop last i=" << i << endl;
}
cout << "lambda() last i=" << i << endl;
};
f();
cout << "last func() i=" << i << endl;
return ans;
}
};

这次的输出如下:(为方便查看我用换行进行分割)

func()  i=0

lambda() first i=0
loop first i=0

lambda() first i=0
loop first i=0
loop first i=1

lambda() first i=1
loop first i=0
loop first i=1
loop first i=2

lambda() first i=2
after recur, path=1 2 3
loop last i=2

lambda() last i=3
after recur, path=1 2
loop last i=3

lambda() last i=4
after recur, path=1
loop last i=4

lambda() last i=5

last func() i=5
1 2 3

这也就找到了问题的所在, 剩下的部分还没有走到回溯的过程, 程序就被​​return;​​结束了…

为什么会有这样的问题呢? 可以看到最后​​i​​​的值竟然超过了​​3​​​, 直到递增到​​5​​​才结束, 这虽然不会引起​​vector​​​溢出, 但是循环之后的回溯过程只会进行一次, 就导致​​ans​​只有一个值了.

当递归深度到达3的时候, 也就是​​loop last i=2​​​这块, 可以看到​​i​​​此时已经是​​2​​​了, 也就是在这里, ​​for​​​中的自增运算符​​++​​​给​​i​​​赋值为​​3​​​, 不满足循环然后退出, 然后来到了递归深度为​​2​​​的地方, 这里面由于​​i​​​的值没进行更新, 所以还是不满足循环, 退出, ​​i​​​变成了​​4​​​, 最后一次​​i​​​加到了​​5​​​, 这时候程序内存空间中已经没有了开辟出来的新栈帧了, 于是​​f()​​​执行完毕, 最终​​ans​​只返回了一个值, 也就是自身.

不难发现, 对于递归函数来说, 特别是存在值共享的lambda函数中, 循环变量的初始化非常重要, 像这个程序中的​​i​​​值, 由于其初始化发生在递归函数外部, 内部访问​​i​​​的时候就只能通过lambda隐式捕获的方式获取, 即​​[&](){}​​​形式, 这就导致了整个递归函数内部的同时共享一份变量​​i​​, 这才会出现上述程序的执行结果不正确的问题.

要解决这个问题其实非常简单, 只需要将​​for​​​的循环变量初始化改为​​int i=0​​​, 每一次循环都会重新开辟一份​​i​​​, 这也就不会出现递归之后还是共享一份​​i​​导致递归终止的情况了.

小结

  • 使用lambda函数一定要注意值的传递和隐式捕获问题;
  • 循环变量作用域最好只在循环内部, 否则函数其他位置可能也会修改该变量, 导致意想不到的错误.


举报

相关推荐

0 条评论