给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。
示例:
给定如下二叉树,以及目标和 sum = 22,
5/ \4 8/ / \11 13 4/ \ / \7 2 5 1
返回: 3
解释:和为 22 的路径有:[5,4,11,2], [5,8,4,5], [4,11,7]
提示:节点总数 <= 10000
这道题,在我看来,是在考验你能不能从题目描述中,抽象出来它到底想表达什么意思。如果你做不到这一点,那对你来说就是hard
难度,如果你能抽象出来它的意思,那就还不错,可能会觉得稍微有点简单。
下面就让我来带大家来领略这道题真正想要表达的意思:
思考这样一个问题,这道题,是让你去求题目所给的二叉树所有的路径和,然后简简单单的对比一下和sum相当吗?
如果你这么想,那就说明你想的还是太简单了。你还在第一层,而出题人在第二层。
这道题真正的题意,返回当前二叉树中所有节点都作为一颗二叉树,符合等于sum的路径和的数量。
。
了解了抽象出来的题意后,我们再来根据题意撰写我们的代码。
我们刚刚已经了解了真正的题意了。也就不难写出符合题意的优质代码。
如果说,返回当前二叉树中所有节点
都作为一颗二叉树,符合等于sum的路径和的数量,这是一个大问题的话,那么返回单个节点
作为一颗二叉树,返回所有等于sum的路径和,这就是一个小问题。
我们只有先将小问题的代码逻辑写好了,才能再去写大问题的代码逻辑。这是自底向上的,没有根基,也就没有高楼。
很巧的是这个小问题,其实也就是我们刚刚想的第一层,它本质上,也是一个递归函数
。我们还能再去抽象出它的单层逻辑来,然后去撰写它的递归代码。下面是它的单层逻辑:
首先先设置一个值ret置为0。先判断 单个节点的值是否与sum相等。如果相等,则这就是一个符合sum的路径和。
然后因为判断完这单个节点了,接下来就该判断以这个节点作为一个二叉树的根节点,有没有符合sum的路径和。
现在对于单个节点的单层递归逻辑,到现在为止,我们就已经撰写完毕了。不难发现,这个递归函数的返回值是int类型的。然后需要TreeNode*与int作为形参传递进来。
那递归结束的条件呢?自然是如果root为空节点,然后返回0啦~。
下面给出单个节点作为一颗二叉树,返回所有等于sum的路径和
的代码:
int oneNodeOfTree(TreeNode* root,int sum){if(root == nullptr) return 0;int ret = 0;if(root->val == sum) ++ret;ret += oneNodeOfTree(root->left,sum - root->val);ret += oneNodeOfTree(root->right,sum - root->val);return ret;}
现在我们再重申一下这道题抽象出来的题目要求:
返回当前二叉树中所有节点都作为一颗二叉树,符合等于sum的路径和的数量。
我们已经做完单个节点作为一颗二叉树,返回所有等于sum的路径和
的代码了,现在就是想想这个二叉树有多少个节点嘛,然后我们把这些节点所有符合路径和的数量加起来就好了嘛。
这其实也是个递归函数。甚至可以说是前序遍历,因为我们要去统计二叉树中每一个节点。
这个函数逻辑也就更好写,其实和上面的逻辑差不多。唯一的不同就是 ret 的值是不同的。
单个节点的递归函数ret的初始值是0,这是因为我们只需要判断以这个节点为二叉树,他有多少条符合sum的路径和。
而整个二叉树所有节点的ret的初始值为 oneNodeOfTree(root,sum),这是因为,整个二叉树头节点也是一颗二叉树,它也有很多条路径,我们需要先把他作为单独的一个节点,扔进单个节点的递归函数,去作为ret的初始值。
然后之后的逻辑也差不多,具体的看代码就好了,我也就不多赘述了:
int pathSum(TreeNode* root, int sum) {if(root == nullptr) return 0;int ret = oneNodeOfTree(root,sum);ret += pathSum(root->left,sum);ret += pathSum(root->right,sum);return ret;}
下面是整体的代码:
class Solution {
public:int oneNodeOfTree(TreeNode* root,int sum){if(root == nullptr) return 0;int ret = 0;if(root->val == sum) ++ret;ret += oneNodeOfTree(root->left,sum - root->val);ret += oneNodeOfTree(root->right,sum - root->val);return ret;}int pathSum(TreeNode* root, int sum) {if(root == nullptr) return 0;int ret = oneNodeOfTree(root,sum);ret += pathSum(root->left,sum);ret += pathSum(root->right,sum);return ret;}};
时间复杂度:
空间复杂度:
关于什么是前缀表可以去看我的这篇文章什么是前缀表,下面来说说,前缀表,到底做了哪些优化。
该解决方案采用哈希表原理来存储数据,并利用前缀和的思想来解决子问题。在访问每个节点时,我们都检查是否有累加到期望和的路径存在。因此时间复杂度为:O(N),其中N是树中节点的数量。哈希表操作的空间要求是O(N),因为在最坏情况下哈希表将保存所有节点。同时, 这种做法的空间复杂度等价于我们使用队列迭代遍历的空间复杂度。所以空间复杂度也是O(N)。
综合而言,比较代码段1与代码段2,发现第二段代码是更好解决方案,因为它具有更低的时间复杂度和更高的空间效率,同时其递归深度更小,这使得其通常不会耗尽程序堆栈。
下面给出具体的代码实现:
class Solution {
public:unordered_map prefix; //前缀表int pathSum(TreeNode* root, int sum) {if(root == nullptr) return 0;prefix[0] = 1;//这么设置是因为,当表中第一次出现符合路径的时候,cur - sum的值会是0,所以我们的ret就会加上这个1,所以这里设置的键值对就为(0,1)int cur = 0; //这个cur是路径到当前节点的累加值。return dps(root,cur,sum);}int dps (TreeNode* root,int cur,int sum){if(root == nullptr) return 0;int ret = 0; //这个ret是最终返回的符合对应路径值的个数cur += root->val;if(prefix.count(cur - sum)) ret = prefix[cur-sum];//如果这个我们在前序表中,找到了这个键了,那么ret就加上对应的值。注意,键值可不只可能是0!!prefix[cur]++; //将当前路径的累加值作为一个键添加到前序表中,并将键对应的值加1。ret += dps(root->left,cur,sum); //遍历左子树,将左子树可能的路径都加入前序表中。ret += dps(root->right,cur,sum);//遍历右子树,将右子树可能的路径也都加入前序表中//当我们遍历完一条路径的时候,我们要将对应键值对中的值清空。如果不清空,前序遍历到右子树的时候,//就有可能出现找到了之前左子树没有清空的那个键,导致 多加了一个错误的1prefix[cur]--;return ret;}
};
这道题一点也不算简单。真的是很考验,将题目抽象出具体的意思的能力。还有就是考虑你对一些算法技巧或思想的掌握情况,比如这道题就考验到了你关于算法前缀和这个思想理解的透不透彻,能不能想到用这种方式去解决问题。