这道题折磨了我好长时间,终于通过了所有test。语言c++,时间16ms。成就感能让我继续努力,所以记录一下。
题目在这里Path Sum II,题目的意思是定义从二叉树的根结点到叶子结点为一条路径,将路径上所有结点的值加起来是该路径的和(path sum),现在给定一个整数,求出所有路径和等于该数的路径,结果保存在二维数组中。
思路一:递归。
与二叉树有关的大部分问题都可以用递归来处理,当问题规模不大时,基本上都很有效。将一维数组的引用作为参数传递进来,每次递归都新开一个数组,用来赋值参数数组中的元素,然后再加上新结点的值;如果当前结点为叶子结点,那么计算数组的和,看是否与给定的数相等,如果相等,将该数组添加到二维数组中去;如果不是叶子节点,分别为左右子树递归调用该函数。递归的返回条件是:1、该结点为叶子结点。代码很简单,我就不详细说了。
但是递归通常比迭代法更慢,OJ给出的时间是24ms,而且上述方法占用空间较大,因为每次调用该函数都要新开一个空间。不是最好的办法。
思路二:回溯
这也是一种常规方法,但是其中的逻辑比较复杂。从根结点出发,沿着左子树一直向下走,直至碰到叶子结点;然后回溯至其父结点,访问其兄弟结点(如果右孩子存在);两个孩子结点都访问之后,再次向上回溯至其祖父节点。对每个结点都执行此操作,如果找一个叶子结点,则比较路径和与给定整数是否相等,若相等,添加一条路径。
- 设一个记录结点指针的栈
stack<TreeNode*> s;
,栈顶为当前结点的指针。 - 设一个记录路径的数组
vector<int> path;
利用它的push_back方法和pop_back方法可以实现向前的探索和向后的回溯,它记录的结点顺序和上面栈的顺序完全一致。 两个整形变量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即可。