导读
这是我写的第一次复盘总结,利用递归法和迭代法解决二叉树相关的题目,里面还会涉及到其他的概念,例如前缀和等等。
递归解法一共分四步:
框架
1.terminator——下探到底该往上返回了,一般会出现在叶子节点的左右子树上。有可能返回0(涉及到求数值的题目),有可能返回false(涉及到判断的题目);
2.process the current logic——每到新的一层,要处理当前层的逻辑(包括最后一句返回,返回的是该层的返回值);
3.drill down——下探,分别下探左右子树(若存在);
4.reverse——回溯,若有全局变量,例如哈希表map等,往上返回时,要把当前层的值或者节点从map里剔除。
迭代解法一共分三步:
框架
1.准备工作:
a.建立栈 / 队列;
b.把根节点压进去;
2.当队列不为空时while(!q.empty()):
a.记录队列当前的大小(因为队列的大小是动态的,后面会有新加进来的),即本层的节点数;
b.for循环遍历里面的每个节点;
c.依次弹出,处理它们;
递归部分
1.二叉树的最大深度 / 树的高度(Leetcode104)
难度:简单Easy
e.g:
0
/ \
1 2
\
3
递归解法
每下探到新的一层要做两件事:
- 获取左右子树的高度left和right(本例中当前节点为[0],需要获取从[1]节点往下的高度和[2]节点往下的高度);
- 左右子树高度的较大值+1(加的1是[0]和[1]之间的连线,或者[0]和[2]之间的连线),把该值往上返回
递归过程:
maxDepth(Node[0]) = 1 + max(maxDepth(Node[1]), maxDepth(Node[2])) -----------------------------------------(1)
maxDepth(Node[1]) = 1 + max(maxDepth(1的左子树), maxDepth(1的右子树)) = 1 + max(0, 0) = 1---------(2)
maxDepth(Node[2]) = 1 + max(maxDepth(2的左子树), maxDepth(Node[3])) = 1 + max(0, maxDepth(Node[3]))–(3)
maxDepth(Node[3]) = 1 + max(maxDepth(3的左子树, maxDepth(3的右子树)) = 1 + max(0, 0) = 1-------------------(4)
(4)式代入(3)式: maxDepth(Node[2]) = 1 + max(0, 1) = 1 + 1 = 2;
(2)式(3)式代入(1)式: maxDepth(Node[0]) = 1 + max(1, 2) = 3;
代码如下
C++版:(求若干个数中最大值的函数是max)
class Solution {
public:
int maxDepth(TreeNode* root) {
// terminator
if(root == NULL) return 0;
else {
// process the current logic(获取)+ drill down(下探)
int left = maxDepth(root -> left);
int right = maxDepth(root -> right);
// process the current logic
return max(left, right) + 1; // 每一层的高度为: 左右子树较大值+1
}
}
};
/*
也可以把最后三句合并成
return max(maxDepth(root -> left), maxDepth(root -> right)) + 1;
*/
Java版:(求两个数中最大值的函数是Math.max)
class Solution {
public int maxDepth(TreeNode root) { // 求最大深度的函数
// terminator
if(root == null) {
return 0;
} else {
// process the current logic(获取)+ drill down(下探)
int left = maxDepth(root.left); // 把root.left当作root,再次调用maxDepth,得到left的值
int right = maxDepth(root.right);
// process the current level
return Math.max(left, right) + 1; // 要往上返回,需要知道left和right的值
}
}
}
Python版:(求若干个数中最大值的函数是max)
class Solution:
def maxDepth(self, root:TreeNode) -> int:
if not root:
return 0
left = self.maxDepth(root.left)
right = self.maxDepth(root.right)
return max(left, right) + 1
"""
也可以把最后三句合并成
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
"""
迭代解法
1.准备工作:
a.建立栈 / 队列;
b.把根节点压进去;
2.当队列不为空时:
C++:while(!q.empty());
Java:while(!q.isEmpty());
Python:while q:
a.记录队列当前的大小(因为队列的大小是动态的,后面会有新加进来的),即本层的节点数;
b.for循环遍历里面的每个节点;
c.依次弹出,处理它们;
C++版
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == NULL) return 0;
deque<TreeNode*> q; // 定义一个队列,从队尾加push_back,从队首弹pop_front
q.push_back(root);
int deep = 0;
while(!q.empty()) // 广度优先遍历的套路:只要队列不为空
{
// 每到新的一层
deep++;
int num = q.size(); // 记录队列当前的大小,因为队列大小是动态变化的,后面还有其他的节点加进来
for(int i = 1; i <= num; i++) // 每一层只处理队列q里num个元素,因为它们属于同一层
{
TreeNode* p = q.front(); // 看一眼
q.pop_front(); // 再弹出去
if(p -> left) q.push_back(p -> left); // 把节点p的左节点加到队列q里
if(p -> right) q.push_back(p -> right);
}
}
return deep;
}
};
Java版
class Solution{
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
LinkedList<TreeNode> q = new LinkedList<>(); // 建队列,把根节点压入
q.add(root); // 把根节点加入队列,也可用offer(更安全)
int deep = 0;
while (!q.isEmpty()) {
deep++;
int num = q.size(); // 获取每一层的节点数
for (int i = 0; i < num; i++) {
TreeNode node = q.pollFirst(); // 弹出队首的节点(也可用remove)
if (node.left != null) { // 若左节点存在
q.add(node.left);
}
if (node.right != null) { // 若右节点存在
q.add(node.right);
}
}
}
return deep;
}
}
Python版
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if root is None:
return 0
q = [root] # 建立队列
deep = 0
while q: # 还是python简单
deep += 1
for i in range(len(q)): # 每次只取当前层的节点个数
p = q.pop(0)
if p.left:
q.append(p.left)
if p.right:
q.append(p.right)
return deep
2.平衡二叉树(Leetcode110)
要判断是否是平衡二叉树需要两步:
1.知道左右子树的高度;
2.判断是否是平衡二叉树;(因为树的高度都是正的,我们用一个记号来标识:"-1",当为-1时不是平衡二叉树,当不为-1时是平衡二叉树)
每下探到新的一层要做两件事:
1.获取左右子树的高度left、right;
2.判断以该节点为根节点是否还是平衡的,若是则计算高度;若不是,向上返回-1;
方法延续了上一题的思路:即求当前节点的高度时用到 max(left,right) + 1,每次判断左右子树的高度差是否大于1了,大于1了则当前根节点置-1,不大于取二者的较大值当作当前节点的深度,代码如下:
C++
class Solution {
public:
bool isBalanced(TreeNode* root) {
return maxDepth(root) != -1;
}
int maxDepth(TreeNode *node) {
if(node == NULL) return 0;
int left = maxDepth(node -> left);
// 这里有个截断机制,若左子树为-1了,就不用判断最下面return中的高度差,直接把当前根节点置-1,否则如果左右子树都为-1,则高度差小于2,当前根节点高度就成0了,不合题意.
if(left == -1) return -1; // 特殊情况
int right = maxDepth(node -> right);
if(right == -1) return -1; // 特殊情况
return abs(left - right) < 2 ? max(left, right) + 1 : -1; // 一般情况
}
};
Python
class Solution:
def isBalanced(self, root:TreeNode) -> bool:
def DFS(root:TreeNode) -> int:
# terminator
if not root:
return 0
# 获取左右子树的高度(Drill down)
left = DFS(root.left)
if left == -1: return -1
right = DFS(root.right)
if right == -1: return -1
# 判断+计算
return max(left, right) + 1 if abs(left - right) < 2 else -1
# 嘿嘿,我会Python的糖衣炮弹写法啦
return DFS(root) != -1
本题有个提前截断机制:如果某个节点的左子树不是平衡树了即left == -1
,则当前节点直接返回-1,不看右子树了,也不计算高度差了,目的是为了防止特殊情况:如果左右子树都不是平衡二叉树(都为-1),那它们的差不大于1,又成平衡二叉树了(捂脸),如下面示例:
[1,2,3,4,null,null,5,6,null,null,7]
1
/ \
2 3
/ \
4 5
/ \
6 7
节点1的左右子树2和3已经不是平衡二叉树了(返回均是-1),显然1也不是平衡树了,但判断1是否是平衡二叉树的时候,高度差小于2(为0),又成二叉树了。。。
3.二叉树的直径 / 两节点的最长路径(LeetCode543)
本题需要额外设一个变量,记录最长路径maxDiameter,最终返回的是这个数,而每层返回两个子树中长度较大的,故要额外设一个下探函数DFS,不能用给定的原函数了
每到新的一层要做三件事:
1.获取左右子树的高度;
2.每层都计算一下以当前节点为根节点的左右子树长度之和,并把结果与maxDiameter比较,若比它大则更新它;
3.返回以当前节点为路过节点,左右子树的较大值+1(与LeetCode104一样,加的1为根节点与子树节点之间的那段长度),因为求最大路径,肯定是要路过当前节点的,不能两条子树都用;
class Solution {
int maxDiameter = 0; // 总路径
public int diameterOfBinaryTree(TreeNode root) {
if(root == null) return 0;
DFS(root);
return maxDiameter;
}
public int DFS(TreeNode node) {
// terminator
if(node == null) return 0; // 到底了,往上返回
// drill down
int left = DFS(node.left); // 第一步:求左右子树的长度
int right = DFS(node.right);
// process the current logic
maxDiameter = Math.max(left + right, maxDiameter); // 第二步,以当前节点为根节点更新一下直径最大值
return Math.max(left, right) + 1; // 第三步:返回以当前节点为路过节点的深度(左右子树较大值+1)
}
}
注意,定义的全局变量maxDiameter在函数外。
4.翻转二叉树(LeetCode226)
本题是in-place的,故不需要返回某个值。
每到新的一层要做两件事:
1.交换左右节点的值(C++和Java中很经典的交换方法,定义一个临时变量tmp,类型与之前的一致),a.右给tmp,b.左给右,c.tmp给左;
2.下探,即交换左子树的左右子树和右子树的左右子树,每次返回当前节点即可。
class Solution {
public TreeNode invertTree(TreeNode root) {
// teminator
if(root == null) {
return null;
}
// process the current logic
TreeNode tmp = root.right; // 交换当前节点的左右子树
root.right = root.left;
root.left = tmp;
// drill down
invertTree(root.left); // 交换当前节点左右子树的左右子树
invertTree(root.right);
return root; // 返回当前节点,此时已经反转完了
}
}
注意到“交换”是在“下探”前的,故交换的时候当前节点左右子树下面的所有节点都跟着一起挪过去了,每层返回当前节点即可,其左右子树都已经交换完毕了。
5.合并二叉树 / 归并两棵树(LeetCode617)
因为涉及到合并,故我们把两个节点的值加到一个树上,假设加到树1上。若有一个树的某一层是空的,则取另一个不为空的节点当作树1的节点。
每到新的一层要干三件事:
1.判断两个节点是否为空:a.节点1或2有一个为空,保留不为空的;b.两个都不为空的,把节点2的值加到节点1上;
2.下探左右子树,参数都为左子树或者都为右子树,返回值当做树1的值;
3.返回当前节点(TreeNode型的,即节点值+结构),即下探函数DFS返回TreeNode型的
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null || root2 == null) {
return root1 == null ? root2 : root1;
}
return DFS(root1, root2);
}
public TreeNode DFS(TreeNode r1, TreeNode r2) {
// 1.先判断两个节点是否为空
// terminator:到底了
if(r1 == null || r2 == null) { // 当有一个为空时,当前节点取不为空的
return r1 == null? r2 : r1;
}
// process the current logic
r1.val += r2.val; // 当两个都不为空时,把值相加
// drill down
r1.left = DFS(r1.left, r2.left); // 下探左右子树,注意返回的是r1的节点
r1.right = DFS(r1.right, r2.right);
// 返回,返回当前节点(TreeNode型的)
return r1;
}
}
6.路径总和(LeetCode112)
idea: 递归法(DFS)
每到新的一层要做三件事:
1.先判断是否是空节点了,是的话置false;
2.再判断是否到叶子节点了,是的话看最后剩的值是否跟叶子节点的值相等.
3.下探:往左右子树下探,同时减去当前节点的值
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) { // 返回的是bool型
// terminator
if(root == null) {
return false;
}
// terminator
if(root.left == null && root.right == null) {
return root.val == targetSum; // 后面的targetSum不是最初的它了,每一层都要少一点
}
// process the current logic
targetSum = targetSum - root.val;
// drill down
return hasPathSum(root.left, targetSum) || hasPathSum(root.right, targetSum);
}
}
7.路径总和 III(LeetCode437)
idea: 前缀和(PreSum)
这种求和个数的并且不要求以根节点开始以叶子节点结束的就可以用前缀和了(即只需要中间的一段)
e.g
叶子节点: 1 5 8 13 3 5
前缀和: 1 6 14 27 30 35 找"8"
即用后面的前缀和减去要找的数字"8",得到的结果在前面的前缀和里查,若有,证明前后前缀和之差就是要找的数字,
例如: 14 - 6 = 8, 35 - 27 = 8
另外,因为涉及到负数节点,所以用哈希表的值记录每个前缀和出现的次数,例如前缀和为14可能有2个:
1 4 7 2 3 -3, 此时"14"这个前缀和出现了两次
本题思路: 每到新的一层
1.求新的前缀和;
2.看一下用新的前缀和减去要求的值在不在map里,在的话把对应的次数加到res里;
3.把新的前缀和以及对应的次数加到map里;
4.左右下探,把每层的结果加到res里(注意区分之前的res和现在的res,之前的res是从上往下探的时候的res,后一个res是从底返回的时候的res)
5.回溯,从底往上返回值的时候,用完了下面一层就应该把结果从map里剔除,不能一直还占着坑,因为已经跟它没关系了.
class Solution {
public int pathSum(TreeNode root, int sum) {
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(0, 1);
return helper(root, map, sum, 0);
}
public int helper(TreeNode root, HashMap<Integer, Integer> map, int sum, int pathSum){
int res = 0;
if(root == null) return 0;
// 1.求新的前缀和
pathSum += root.val;
// 2.看一下用新的前缀和减去要求的值在不在map里,在的话把对应的次数加到res里
res += map.getOrDefault(pathSum - sum, 0);
// 3.把新的前缀和以及对应的次数加到map里
map.put(pathSum, map.getOrDefault(pathSum, 0) + 1); // 每次都保存一下前缀和
res = helper(root.left, map, sum, pathSum) + helper(root.right, map, sum, pathSum) + res;
// reverse回溯
map.put(pathSum, map.get(pathSum) - 1);
return res;
}
}
8.另一个树的子树(LeetCode572)
idea: 深度优先搜索(DFS)
定义一个判断是否是同一棵树的函数,isSametree,里面有两个变量,均为TreeNode型的.
每次都只比较一个节点
class Solution:
def isSubtree(self, s: TreeNode, t: TreeNode) -> bool:
if not s and not t:
return True
if not s or not t:
return False
return self.isSametree(s, t) or self.isSubtree(s.left, t) or self.isSubtree(s.right, t)
# 在判断是否为相同的树的函数里,要判断每个节点都一样:当前节点,当前节点的左子树,当前节点的右子树
def isSametree(self, s: TreeNode, t: TreeNode) -> bool:
if not s and not t:
return True
if not s or not t:
return False
return s.val == t.val and self.isSametree(s.left, t.left) and self.isSametree(s.right, t.right)
9.对称二叉树(LeetCode101)
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return DFS(root.left, root.right);
}
public boolean DFS(TreeNode left, TreeNode right) {
// terminator
if(left == null && right == null) return true;
if(left == null || right == null) return false;
// process the current logic
if(left.val != right.val) return false; // 每次只判断当前节点的值即可
// drill down
return DFS(left.left, right.right) && DFS(left.right, right.left);
}
}
10.二叉树的最小深度(LeetCode111)
idea: 递归法(DFS)
每到新的一层,要做两件事:
1.获取左右子树的高度; 2.返回左右子树较小者+1;
有一种特殊情况要考虑,就是某个节点只有1个子树的情况,此时肯定要计算该子树的高度,但由于一般的逻辑是保留较小高度+1,故不适用在此处,故可以计算"左子树+右子树+1",这样为空的子树高度为0就不用管了.
class Solution:
def minDepth(self, root: TreeNode) -> int:
# terminator
if not root:
return 0
left = self.minDepth(root.left) # 自引要加self
right = self.minDepth(root.right)
# 特殊情况,需要放在一般返回语句的前面(毕竟特殊嘛,不一定每次都执行的)
if not left or not right:
return left + right + 1 # 当瘸腿或者到达叶子节点时
# 一般情况
return min(left, right) + 1
11.左叶子之和(LeetCode404)
idea: 递归法(recursive method)
定义一个判断是否是叶子节点的函数isLeaf,每到新的一层
(特殊情况)判断当前节点的左子树是否是叶子节点,若是则把左子树的值往上返回,不要忘了下探该节点的右子树;
(一般情况)下探当前节点的左右子树,并把结果相加
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
// terminator
if (root == null) return 0;
// process the current logic(特殊情况) + drill down
if (isLeaf(root.left)) return root.left.val + sumOfLeftLeaves(root.right);
return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
}
private boolean isLeaf(TreeNode node){
if (node == null) return false;
return node.left == null && node.right == null; // 左右子树同时为null才是叶子节点
}
}
12.最长同值路径(LeetCode687)
idea: 递归法(DFS)
定义一个全局变量path,最终要返回这个值,因此不能用已经给定的函数当作DFS函数了(因为它要返回别的),所以要额外定义一个DFS函数
e.g:
1
/ \
2 3
/ \ / \
4 5 6 7
每到新的一层,要做4件事:
1.获取左右子树的长度length(左子树:2,4,5; 右子树:3,6,7)
2.看一下左子树与根的值是否一样,一样的话左子树长度+1(算上了1和2之间的长度);
看一下右子树与根的值是否一样,一样的话右子树长度+1(算上了1和3之间的长度);
3.更新下以当前节点为根节点的最大长度path
4.每次返回左右子树长度中较大值(即不以当前节点为根节点来计算)
class Solution {
private int path = 0; // 最终要返回的
public int longestUnivaluePath(TreeNode root) {
DFS(root);
return path; // 因为DFS函数要返回别的,所以不能用主函数作为下探函数了
}
private int DFS(TreeNode root){
if(root == null) return 0;
int left = DFS(root.left); // 获取左子树的长度
int right = DFS(root.right);
// 计算以当前节点为根节点的左边的长度
int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0;
// 计算以当前节点为根节点的右边的长度
int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0;
// 每次都看一下以当前节点为根节点的长度,更新最大长度path
path = Math.max(path, leftPath + rightPath);
// 返回的时候只返回以一边的长度,即把当前节点当作路过节点
return Math.max(leftPath, rightPath);
}
}
13.打家劫舍 III(LeetCode337)
idea: 递归法(DFS)
根据题意,我们把整棵树拆成两部分来偷,第一部分是只偷单数,第二部分只偷双数,
每到新的一层,做两件事:
1.以当前层的节点为根节点,计算偷单数(即节点本身和子树的子树)以及偷双数获得的钱数val1和val2
2.返回二者的较大值
class Solution {
public int rob(TreeNode root) {
// terminator
if(root == null) return 0;
// 计算偷单数的值(包括当前节点本身)
int val1 = root.val;
if(root.left != null) val1 += rob(root.left.left) + rob(root.left.right);
if(root.right != null) val1 += rob(root.right.left) + rob(root.right.right);
// 计算偷双数的值
int val2 = rob(root.left) + rob(root.right);
// 返回二者之间的较大值
return Math.max(val1, val2);
}
}
14.二叉树中第二小的节点(LeetCode671)
idea: 递归法(recursive method)
由题意,一个节点若有子节点,那么该节点的值为两个子节点中较小的一个.为了方便理解,假设所有节点的值都与左节点一样,右节点为较大的那一个.
从根节点开始看起,第二小的节点肯定是右节点
e.g
5
/ \
5 12
此时5为根节点,12为第二小的节点.从第三层开始就不看12的左右子树了,因为肯定都大于等于12.再来看第二层的5这边,从第三层开始,只看它的右子树,因为只有右子树才有可能比5大,并且夹在5和12中间.例如找到了一个节点值为7,此时7为第二小的节点了,因为5 < 7 < 12.
每到新的一层要做两件事:
1.先看有没有左右子树并且是否左右子树不相等,若为真,取较大值bigger,做一次判断:
(特殊情况)当为第二层时,即根节点的左右子树时,最终结果res为较大值bigger,此时保证有第二小的值了;
(一般情况)往左子树下探(右子树都是大值了),当发现更小的bigger时更新res;
2.往较小的子树下探.
class Solution {
int res = -1;
public int findSecondMinimumValue(TreeNode root) {
// terminator
if(root == null) { // 到底了,不用往上返回
return res;
}
// 如果存在子树且值不相等,取较大值bigger,做两种情况判断
if (root.left != null && root.left.val != root.right.val) {
// 获取左右子树中将较大的值
int bigger = root.left.val > root.right.val ? root.left.val : root.right.val;
// 如果返回值没有被更改过(即第二层),则bigger有可能就是第二小的;如果返回值被更改过(其它层),则新的bigger比老的更小,更新res
res = res == -1 ? bigger:Math.min(res, bigger);
// 只往较小的子树下探(因为较大的子树里的值肯定都比res大了)
findSecondMinimumValue(root.left.val > root.right.val ? root.right : root.left);
}
// 如果左右子树相等或为空,分别递归
else{
findSecondMinimumValue(root.left);
findSecondMinimumValue(root.right);
}
return res;
}
}
层次遍历
1.二叉树的层平均值 / 一棵树每层节点的平均数(Leetcode637)
难度:简单Easy
idea: 广度优先遍历(BFS)
在BFS框架上加点东西
- 准备工作:
a.建立栈 / 队列;
b.把根节点压进去; - 正片:当队列不为空时while(!q.empty()):
a.记录队列当前的大小,即本层的节点数num,放在分母上;
(本题新加: 建立一个整形变量sum,用于记录当前层的和)
b.for循环遍历里面的每个节点;
c.挨个弹出,处理它们,(本题新加: 先把节点的值加到sum里),再把左右节点加到队列里
(本题新加: for循环遍历完求平均值: sum / num)
代码:
Java版:
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> queue = new LinkedList<>(); // 准备工作之一
queue.add(root); // 准备工作之二
while (!queue.isEmpty()) {
int num = queue.size();
double sum = 0;
for (int i = 0; i < num; i++) {
TreeNode node = queue.poll();
sum += node.val; // 在框架上新加的
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
}
res.add(sum / num); // 在框架上新加的
}
return res;
}
}
2.找树左下角的值 / 得到左下角的节点(Leetcode513)
idea: 广度优先遍历(BFS)
在BFS模板上进行修改:
准备工作: 1.建; 2.压根;
正片,当队列不为空时: 1.弹; 2.把右节点和左节点分别加到队列里;
因为队列是"先进先出",故先进队列的先弹出,后进队列的后弹出; 每一层的最左边元素是最后一个入队列的,肯定最后一个弹出.
e.g:
1
/ \
2 3
/ \ / \
4 5 6 7
每一层,先压右,再压左; 故先弹右,再弹左.过程如下:
[
1
]
−
>
[
3
,
2
]
−
>
[
2
,
7
,
6
]
−
>
[
7
,
6
,
5
,
4
]
−
>
[
6
,
5
,
4
]
−
>
[
5
,
4
]
−
>
[
4
]
[1] -> [3,2] -> [2,7,6] -> [7,6,5,4] -> [6,5,4] -> [5,4] -> [4]
[1]−>[3,2]−>[2,7,6]−>[7,6,5,4]−>[6,5,4]−>[5,4]−>[4]
代码:
Java版
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>(); // 准备工作,建立链表(队列)
queue.add(root); // 准备工作,建立链表(队列)
while (!queue.isEmpty()) { // 正片
root = queue.poll(); // 弹
if(root.right != null) queue.add(root.right); // 先压右(先入先出嘛)
if(root.left != null) queue.add(root.left);
}
return root.val;
}
}
前中后序遍历二叉树
递归解法
前序:
void dfs(TreeNode root) {
visit(root);
dfs(root.left);
dfs(root.right);
}
中序:
void dfs(TreeNode root) {
dfs(root.left);
visit(root);
dfs(root.right);
}
后序:
void dfs(TreeNode root) {
dfs(root.left);
dfs(root.right);
visit(root);
}
迭代解法
1. 非递归实现二叉树的前序遍历(LeetCode144)
难度:中等Medium
1
/ \
2 3
/ \ / \
4 5 6 7
还是套用迭代的模板:
准备工作:
- 建(栈、结果列表)
- 压根
正片:当栈不为空时while(!stack.isEmpty())
- 弹节点,并将值加到结果列表里;
- 先把右节点压入栈中,再把左节点压入栈中(因为前序:根-左-右,栈:先入后出)
迭代过程:
stack = [1] -> [3,2] -> [3,5,4] -> [3,5] -> [3] -> [7,6] -> [7] -> []
res = [] -> [1] -> [1,2] -> [1,2,4] -> [1,2,4,5] -> [1,2,4,5,3] -> [1,2,4,5,3,6] -> [1,2,4,5,3,6,7]
代码:
Java版
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>(); // 准备工作: 1.建栈
stack.push(root); // 准备工作: 2.压根节点
while (!stack.isEmpty()) { // 正片: 当队列不为空时
TreeNode node = stack.pop(); // 正片: 1.弹节点
if (node == null) continue;
res.add(node.val); // 正片: 2.把节点值加到结果列表里
stack.push(node.right); // 前序:根-左-右; 栈: 先入后出
stack.push(node.left); // 先压右(后出),后压左(先出)
}
return res;
}
}
2. 非递归实现二叉树的中序遍历(LeetCode94)
难度:中等Medium
1
/ \
2 3
/ \ / \
4 5 6 7
还是套用迭代的模板:
准备工作:
- 建(栈、结果列表)
- 压根(没压,定了一个TreeNode型指针cur)
正片:当栈不为空时while(!stack.isEmpty())
- 先处理:一路往左探到底(压当前节点进栈,指针指左子树);
- 弹节点,弹完了把值加到res里,然后指针指右子树;
迭代过程:
stack = [1]->[1,2]->[1,2,4]->[1,2]->[1,5]-> [1] -> [3,6] -> [3] -> [7] -> []
res = [] -> [] -> [] -> [4] ->[4,2]->[4,2,5]->[4,2,5,1]->[4,2,5,1,6]->[4,2,5,1,6,3]->[4,2,5,1,6,3,7]
代码:
Java版
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>(); // 准备工作: 建立结果列表
if (root == null) return res;
Stack<TreeNode> stack = new Stack<>(); // 准备工作: 建栈
TreeNode cur = root; // 没压栈,而是定义了一个指针
while (cur != null || !stack.isEmpty()) {
// 一直往左子树下探,并且把节点压倒栈里
while (cur != null) {
stack.push(cur); // 注意到第一个cur是root
cur = cur.left;
}
// 探到底之后,开始弹出节点
TreeNode node = stack.pop();
res.add(node.val); // 先弹出一个节点并把值加到res里
cur = node.right; // 只要弹出的节点有右子树,就往右子树下探
}
return res;
}
}
3.非递归实现二叉树的后序遍历(LeetCode145)
难度:中等Medium
idea: 广度优先遍历(BFS)
前序遍历: root -> left -> right; 变体: root -> right -> left, 即先压右再压左
后序遍历: left -> right -> root;
故只要按前序遍历变体去压节点,然后再翻过来,就是后序遍历了.
还是利用栈(先入后出),根据前序遍历那题的方法,要想先遍历左,再遍历右,就需要先把右压进栈,再把左压进栈;
同理本题遍历顺序是先右后左,故先把左压进栈,再把右压进栈.
还是套用广度优先遍历的模板
1.准备工作: 建栈/队列; 压根(因为前序遍历压根了,所以这里也有);
2.正片: 当队列不为空时,弹,先压左,再压右;
3.翻转;
代码:
Java版
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(root); // 因为前序压根了,而这里只是对前序换了个顺序,故也压根
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node == null) continue; // 或者if(node){下面三行}
res.add(node.val); // 遍历顺序: 根-右-左, 故压栈顺序是先左后右
stack.push(node.left);
stack.push(node.right);
}
Collections.reverse(res);
return res;
}
}
二叉查找树(BST)
定义:根节点大于等于左子树所有节点,小于等于右子树所有节点(注意“所有”二字)。二叉查找树中序遍历有序。
1.修剪二叉搜索树 / 修剪二叉查找树(LeetCode669)
idea: 深度优先遍历(DFS)
套用递归模板:
1.terminator: 当根节点为空时,返回null
2.process the current logic:
当根节点值小于low时,左子树就不看了,只看右子树;
当根节点值大于right时,右子树就不看了,只看左子树;
3.drill down:
往左子树递归: 即把左节点的值当做根节点递归,最后用左子树接住;
往右子树递归: 即把右节点的值当做根节点递归,最后用右子树接住;
每层返回根节点即可(因为每层我们只关心根节点)
代码:
C++版
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (root == nullptr ) return nullptr;
if (root->val < low) {
TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点
return right;
}
if (root->val > high) {
TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点
return left;
}
root->left = trimBST(root->left, low, high); // 用root->left接住符合条件的左孩子
root->right = trimBST(root->right, low, high); // 用root->right接住符合条件的右孩子
return root;
}
};
Java版
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
if (root == null) return null; // terminator
if (root.val > high) return trimBST(root.left, low, high); // process the current
if (root.val < low) return trimBST(root.right, low, high); // process the current
root.left = trimBST(root.left, low, high); // drill down
root.right = trimBST(root.right, low, high); // drill down
return root;
}
}
2.二叉搜索树中第K小的元素 / 寻找二叉查找树的第 k 个元素(LeetCode230)
idea: 递归法中序遍历
由于二叉搜索树的中序遍历序列为递增序列,因此可中序遍历二叉搜索树,返回第K个元素
直接在递归模板上改:
- 先下探左子树;
- 计数器+1,并判断其值是否与k相等了;
- 下探右子树;
定义一个下探函数inOrder,没有返回值
代码:
递归法
class Solution {
private int count = 0; // 全局变量cnt
private int val; // 全局变量val
public int kthSmallest(TreeNode root, int k) {
inOrder(root, k);
return val;
}
private void inOrder(TreeNode node, int k) {
// terminator
if (node == null) return;
// 按照递归遍历二叉树的顺序: 1.左; 2.处理根; 3.右
inOrder(node.left, k);
count++; // 把当前节点node计入总数count里
if (count == k) {
val = node.val; // 注意到val是全局变量
return;
}
inOrder(node.right, k);
}
}
idea: 迭代法中序遍历
套用迭代模板:
1.准备工作:
a. 建: 栈/队列/指针;
b. 压入栈/指向根节点;
2. 正片(当栈不为空|指针不为空时)
(对于每一个有左子树的根节点,都要往下探到底)
弹出栈顶节点,进行处理: 先判断是否是第k小的值了,若不是且存在右子树,就先往右子树走;
迭代法
C++版
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
stack<TreeNode *> stack; // 准备工作: 建栈 and 指针
TreeNode *cur = root;
while(cur || !stack.empty())
{
while(cur) // 对于每一个有左子树的根节点
{
stack.push(cur);
cur = cur->left; // 都要往下探到底
}
cur = stack.top();
if(--k == 0)
return cur->val;
stack.pop();
cur = cur->right; // 因为当前节点右子树的值肯定小于其父亲节点,故先往右子树探
}
return -1;
}
};
3.把二叉搜索树转换为累加树 / 把二叉查找树每个节点的值都加上比它大的节点的值(LeetCode538)
idea: 深度优先遍历(DFS)
直接在DFS模板上进行修改,因为只加比其大的节点值,故对于任意一个节点,需要加:
1.其右子树上的值;
2.其父亲节点上的值;
3.其父亲节点右子树上的值;
利用DFS,先往右下探到底,然后再往上一层一层返回,再往左下探
定义一个全局整型变量sum,从最右下角的节点值开始逐渐累加,往上走每个节点都需要加上累加值sum
e.g
4
/ \
1 6
/ \ / \
0 2 5 7
因为遍历顺序是右-根-左,故具体到本例中的顺序是:
7 - 6(此时右节点7已经遍历完) - 5(此时父亲节点6和右子树7已经遍历完) - 4(此时右子树5,6,7已经遍历完) - 2 - 1 - 0
递归法
代码:
Java版
class Solution {
private int sum = 0; // 全局变量sum,按顺序每个节点都需要加上其值
public TreeNode convertBST(TreeNode root) {
traver(root);
return root; // 返回的是二叉树,故只需要返回根节点即可
}
private void traver(TreeNode node) {
// terminator
if (node == null) return;
// 遍历顺序: 右 - 根 - 左
traver(node.right);
sum += node.val; // 先加上当前节点的值,再赋给当前节点
node.val = sum;
traver(node.left);
}
}
4.二叉搜索树的最近公共祖先 / 二叉查找树的最近公共祖先(LeetCode235)
idea: 深度优先遍历(DFS)
从根节点开始下探, 分三步:
1.如果当前节点node的值小于p和q的值,那么node可以作为p和q的祖先,但是不是最近祖先,往右子树下探还有更合适的;
2.同理若当前节点node的值大于p和q的值,那么node可以作为p和q的祖先,但是不是最近祖先,往左子树下探还有更合适的;
3.若node.val正好介于p和q的值中间,那么node就是最近公共祖先,返回它即可;
e.g
4
/ \
1 6
/ \ / \
0 2 5 7
\ \
3 8
p = 7, q = 8, 先从4开始下探:
4 < 7 & 4 < 8,则虽然4可以作为7和8的公共祖先,但是往右下走会有更合适的
代码:
Java版
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
return root; // 若当前root.val正好介于p.val和q.val之间,则最近公共祖先就是root
}
}
5.二叉树的最近公共祖先(LeetCode236)
idea: 深度优先遍历(DFS)
跟上一题235二叉搜索树的公共祖先不同,本题无法根据节点大小决定搜索方向,故从根节点遍历整棵树
terminator是当前节点为空,或者遇到了p或q的其中一个时,此时返回当前节点node即可;
其他情况,递归获取当前节点的左右子树left和right,若其中一个为空,则返回另外一个; 若都不为空,返回当前节点;
代码:
Java版
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
return left == null ? right : right == null ? left : root;
}
}
6.将有序数组转换为二叉搜索树 / 从有序数组中构造二叉查找树(LeetCode108)
idea: 深度优先遍历(DFS)
定义一个构造二叉搜索树的函数toBST,参数为数组的起点和终点下标,分为3步:
- 找数组中间值下标;
- 以中间值为根,建立一个节点;
- 下探建立左右子树;
代码:
Java版
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return toBST(nums, 0, nums.length - 1);
}
private TreeNode toBST(int[] nums, int sIdx, int eIdx){
if (sIdx > eIdx) return null; // 到底了,可能会出现sIdx=eIdx的情况
int mIdx = (sIdx + eIdx) >> 1;
TreeNode root = new TreeNode(nums[mIdx]); // 找到中间下标对应的数,建立根节点
root.left = toBST(nums, sIdx, mIdx - 1); // 同样的方法,建立左右子树
root.right = toBST(nums, mIdx + 1, eIdx);
return root;
}
}
7.有序链表转换二叉搜索树 / 根据有序链表构造平衡的二叉查找树(LeetCode109)
idea: 快慢指针法
- 获取中间值节点(快慢指针法);
- 截断并以中间节点建立根节点;
- 建立左右子树
代码:
Java
class Solution {
public TreeNode sortedListToBST(ListNode head) {
if (head == null) return null;
if (head.next == null) return new TreeNode(head.val);
ListNode preMid = preMid(head); // 快慢指针法
ListNode mid = preMid.next; // 找到中间值节点
preMid.next = null; // 断开链表
TreeNode root = new TreeNode(mid.val);
root.left = sortedListToBST(head);
root.right = sortedListToBST(mid.next);
return root; // 每层返回当前节点t即可
}
// 快慢指针法,找链表中间值
private ListNode preMid(ListNode head) {
ListNode slow = head, fast = head.next;
ListNode pre = head;
while (fast != null && fast.next != null) {
pre = slow;
slow = slow.next;
fast = fast.next.next;
}
return pre;
}
}
8.两数之和 IV - 输入 BST / 在二叉查找树中寻找两个节点,使它们的和为一个给定值(LeetCode653)
idea: 递归法中序遍历 + 双指针夹逼法
- 先中序遍历二叉搜索树,把值存入一个数组里,数组里的数即升序排列;
- 因为数组中的数是有序的,再利用双指针夹逼法,小了左指针前进一步,大了右指针前进一步.
代码:
Java版
class Solution {
public boolean findTarget(TreeNode root, int k) {
List<Integer> nums = new ArrayList<>();
inOrder(root, nums); // 得到升序排列的数组nums
int i = 0, j = nums.size() - 1; // 双指针夹逼
while (i < j) {
int sum = nums.get(i) + nums.get(j);
if (sum == k) return true;
if (sum < k) i++; // 当前和太小了,左端点前进一步
else j--; // 当前和太大了,右端点前进一步
}
return false;
}
// 递归法中序遍历二叉搜索树
private void inOrder(TreeNode root, List<Integer> nums) {
if (root == null) return;
inOrder(root.left, nums);
nums.add(root.val);
inOrder(root.right, nums);
}
}
9.二叉搜索树的最小绝对差 / 在二叉查找树中查找两个节点之差的最小绝对值(LeetCode530)
idea: 中序遍历二叉搜索树
直接在中序遍历模板上修改:
- 定义一个记录最小值的整型变量minDiff,
- 再定义一个下探函数inOrder,遍历顺序为左-根-右,当遍历到根节点时,计算当前节点的值和上一个节点值的差,并更新; 上一个节点是指: 根节点的上一个节点是左孩子, 右孩子的上一个节点是其父亲节点.
代码:
Java版
class Solution {
private int minDiff = Integer.MAX_VALUE;
private TreeNode preNode = null;
public int getMinimumDifference(TreeNode root) {
inOrder(root);
return minDiff;
}
private void inOrder(TreeNode node) {
if (node == null) return;
// 中序遍历二叉树顺序: 左 - 根 - 右
inOrder(node.left);
if (preNode != null) minDiff = Math.min(minDiff, node.val - preNode.val);
preNode = node; // 左子树没有preNode,根节点的preNode为左孩子,右孩子的preNode为父亲节点
inOrder(node.right);
}
}
10.二叉搜索树中的众数 / 寻找二叉查找树中出现次数最多的值(LeetCode501)
idea: 递归法中序遍历二叉搜索树
利用"中序遍历二叉搜索树是上升序列"这个特点,一看到二叉搜索树,就立马想到中序遍历: 左 - 根 - 右,
因为出现次数最多的数可能不止一个,故我们用一个数组maxCntNums接收答案
还是套种递归法中序遍历二叉树的模板, 在遍历"根"节点这一步进行处理, 一共是3步:
- 判断当前节点node是否与前一个相等,若相等,则当前次数curCnt+1; 若不等,当前次数curCnt重新计
- 判断当前次数和最大次数, 分两种情况:
若相等,则出现最多次数的集合maxCntNums又有新成员了;
若大于, 则之前maxCntNums集合里都不是出现次数最多的数字了,需要清空, maxCnt更新, maxCntNums记录新的最大次数对应数字.
代码:
Java版
class Solution {
private int curCnt = 1; // 记录当前数字出现次数
private int maxCnt = 1; // 记录历史最大出现次数
private TreeNode preNode = null;
public int[] findMode(TreeNode root) {
List<Integer> maxCntNums = new ArrayList<>(); // 记录历史最大出现次数的数字
inOrder(root, maxCntNums);
// 把集合list转化为数组
int[] res = new int[maxCntNums.size()];
int idx = 0;
for (int num : maxCntNums) {
res[idx++] = num;
}
return res;
}
private void inOrder(TreeNode node, List<Integer> nums) {
if (node == null) return;
inOrder(node.left, nums);
if (preNode != null) { // 为了跳过第一个数
if (preNode.val == node.val) curCnt++; // 还是之前的数
else curCnt = 1; // 换新的数了,curCnt重新计
}
// 又有新的更多出现次数的数字了,之前maxCntNums里面需要清空了
if (curCnt > maxCnt) {
maxCnt = curCnt; // 新的最大次数
nums.clear();
nums.add(node.val); // 新的最大次数对应的数字
} else if (curCnt == maxCnt) { // 还是之前的数字,又有一个达到最大次数的数字加到maxCntNums里来了
nums.add(node.val);
}
preNode = node;
inOrder(node.right, nums);
}
}