LeetCode: 113. Path Sum II

这道题折磨了我好长时间,终于通过了所有test。语言c++,时间16ms。成就感能让我继续努力,所以记录一下。

题目在这里Path Sum II,题目的意思是定义从二叉树的根结点到叶子结点为一条路径,将路径上所有结点的值加起来是该路径的和(path sum),现在给定一个整数,求出所有路径和等于该数的路径,结果保存在二维数组中。

思路一:递归。

与二叉树有关的大部分问题都可以用递归来处理,当问题规模不大时,基本上都很有效。将一维数组的引用作为参数传递进来,每次递归都新开一个数组,用来赋值参数数组中的元素,然后再加上新结点的值;如果当前结点为叶子结点,那么计算数组的和,看是否与给定的数相等,如果相等,将该数组添加到二维数组中去;如果不是叶子节点,分别为左右子树递归调用该函数。递归的返回条件是:1、该结点为叶子结点。代码很简单,我就不详细说了。
但是递归通常比迭代法更慢,OJ给出的时间是24ms,而且上述方法占用空间较大,因为每次调用该函数都要新开一个空间。不是最好的办法。

思路二:回溯

这也是一种常规方法,但是其中的逻辑比较复杂。从根结点出发,沿着左子树一直向下走,直至碰到叶子结点;然后回溯至其父结点,访问其兄弟结点(如果右孩子存在);两个孩子结点都访问之后,再次向上回溯至其祖父节点。对每个结点都执行此操作,如果找一个叶子结点,则比较路径和与给定整数是否相等,若相等,添加一条路径。

  1. 设一个记录结点指针的栈stack<TreeNode*> s;,栈顶为当前结点的指针。
  2. 设一个记录路径的数组vector<int> path;利用它的push_back方法和pop_back方法可以实现向前的探索和向后的回溯,它记录的结点顺序和上面栈的顺序完全一致。
  3. 两个整形变量left_visited和right_visited用来记录当前结点的左右孩子是否已经访问,这是这个逻辑中最重要的回溯的条件。

    下面先把代码贴出来

vector<vector<int> > pathSum(TreeNode* root, int sum)
{
    vector<vector<int> > ret;
    vector<int> path;

    if(!root)
        return ret;

    int left_visited = 0;//0: not visited; 1: visited
    int right_visited = 0;
    TreeNode* cur = root;
    TreeNode* pre = NULL;
    stack<TreeNode*> s;
    s.push(cur);

    while(!s.empty())
    {
        cur = s.top();
        //计算当前结点的左右孩子是否被访问
        if((pre && pre == cur->left) || (pre == cur && cur->left == NULL))
            left_visited = 1;
        else
            left_visited = 0;
        if((pre && pre == cur->right) || (pre == cur && cur->right == NULL))
            right_visited = 1;
        else
            right_visited = 0;

        //只有当结点的左右孩子都没有访问过时,才加入路径中
        if(!left_visited && !right_visited)
            path.push_back(cur->val);

        //找到叶子结点,计算路径和,如果与sum相等,将该路径添加到ret
        if(!cur->left && !cur->right)
        {
            int calc = accumulate(path.begin(), path.end(), 0);
            if(calc == sum)
                ret.push_back(path);
            path.pop_back();
            s.pop();
        }
        else//若不是,分类讨论
        {
            //如果右孩子已经被访问过,则应该执行回溯了,因为使用了前序遍历的逻辑
            if(right_visited == 1)
            {
                path.pop_back();
                s.pop();
            }
            else//如果没有,应该先判断左孩子是否访问;先访问左孩子
            {
                if(left_visited == 0 && cur->left != NULL)
                    s.push(cur->left);
                else if(cur->right != NULL)
                    s.push(cur->right);
            }
        }
        pre = cur;
    }
    return ret;
}

第3行–第14行:

定义好变量,做好初始化和异常处理,将根结点压栈,然后进入后面的循环不变式

第16行:

循环不变式的条件是栈s不空,如果栈为空,表示已经遍历过所有的路径,下面来看为什么是这样

第18行–第27行:

从栈顶取出当前结点,然后判断左右孩子是否已经访问过。判断的逻辑如下:
1、pre指针指向上一次处理的结点,cur指向当前结点;如果现在还没碰到叶子结点,也就不用回溯,pre一定是指向cur的父结点的,所以判断条件pre && pre == cur->left不成立,后面一个判断条件pre == cur && cur->left == NULL显然也不成立,此时cur的左孩子显然是未被访问的。
2、如果cur上一次已经碰到叶子结点,那么本次它应该是在该叶子结点的父结点位置,pre指向该叶子结点,判断条件pre && pre == cur->left得以成立;后面一个条件是考虑这种情况,即“准备访问cur的左孩子,但由于它不存在(见52-53行)”,这样pre已经被赋值为cur,下次循环cur再指向栈顶元素而不是其左孩子,所以可能出现pre == cur的情况,再加上cur->left == NULL的条件,可以认为已经访问过了左孩子。
3、对于右孩子是否被访问,逻辑是相同的,将所有left换为right即可

第30–31行:

只有当结点的左右孩子都没有被访问过时,才将其纳入路径中。这是为了防止在回溯过程中,多次地添加一个结点的值(最多3次,即分别从其左右孩子返回时,各多加一次)

第34–41行:

如果找到叶子结点,计算路径和,如果与sum相等,将该路径添加到ret,然后回溯(这里出现一次)到其父结点:从路径中的删除该结点,从栈中弹出该结点

第45–49行:

一定要先判断右孩子!如果右孩子已经被访问过了,说明左孩子肯定已经访问过了,因为是前序遍历。此时直接回溯(这里又出现了一次),操作与前面相同。

第50–56行:

如果右孩子没有被访问过,则先看能否访问左孩子,再看能否访问右孩子。注意如果左右孩子任一为空,则不会添加空指针到栈中,所以会出现前面判断逻辑中所说的“pre == cur”的情况,这种情况也要考虑到,说明访问到了一个空的左或右孩子。

第58行:

虽然简单但很容易忽略。每次处理之后将cur的值赋给pre

最后栈s为空,说明已经试探过了每一条路径,路径和相等的已经放到了ret中,最后返回ret即可。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值