• 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;
        }
    };
    
    • 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

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

    #include 
    #include 
    #include 
    #include 
    
    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
    • 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

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

    1 2 3
    
    • 1

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

    我加上了几行输出, 与标准答案进行对比,才逐渐找到了问题所在: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;
        }
    };
    
    • 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

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

    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
    
    • 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

    这也就找到了问题的所在, 剩下的部分还没有走到回溯的过程, 程序就被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函数一定要注意值的传递和隐式捕获问题;
    • 循环变量作用域最好只在循环内部, 否则函数其他位置可能也会修改该变量, 导致意想不到的错误.
  • 相关阅读:
    迅睿cms如何判断用户是否登录
    从零开始学数据结构系列之第三章《二叉排序树基础了解》
    链路负载均衡之ISP选路
    深度学习学习笔记-模型的修改和CRUD
    C++基础——static成员
    uniapp开发小程序—picker结合后台数据实现二级联动的选择
    【Qt6】QWidgetAction 的使用
    基于Arduino的物流分拣控制系统设计
    【论文笔记】Enabling technologies and tools for digital twin
    JS模块化
  • 原文地址:https://blog.csdn.net/qq_41437512/article/details/127444087