《程序员面试金典(第6版)》面试题 04.12. 求和路径
创始人
2025-05-30 08:09:31

题目描述

给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。

示例:
给定如下二叉树,以及目标和 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的路径和的数量。

了解了抽象出来的题意后,我们再来根据题意撰写我们的代码。

深度优先搜索(DFS)(前序遍历)

我们刚刚已经了解了真正的题意了。也就不难写出符合题意的优质代码。

如果说,返回当前二叉树中所有节点都作为一颗二叉树,符合等于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)O(n)O(n),n为二叉树节点的数量。大问题呢,是要遍历整颗二叉树,也要遍历每个节点,每一个节点作为一个小问题,那就是n个小问题。所以总体的时间复杂度就是O(n2)O(n^2)O(n2)

空间复杂度:

  • 关于空间复杂度,由于使用了递归,在递归过程中调用栈将保存在内存中,所以空间复杂度也是O(n)O(n)O(n)。
    其中nnn表示树中节点的数量。这是由于在最坏情况下,当树退化为链表时,代码递归到最深处,将会有nnn层的函数调用堆栈被记录下来。

优化:前缀表

关于什么是前缀表可以去看我的这篇文章什么是前缀表,下面来说说,前缀表,到底做了哪些优化。

该解决方案采用哈希表原理来存储数据,并利用前缀和的思想来解决子问题。在访问每个节点时,我们都检查是否有累加到期望和的路径存在。因此时间复杂度为: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;}
};

总结

这道题一点也不算简单。真的是很考验,将题目抽象出具体的意思的能力。还有就是考虑你对一些算法技巧或思想的掌握情况,比如这道题就考验到了你关于算法前缀和这个思想理解的透不透彻,能不能想到用这种方式去解决问题。

相关内容

热门资讯

【开源协议】关于6种开源协议的... 关于开源协议的说明前言开源协议分为 宽松型 和 著作权型。如何选择?开源协议(GPL,...
easymock不能用了?fa... 什么是fast mock 前端常常要等待后面接口写好后,才能集成测试,免...
GIS的一些简单算法(自己作记... 一、线的矢量算法 1、简单的线相交算法 算法1:快速排斥与矢量跨立         快...
催债令来了!国家急了,严禁机关... 作者 | 剑书发现没有,当前国家摆在最高优先级的一件大事,竟然是给中小企业还清欠款。就在6月1日,国...
数据分析思维|思考问题的25个... 逻辑思维:能够理性分析问题,梳理出问题的本质和关键点,建立...
三、Docker:Compos...   Docker-compose 是用于定义和运行多容器 Docker 应用程序的编排工具。使用 d...
理想汽车5月交付40856辆,... 新京报贝壳财经讯 6月1日,理想汽车公布2025年5月交付数据。5月份,理想汽车交付新车40856辆...
新宙邦:公司对高性能电子材料均... 证券日报网讯 新宙邦5月30日在互动平台回答投资者提问时表示,公司对高性能电子材料均做了一些布局。其...
电商酒水类产品打假有哪些操作(... 线上酒水类产品蓬勃发展,但产品假货问题也随之而来,不仅损害了品牌商家的合法权益,还对消费者的健康和安...
今年很火的AI绘画怎么玩 1.前言 2022年绝对可以说是AIGC元年,从google搜索的趋势来看࿰...
canvas 教程 指南 万字... 前端妹子问我为什么官网上面只有一个canvas标签,里面什么都没有… 我脸色一变”完了...
开发一个app需要多少钱 APP应用开发主要分为原生APP和HTML5APP开发,使用HTML5开发的app价格...
vue项目的创建 npm install postcss postcss-pxtorem --save-dev 什么是...
二十六、对象的实例化内存布局与... 一、对象的实例化 1.判断对象对用的类是否加载、链接、初始化。 2.为对象分配内存。 3.处理并发...
助力私募行业高质量发展,华西证...   践行传播行业文化,进一步促进私募行业高质量发展,资本市场这场现象级行...
创建索引,解决mysql数据查... 实战场景 :两个表 T_PLATFORM A left join T_OPER_REC...
5. Python中的异常处理... 1. 说明: 自己写的代码保证万无一失有点难度,代码报出异常后ÿ...
图神经网络(GCN) 一、GCN的起源 曾经深度学习一直都是被几大经典模型给统治着,如CNN、RNN等等&#...
5月汽车销量出炉:理想力压小鹏... 【市场】6月第一天,多家汽车企业公布了5月的销量数据,和去年相比实现了两位数增长,但排名发生了变化。...
Python数据结构与算法(p... 主要内容:什么是列表查找顺序查找二分查找一、什么是查找?(...
状态机DP 状态机DP算法特征子数组类DP问题的定义:以最后一个元素结尾的最大xxx特征1...
mendeley管理文献样式 编辑GB/T 7714—2005参考文件样式,解决:显示大小写ÿ...
多次谋求A股上市!锂电巨头业绩... 知名锂电池生产企业天津力神电池股份有限公司(下称“天津力神”)的经营情况曝光。证券时报·e公司记者在...
前沿技术揭秘:云原生-展望容器... 2023年最火热的就是ChatGPT,当然还有5G技术、AI、机器学习、区块链等技术。另外还有一个现...
WalletConnect的下... 加密行业中每一个项目都有它自己的作用,很多我们意想不到或者认为的事情往往最后都会出现转...
独家 | 大润发佘咸平任永辉C... 交流永辉,实名添加微信lihua759321进群多位市场人士向《商业观察家》确认,大润发M会员店商品...
拼多多管理层多次强调利润不可持... 近日,拼多多一季度财报引起市场热议。财报显示,拼多多一季度营收957亿元,同比增长10%,归母净利润...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
矿泉水瓶装大米,便利店能增收3... 矿泉水瓶装大米,便利店能增收300万 把大米装进矿泉水瓶,一瓶卖到60块...
港股迎来打新热!“散户失权”引... 港股行情提振,新股表现亮眼,打新又热了。Wind显示,2025年港股已迎来27只新股,其中仅7只上市...