具体问题
路径总和 II
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
解题思路
解法一:DFS
需要用到二维的vector,而且每当DFS搜索到新节点时,都要保存该节点。而且每当找出一条路径之后,都将这个保存为一维vector的路径保存到最终结果二位vector中。并且,每当DFS搜索到子节点,发现不是路径和时,返回上一个结点时,需要把该节点从一维vector中移除。
vector<vector<int>> pathSum(TreeNode* root, int sum) {
vector<vector<int>> res;
vector<int> out;
dfs(root, sum, out, res);
return res;
}
void dfs(TreeNode* root, int sum, vector<int> &out, vector<vector<int>> &res) {
if (!root) return;
out.push_back(root->val);
if (!root->left && !root->right && root->val == sum) {
res.push_back(out);
}
dfs(root->left, sum - root->val, out, res);
dfs(root->right, sum - root->val, out, res);
out.pop_back();
}
解法二:迭代(中序遍历)
中序遍历本来是要用栈来辅助运算的,由于我们要取出路径上的节点值,所以用一个vector来代替stack,首先利用while循环找到最左子节点,在找的过程中,把路径中的节点值都加起来,这时候取出vector中的尾元素,如果其左右子节点都不存在且当前累加值正好等于sum了,我们将这条路径取出来存入结果res中。
下面的部分是和一般的迭代中序写法有所不同的地方,因为如果当前最左节点已经是个叶节点了,要转移到其他的节点上时需要把当前的节点值减去,而如果当前最左节点不是叶节点,下面还有一个右子节点,这时候移动指针时就不能减去当前节点值,为了区分这两种情况,需要用一个额外指针pre来指向前一个节点,如果右子节点存在且不等于pre,直接将指针移到右子节点,反之更新pre为cur,cur重置为空,val减去当前节点,s删掉最后一个节点。
vector<vector<int>> pathSum(TreeNode* root, int sum) {
vector<vector<int>> res;
vector<TreeNode*> s;
TreeNode *cur = root, *pre = NULL;
int val = 0;
while (cur || !s.empty()) {
while (cur) {
s.push_back(cur);
val += cur->val;
cur = cur->left;
}
cur = s.back();
if (!cur->left && !cur->right && val == sum) {
vector<int> v;
for (auto it : s) {
v.push_back(it->val);
}
res.push_back(v);
}
if (cur->right && cur->right != pre) {
cur = cur->right;
} else {
pre = cur;
val -= cur->val;
s.pop_back();
cur = NULL;
}
}
return res;
}
路径总和 III
给定一个二叉树,它的每个结点都存放着一个整数值。找出路径和等于给定数值的路径总数。路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
解题思路
解法一:先序遍历(递归)
相当于先序遍历二叉树,对于每一个节点都有记录了一条从根节点到当前节点到路径,同时用一个变量 curSum 记录路径节点总和,然后看 curSum 和 sum 是否相等,相等的话结果 res 加1,不等的话我们来继续查看子路径和有没有满足题意的,做法就是每次去掉一个节点,看路径和是否等于给定值,注意最后必须留一个节点,不能全去掉了,因为如果全去掉了,路径之和为0,而如果给定值刚好为0的话就会有问题。
int pathSum(TreeNode* root, int sum) {
int res = 0;
vector<TreeNode*> out;
preorder(root, sum, 0, out, res);
return res;
}
void preorder(TreeNode *root, int sum, int curSum, vector<TreeNode*> &out, int &res) {
if (!root) return;
curSum += root->val;
out.push_back(root);
if (curSum == sum) ++res;
int t = curSum;
for (int i = 0; i < out.size() - 1; ++i) {
t -= out[i]->val;
if (t == sum) ++res;
}
preorder(root->left, sum, curSum, out, res);
preorder(root->right, sum, curSum, out, res);
out.pop_back();
}
解法二:递归
用两个递归函数,其中的一个 sumUp 递归函数是以当前结点为起点,和为 sum 的路径个数,采用了前序遍历,对于每个遍历到的节点进行处理,维护一个变量 pre 来记录之前路径之和,然后 cur 为 pre 加上当前节点值,如果 cur 等于 sum,那么返回结果时要加1,然后对当前节点的左右子节点调用递归函数求解,这是 sumUp 递归函数。而在 pathSum 函数中,对当前结点调用 sumUp 函数,加上对左右子结点调用 pathSum 递归函数,三者的返回值相加就是所求。
int pathSum(TreeNode* root, int sum) {
if (!root) return 0;
return sumUp(root, 0, sum) + pathSum(root->left, sum) + pathSum(root->right, sum);
}
int sumUp(TreeNode *root, int pre, int sum) {
if (!root) return 0;
int cur = pre + root->val;
return (cur == sum) + sumUp(root->left, cur, sum) + sumUp(root->right, cur, sum);
}
二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
解题思路
和 路径总和 II 类似,还是用递归来解,然后再回溯回去。在递归函数中,当我们遇到叶结点的时候,即没有左右子结点,那么此时一条完整的路径已经形成了,加上当前的叶结点后存入结果res中,然后回溯。注意这里结果res需要引用,而out是不需要引用的,不然回溯回去还要删除新添加的结点。
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
dfs(root, "", res);
return res;
}
void dfs(TreeNode* root, string out, vector<string> &res) {
if (!root) return;
if (!root->left && !root->right)
res.push_back(out + to_string(root->val));
if (root->left)
dfs(root->left, out + to_string(root->val) + '->', res);
if (root->right)
dfs(root->right, out, to_string(root->val) + '->', res);
}
二叉树中的最大路径和
给定一个非空二叉树,返回其最大路径和。本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
解题思路
对于每个结点来说,要知道经过其左子结点的 path 之和大还是经过右子节点的 path 之和大。那么的递归函数返回值就可以定义为以当前结点为根结点,到叶节点的最大路径之和,然后全局路径最大值放在参数中,用结果 res 来表示。
在递归函数中,如果当前结点不存在,那么直接返回0。否则就分别对其左右子节点调用递归函数,由于路径和有可能为负数,所以和0相比,取较大的那个,就是要么不加,加就要加正数。然后来更新全局最大值结果 res,就是以左子结点为终点的最大 path 之和加上以右子结点为终点的最大 path 之和,还要加上当前结点值,这样就组成了一个条完整的路径。返回值是取 left 和 right 中的较大值加上当前结点值,因为返回值的定义是以当前结点为终点的 path 之和,所以只能取 left 和 right 中较大的那个值,而不是两个都要。
int maxPathSum(TreeNode* root) {
int res = INT_MIN;
dfs(root, res);
return res;
}
int dfs(TreeNode *root, int &res) {
if (!root) return 0;
int left = max(dfs(root->left, res), 0);
int right = max(dfs(root->right, res), 0);
res = max(res, left + right + root->val);
return max(left, right) + root->val;
}
二叉树的边界
给定一棵二叉树,以逆时针顺序从根开始返回其边界。边界按顺序包括左边界、叶子结点和右边界而不包括重复的结点(结点的值可能重复)。左边界的定义是从根到最左侧结点的路径。右边界的定义是从根到最右侧结点的路径。若根没有左子树或右子树,则根自身就是左边界或右边界。注意该定义只对输入的二叉树有效,而对子树无效。最左侧结点的定义是:在左子树存在时总是优先访问,如果不存在左子树则访问右子树。重复以上操作,首先抵达的结点就是最左侧结点。最右侧结点的定义方式相同,只是将左替换成右。
解题思路
用bool型变量来标记当前是求左边界结点还是求右边界结点,同时还有加入叶结点到结果res中的功能。如果左边界标记为true,那么将结点值加入结果res中,下面就是调用对左右结点调用递归函数了。如果是求左边界结点,优先调用左子结点,当左子结点不存在时再调右子结点,而对于求右边界结点,优先调用右子结点,当右子结点不存在时再调用左子结点。综上考虑,在对左子结点调用递归函数时,左边界标识设为leftbd && node->left,而对右子结点调用递归的左边界标识设为leftbd && !node->left,这样左子结点存在就会被优先调用。而右边界结点的情况就正好相反,调用左子结点的右边界标识为rightbd && !node->right, 调用右子结点的右边界标识为 rightbd && node->right,这样就保证了右子结点存在就会被优先调用。
vector<int> boundaryOfBinaryTree(TreeNode* root) {
if (!root) return {};
vector<int> res{root->val};
dfs(root->left, true, false, res);
dfs(root->right, false, true, res);
return res;
}
void dfs(TreeNode *root, bool leftbd, bool rightbd, vector<int> &res) {
if (!root) return;
if (!root->left && !root->right) {
res.push_back(root->val);
return;
}
if (leftbd) res.push_back(root->val);
dfs(root->left, leftbd && root->left, rightbd && !root->right, res);
dfs(root->right, leftbd && !root->left, rightbd && root->right, res);
if (rightbd) res.push_back(root->val);
}
二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。最近公共祖先的定义为:对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
解题思路
在二叉树中来搜索p和q,然后从路径中找到最后一个相同的节点即为父节点,可以用递归来实现,在递归函数中,首先看当前结点是否为空,若为空则直接返回空,若为p或q中的任意一个,也直接返回当前结点。否则的话就对其左右子结点分别调用递归函数。由于这道题限制了p和q一定都在二叉树中存在,那么如果当前结点不等于p或q,p和q要么分别位于左右子树中,要么同时位于左子树,或者同时位于右子树。
- 若p和q分别位于左右子树中,那么对左右子结点调用递归函数,会分别返回p和q结点的位置,而当前结点正好就是p和q的最小共同父结点,直接返回当前结点即可。
- 若p和q同时位于左子树,这里有两种情况,一种情况是 left 会返回p和q中较高的那个位置,而 right 会返回空,所以最终返回非空的 left 即可。还有一种情况是会返回p和q的最小父结点,就是说当前结点的左子树中的某个结点才是p和q的最小父结点,会被返回。
- 若p和q同时位于右子树,同样这里有两种情况,一种情况是 right 会返回p和q中较高的那个位置,而 left 会返回空,所以最终返回非空的 right 即可,还有一种情况是会返回p和q的最小父结点,就是说当前结点的右子树中的某个结点才是p和q的最小父结点,会被返回。
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || p == root || q == root) return root;
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root;
return left ? left : right;
}
最深叶节点的最近公共祖先
给你一个有根节点的二叉树,找到它最深的叶节点的最近公共祖先。
解题思路
最深叶子节点的公共祖先的左右子树高度相同,也就是最深叶子节点的深度一定相同。如果左右子树不等高,高度小的那个子树节点的叶子节点的深度肯定不是最深的(因为比高度大的子树深度小)。所以,最深叶子节点肯定在深度较大的子树当中,采用深度优先搜索,每次只要继续往深度更大的子树进行递归即可。如果左右子树深度相同,表示获取到了最深叶子节点的最近公共祖先。
TreeNode* lcaDeepestLeaves(TreeNode* root) {
if (!root) return root;
int left = maxDepth(root->left);
int right = maxDepth(root->right);
if (left == right) return root;
return left > right ? lcaDeepestLeaves(root->left) : lcaDeepestLeaves(root->right);
}
int maxDepth(TreeNode *root) {
if (!root) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
节点与其祖先之间的最大差值
给定二叉树的根节点 root,找出存在于不同节点 A 和 B 之间的最大值 V,其中 V = |A.val - B.val|,且 A 是 B 的祖先。(如果 A 的任何子节点之一为 B,或者 A 的任何子节点是 B 的祖先,那么我们认为 A 是 B 的祖先)
解题思路
每次递归查找当前路径的最大值和最小值,到达叶子节点时使用当前路径的最大值和最小值计算结果。
int maxAncestorDiff(TreeNode* root) {
return dfs(root, root->val, root->val);
}
int dfs(TreeNode *root, int minVal, int maxVal) {
if (!root) return maxVal - minVal;
maxVal = max(maxVal, root->val);
minVal = min(minVal, root->val);
return max(dfs(root->left, minVal, maxVal), dfs(root->right, minVal, maxVal));
}
二叉树最长连续序列
给你一棵指定的二叉树,请你计算它最长连续序列路径的长度。该路径,可以是从某个初始结点到树中任意结点,通过「父 - 子」关系连接而产生的任意路径。这个最长连续的路径,必须从父结点到子结点,反过来是不可以的。
解题思路
使用先序遍历,对于每个遍历到的节点,看节点值是否比参数值(父节点值)大1,如果是则长度加1,否则长度重置为1,然后更新结果res,再递归调用左右子节点即可。
int longestConsecutive(TreeNode* root) {
if (!root) return 0;
int res = 0;
preorder(root, root->val, 0, res);
return res;
}
void preorder(TreeNode *root, int val, int len, int &res) {
if (!root) return;
if (root->val == val + 1) {
++len;
} else {
len = 1;
}
res = max(res, len);
preorder(root->left, root->val, len, res);
preorder(root->right, root->val, len, res);
}
二叉树最长连续序列 II
给定一个二叉树,你需要找出二叉树中最长的连续序列路径的长度。请注意,该路径可以是递增的或者是递减。例如,[1,2,3,4] 和 [4,3,2,1] 都被认为是合法的,而路径 [1,2,4,3] 则不合法。另一方面,路径可以是「子-父-子」顺序,并不一定是「父-子」顺序。
解题思路
因为每个结点的最长连续序列长度等于其最长递增路径长度跟最长递减路径之和加1,然后分别对其左右子结点调用递归函数,取三者最大值,相当于对二叉树进行了先序遍历。
int longestConsecutive(TreeNode* root) {
if (!root) return 0;
int res = dfs(root, 1) + dfs(root, -1) + 1;
return max(res, max(longestConsecutive(root->left), longestConsecutive(root->right)));
}
int dfs(TreeNode *root, int diff) {
if (!root) return 0;
int left = 0, right = 0;
if (root->left && root->val - root->left->val == diff) {
left = 1 + dfs(root->left, diff);
}
if (root->right && root->val - root->right->val == diff) {
right = 1 + dfs(root->right, diff);
}
return max(left, right);
}
二叉树中所有距离为 K 的结点
给定一个二叉树(具有根结点 root), 一个目标结点 target ,和一个整数值 K 。返回到目标结点 target 距离为 K 的所有结点的值的列表。 答案可以以任何顺序返回。
示例
输入:root = [3,5,1,6,2,0,8,null,null,7,4], target = 5, K = 2
输出:[7,4,1]
解释:
所求结点为与目标结点(值为 5)距离为 2 的结点,
值分别为 7,4,以及 1
解题思路
解法一:Graph + DFS
在子树中寻找距离为 K 的结点很容易,因为只需要一层一层的向下遍历即可,难点就在于符合题意的结点有可能是祖先结点,或者是在旁边的兄弟子树中,这就比较麻烦了,因为二叉树只有从父结点到子结点的路径,反过来就不行。既然没有,就手动创建这样的反向连接即可,这样树的遍历问题就转为了图的遍历(其实树也是一种特殊的图)。建立反向连接就是用一个 HashMap 来建立每个结点和其父结点之间的映射,使用先序遍历建立好所有的反向连接,然后再开始查找和目标结点距离K的所有结点,这里需要一个 HashSet 来记录所有已经访问过了的结点。
在递归函数中,首先判断当前结点是否已经访问过,是的话直接返回,否则就加入到 visited 中。再判断此时 K 是否为0,是的话说明当前结点已经是距离目标结点为 K 的点了,将其加入结果 res 中,然后直接返回。否则我们分别对当前结点的左右子结点调用递归函数,注意此时带入 K-1,这两步是对子树进行查找。之前说了,还得对父结点,以及兄弟子树进行查找,这是就体现出建立的反向连接 HashMap 的作用了,若当前结点的父结点存在,也要对其父结点调用递归函数,并同样带入 K-1,这样就能正确的找到所有满足题意的点了。
vector<int> distanceK(TreeNode* root, TreeNode* target, int K) {
if (!root) return {};
vector<int> res;
unordered_map<TreeNode*, TreeNode*> graph;
unordered_set<TreeNode*> visited;
buildGraph(root, graph);
dfs(target, K, graph, visited, res);
return res;
}
void dfs(TreeNode *node, int K, unordered_map<TreeNode*, TreeNode*> &graph, unordered_set<TreeNode*> &visited, vector<int> &res) {
if (visited.count(node)) return;
visited.insert(node);
if (K == 0) {
res.push_back(node->val);
return;
}
if (node->left) dfs(node->left, K - 1, graph, visited, res);
if (node->right) dfs(node->right, K - 1, graph, visited, res);
if (graph[node]) dfs(graph[node], K - 1, graph, visited, res);
}
void buildGraph(TreeNode* node, unordered_map<TreeNode*, TreeNode*> &graph) {
if (!node) return;
if (node->left) graph[node->left] = node;
if (node->right) graph[node->right] = node;
buildGraph(node->left, graph);
buildGraph(node->right, graph);
}
解法二:Graph + BFS
直接建立一个邻接链表,即每个结点最多有三个跟其相连的结点,左右子结点和父结点,使用一个 HashMap 来建立每个结点和其相邻的结点数组之间的映射,这样就几乎完全将其当作图来对待了,建立好邻接链表之后,原来的树的结构都不需要用了。既然是 BFS 进行层序遍历,那么就使用队列 queue,还要一个 HashSet 来记录访问过的结点。在 while 循环中,若K为0了,说明当前这层的结点都是符合题意的,那么就把当前队列中所有的结点加入结果 res,并返回即可。否则就进行层序遍历,取出当前层的每个结点,并在邻接链表中找到和其相邻的结点,若没有访问过,就加入 visited 和 queue 中即可。记得每层遍历完成之后,K要自减1。
vector<int> distanceK(TreeNode* root, TreeNode* target, int K) {
if (!root) return {};
vector<int> res;
unordered_map<TreeNode*, vector<TreeNode*>> graph;
buildGraph(nullptr, root, graph);
unordered_set<TreeNode*> visited{{target}};
queue<TreeNode*> q{{target}};
int k = 0;
while (!q.empty() && k <= K) {
int size = q.size();
while (size--) {
TreeNode *parent = q.front(); q.pop();
if (k == K) {
res.push_back(parent->val);
}
for (TreeNode *child : graph[parent]) {
if (visited.count(child)) continue;
visited.insert(child);
q.push(child);
}
}
++k;
}
return res;
}
void buildGraph(TreeNode* parent, TreeNode *child, unordered_map<TreeNode*, vector<TreeNode*>> &graph) {
if (!child || graph.count(child)) return;
if (parent) {
graph[parent].push_back(child);
graph[child].push_back(parent);
}
buildGraph(child, child->left, graph);
buildGraph(child, child->right, graph);
}