Leetcode记录库数据结构篇之二:树
- 结论
- 1 104. 二叉树的最大深度
- 2 110. 平衡二叉树
- 3 543. 二叉树的直径
- 4 226. 翻转二叉树
- 5 617. 合并二叉树
- 6 112. 路径总和
- 7 437. 路径总和 III
- 8 572. 另一棵树的子树
- 9 101. 对称二叉树
- 10 111. 二叉树的最小深度
- 11 404. 左叶子之和
- 12 687. 最长同值路径
- 13 337. 打家劫舍 III
- 14 671. 二叉树中第二小的节点
- 15 637. 二叉树的层平均值
- 16 513. 找树左下角的值
- 17 144. 二叉树的前序遍历
- 18 94. 二叉树的中序遍历
- 19 145. 二叉树的后序遍历
- 20 669. 修剪二叉搜索树
- 21 230. 二叉搜索树中第K小的元素
- 22 538. 把二叉搜索树转换为累加树
- 23 235. 二叉搜索树的最近公共祖先
- 24 236. 二叉树的最近公共祖先
- 25 108. 将有序数组转换为二叉搜索树
- 26 109. 有序链表转换二叉搜索树
- 27 653. 两数之和 IV - 输入 BST
- 28 530. 二叉搜索树的最小绝对差
- 29 501. 二叉搜索树中的众数
- 30 116. 填充每个节点的下一个右侧节点指针
- 31 117. 填充每个节点的下一个右侧节点指针 II
- 32 剑指 Offer 32 - III. 从上到下打印二叉树 III
- 33 剑指 Offer 26. 树的子结构
- 34 剑指 Offer 36. 二叉搜索树与双向链表
- 35 剑指 Offer 07. 重建二叉树
- 36 剑指 Offer 33. 二叉搜索树的后序遍历序列
- 37 剑指 Offer 37. 序列化二叉树
- 38 662. 二叉树最大宽度
- 39 124. 二叉树中的最大路径和
- 40 129. 求根节点到叶节点数字之和
- 41 450. 删除二叉搜索树中的节点
- 42 98. 验证二叉搜索树
- 43 337. 打家劫舍 III
- end
结论
一些很主观的东西,归根到底可能还是自己的实力不够:
1.面对和树有关的题目,解法分为两大类:递归和非递归。树是递归定义的数据结构,所以大部分有关树的问题使用递归会比较容易。像一些很明显需要递归的处理树中节点的问题,第一时间就能想到使用递归的方法,比如9 101. 对称二叉树,而有的题目却并不是很明显,比如10 111. 二叉树的最小深度。(但是我在写这道题时,受到了第一道题1 104. 二叉树的最大深度的影响,一直想着用递归方法了,虽然做出来了,但是时空间复杂度都很高)
2.递归的方式如果想到了正确的方法确实写起来简单,而有时这个想法有时却很难想得到,而且递归的解法在时空间复杂度上大多都不是最优的,所以不要过分依赖递归方式,有时普通非递归方法不仅解法容易想到,而且时空间复杂度还会更优。
3.递归三部曲:1.确定递归函数的参数和返回值;2.确定终止条件;3.确定单层递归的逻辑
对于递归,我们要想中断递归过程,一般都要借助于类中的成员变量作为标志,而检测这个标志是否发生了变化的代码所处的位置,应该写到对单个节点进行操作的代码段之前,换言之也就是可能会再次导致标志发生变化的代码之前。或者说在进行单层递归逻辑之前进行标志的判断。
1.BTS(Binary Search Tree)目的是为了提高查找的性能,其查找在平均和最坏的情况下都是logn级别,接近二分查找。其特点是:
每个节点的值大于其任意左侧子节点的值,小于其任意右节点的值。
中序遍历的结果便是树中数据的递增数列。所以我们在解题时,可以同样思考一下题目中的要求如果是针对列表提出的话,该怎么解决?这样我们就有了两种思路。例如第27、28、29三道题。
1 104. 二叉树的最大深度
思路描述
- 树的的东西忘得差不多了,深度应该是要用层次遍历来求。
- 使用队列来存储树的节点
- 先出队,然后判断当前队列是否为空,空的话说明遍历完了一层level++
- 不为空自左向右将叶子节点存到队列中。
- 每遍历完一层level++
代码实现
- 原始C语言实现:
int maxDepth(struct TreeNode* root){
if(root == NULL) return 0;
struct TreeNode *Q[5841], *p;// 经过我的测试,用例中树节点最多为5841.
int front = 0, rear = 0, level = 0, last;
Q[rear++] = root;
last = rear;
while(front != rear){
p = Q[front++];
if(p->left != NULL) Q[rear++] = p->left;
if(p->right != NULL) Q[rear++] = p->right;
if(front == last){
level++;
last = rear;
}
}
return level;
}
java代码:
class Solution {
private int level = 0;
public int maxDepth(TreeNode root) {
if(root == null) return 0;
// 层次遍历
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
q.offer(null); // 层结束标志
while(!q.isEmpty()){
root = q.poll();
if(root != null){
// visit(root);
if(root.left != null) q.offer(root.left);
if(root.right != null) q.offer(root.right);
}else{
level++;
if(!q.isEmpty()){
q.offer(null);
}
}
}
return level;
}
}
注意事项
- 因为要使用java的Queue类所以不能像原来C语言那样写了。
- 初始化时就在队列中根节点的后面加上一个“null”作为一层结束的标志,后续出队后发现为“null”但是队列还不空,说明遍历完了一层,执行level++。
- 上一层遍历完了,说明上一层的子节点也都已入队,所以入队一个“null”作为一层的结束标志。
拓展延伸
- 递归的写法
思路:
1.确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型。
代码如下:
int getDepth(TreeNode* root)
2.确定终止条件:如果为空节点的话,就返回0,表示高度为0。
代码如下:
if (root == NULL) return 0;
3.确定单层递归的逻辑:先求它的左子树的深度,再求的右子树的深度,最后取左右深度最大的数值 再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。
代码如下:
int leftDepth = getDepth(rot.left); // 左
int rightDepth = getDepth(root.right); // 右
int depth = 1 + max(leftDepth, rightDepth); // 中
return depth;
整体代码如下:
class Solution {
public int maxDepth(TreeNode root) {
return getDepth(root);
}
private int getDepth(TreeNode root){
if(root == null) return 0;
int leftDepth = getDepth(root.left); // 左
int rightDepth = getDepth(root.right); // 右
int depth = 1 + Math.max(leftDepth, rightDepth); // 中
return depth;
}
}
简洁版:
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
另一种非递归写法:
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> q = new LinkedList<>();
int num, i, level = 0; // num用来暂存一层有多少个节点;i用来计数
q.offer(root);
while(!q.isEmpty()){
num = q.size(); // 当前队列的size正好是这一层节点的数量
for(i = 0; i < num; i++){
root = q.poll();
// visit(root);
//System.out.print(root.val + " ");
if(root.left != null) q.offer(root.left);
if(root.right != null ) q.offer(root.right);
}
level++; // 层数加加
}
return level;
}
}
2 110. 平衡二叉树
思路描述
- 试试递归:
- 1.确定递归函数的参数和返回值:返回值(int)用于判断记录左右子树的深度,参数(root)
- 2.确定终止条件:如果左右子树的高度相差大于1,或者节点为空。
- 3.确定单层递归逻辑:如果root为空,返回0;不为空,获取当前节点左右子树的深度,判断是否是平衡二叉树,不是的话修改flag,然后跳出递归。
代码实现
java代码:
class Solution {
private boolean flag = true;
public boolean isBalanced(TreeNode root) {
// 1.确定递归函数的参数和返回值;2.确定终止条件;3.确定单层递归的逻辑
getFlag(root);
return flag;
}
// 递归函数
private int getFlag(TreeNode root){
// 跳出递归
if(flag == false) return -1;
// 计算树的深度
if(root == null) return 0;
// 递归
int l = getFlag(root.left);
int r = getFlag(root.right);
// 设置平衡标志
if(Math.abs(l - r) > 1) flag = false;
return 1 + Math.max(l, r);
}
}
注意事项
- 注意事项
拓展延伸
- 这道题也是典型的使用递归会很简单,非递归很难的题目。好像不用递归还做不出来?
- 我想到的非递归算法就是:遍历每一个节点,计算每一个节点的左右子树的深度,并判断是否是平衡,但其实这样的想法和递归的思路是一样的……
3 543. 二叉树的直径
思路描述
- 二叉树的直径被定义为任意两个节点间路径长度的最大值。 那么就变成了求每个节点左右深度之和中最大的那个值。
代码实现
java代码:
class Solution {
private int length = 0;
public int diameterOfBinaryTree(TreeNode root) {
getLength(root);
return length;
}
private int getLength(TreeNode root){
if(root == null) return 0;
int l = getLength(root.left);
int r = getLength(root.right);
length = (l + r) > length ? (l + r): length;
return 1 + Math.max(l, r);
}
}
注意事项
- 注意事项
拓展延伸
- 非递归,遍历每个节点,并记录每个节点的左右子树深度,最后再遍历一次树,求左右子树深度相加最大的值。
4 226. 翻转二叉树
思路描述
- 递归的处理每一个节点,交换左右子树,这有什么难的?
- 这个问题是受到 Max Howell 的 原问题 启发的 :谷歌:我们90%的工程师使用您编写的软件(Homebrew),但是您却无法在面试时在白板上写出翻转二叉树这道题,这太糟糕了。蛤?这怎么能写不出来???我都能写出来
代码实现
java代码:
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null || (root.left == null && root.right == null)) return root;
TreeNode swap = root.right;
root.right = root.left;
root.left = swap;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
注意事项
- 注意事项
拓展延伸
- 更简洁的写法:
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
TreeNode left = root.left; // 后面的操作会改变 left 指针,因此先保存下来
root.left = invertTree(root.right);
root.right = invertTree(left);
return root;
}
}
- 非递归算法:遍历原树,原树的左节点赋值给的右节点,右节点赋值给左节点。
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
Deque<TreeNode> queue = new LinkedNode<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return root;
}
}
5 617. 合并二叉树
思路描述
第一种想法:
- 很自然的递归写法:两棵树的节点都存在的话,节点值相加作为新的节点值;如果有一个节点不存在将节点返回连到树上。
第二种想法:
- 先序遍历两棵树,同时存在的节点节点值相加,否则覆盖到第一棵树上。
- 非递归的遍历两棵树,同时存在的节点节点值相加,否则覆盖到第一棵树上。(也不好做)
- 有了,我把他们都遍历出来,存到一个数组里,然后对应的位置相加。(但是这样空间复杂度就上来了)
代码实现
java代码:
第一种:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null || root2 == null) return root1 == null ? root2 : root1;
root1.val = root1.val + root2.val;
root1.left = mergeTrees(root1.left, root2.left);
root1.right = mergeTrees(root1.right, root2.right);
return root1;
}
}
第二种:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
// 根节点处理
root1 = traversal(root1, root2);
return root1;
}
private TreeNode traversal(TreeNode root1, TreeNode root2){
// 预存节点
TreeNode temp = new TreeNode();
if(root1 != null && root2 != null){// 双非空节点,可以继续进行递归
temp.val = root1.val + root2.val;
}else if(root1 != null && root2 == null){// root2为空,没必要继续递归了,直接返回root1的后续结构即可
return root1;
}else if(root1 == null && root2 != null){// root1为空,没必要继续递归了,把root2的后续结构赋值给root1后返回即可
temp = root2;
return temp;
}else if(root1 == null && root2 == null){// 双空节点,直接返回
return root1;
}
// 递归
temp.left = traversal(root1.left, root2.left);
temp.right = traversal(root1.right, root2.right);
return temp;
}
}
注意事项
- 注意事项
拓展延伸
- 简洁的写法
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null) return null;
if (t1 == null) return t2;
if (t2 == null) return t1;
TreeNode root = new TreeNode(t1.val + t2.val);
root.left = mergeTrees(t1.left, t2.left);
root.right = mergeTrees(t1.right, t2.right);
return root;
}
}
6 112. 路径总和
思路描述
- 简单,递归遍历相加,判断是否有一刻sum == targetSum,有的话立即返回。
代码实现
java代码:
class Solution {
private boolean flag = false;
public boolean hasPathSum(TreeNode root, int targetSum) {
int sum = 0; // 计数
getSum(root, sum, targetSum);
return flag;
}
private boolean getSum(TreeNode root, int sum, int targetSum){
if(root == null) return flag; // 树为空了,返回
sum += root.val; // 加和
if(sum == targetSum && root.left == null && root.right == null){ // 判断是否满足了要求
flag = true;
return flag;
}
// 递归
getSum(root.left, sum, targetSum);
if(flag) return flag; // 递归减枝
getSum(root.right, sum, targetSum);
return flag;
}
}
注意事项
- 注意事项
拓展延伸
- 另一种思路,减法的思想。一个一个节点的减下去,直到遇到一个叶子节点的值等于targetSum。
- 左子树和右子树只要有一个能够满足条件即可。
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
if (root.left == null && root.right == null && root.val == targetSum) return true;
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
}
7 437. 路径总和 III
思路描述
- 和上一道类似,不过这一次我们需要遍历所有节点,让所有节点都作为起始节点进行一次计算。
- 用一个全局变量进行记录路径数目。
- 终止条件为将所有的节点都遍历一遍。
- 时间复杂度n2,空间复杂度n
代码实现
java代码:
class Solution {
private int pathnums = 0;
public int pathSum(TreeNode root, int targetSum) {
if(root == null) return pathnums; // 如果当前节点是空,那么下面的步骤也没有必要进行了
// getPath()用于计算sum
getPath(root, targetSum);
// 遍历树中每一各节点,每一各节点都做为“根节点”进行一次遍历
pathSum(root.left, targetSum);
pathSum(root.right, targetSum);
return pathnums;
}
private void getPath(TreeNode root, int targetSum){
if(root == null) return; // 如果当前节点是空,那么下面的步骤也没有必要进行了
if(targetSum == root.val) pathnums++; // 如果找到一条路径pathnums++
targetSum -= root.val; // 使用减法,每过一个节点targetSum就减少一个节点的值
// 遍历当前节点以下的节点,求解path
getPath(root.left, targetSum);
getPath(root.right, targetSum);
}
}
思路描述
- 引入前缀和的概念,然后递归 + 回溯
- 动态规划空间换时间。
- 时间复杂度n,空间复杂度n
class Solution {
Map<Integer, Integer> map = new HashMap<>();
int target;
public int pathSum(TreeNode root, int targetSum) {
map.put(0, 1); // 如果树为空,那么一定有一个前缀和为0的路径
target = targetSum; // 目标前缀和
return getPathSum(root, 0);
}
private int getPathSum(TreeNode root, int curSum){
if(root == null) return 0;
curSum += root.val; // 得到当前节点的前缀和
int ret = 0; // 结果
/**
* 子节点的前缀和 - 祖先节点的前缀和 = target, 应该是我们想要的结果
* 现在我们用刚刚求出的子节点的前缀和 - target = 祖先节点的前缀和,
* 然后在Map中查找符合要求的祖先节点前缀和的路径有几条。就得出了我们的结果。
*/
ret = map.getOrDefault(curSum - target, 0); // 得到我们想要的前缀和路径的数目
map.put(curSum, map.getOrDefault(curSum, 0) + 1); // 更新当前前缀和对应的路径数的值
// 向左右遍历
ret += getPathSum(root.left, curSum);
ret += getPathSum(root.right, curSum);
map.put(curSum, map.get(curSum) - 1); // 更新当前前缀和对应的路径数的值,否则可能会在另一棵子树中计算到另一棵子树的前缀和的值。因为我们不能跨子树获取路径
return ret;
}
}
注意事项
- 注意事项
拓展延伸
- 和我的方法本质一样,不过还是类似上一道的减法的思想,一个一个节点的减下去,直到遇到一个叶子节点的值等于targetSum。确实这样可以少申请一个变量来存数。
- 不过这个用时和空间量都比我的大。
class Solution {
public int pathSum(TreeNode root, int targetSum) {
if (root == null) return 0;
int ret = pathSumStartWithRoot(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum);
return ret;
}
private int pathSumStartWithRoot(TreeNode root, int sum) {
if (root == null) return 0;
int ret = 0;
if (root.val == sum) ret++;
ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val);
return ret;
}
}
8 572. 另一棵树的子树
思路描述
- 我的想法是想把subRoot遍历一下,将他的节点相关值都存下来。然后再递归的遍历root的所有节点,查看是否和subRoot的节点一样。(这个想法好像比较难,涉及到字符串匹配,以及树形状的判断。)
- 新想法:先遍历一个subRoot的节点,然后遍历root,一旦发现一个节点的值和subRoot节点1的值相同了,在同步遍历两棵树,查看两棵子树的形状是否一样。
代码实现
java代码:
class Solution {
boolean flag = false; // 标志是否找到了相同子树
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (flag) {return flag;} // 一旦flag变成true,直接返回
if (subRoot == null) {
return true; // 如果subRoot为空直接返回true
}
if (root == null) {
return false;
}
flag = judge(root, subRoot);
isSubtree(root.left, subRoot); // 遍历root树
isSubtree(root.right, subRoot);
return flag;
}
private boolean judge(TreeNode root, TreeNode subRoot) {
if (root == null && subRoot == null) return true; // 如果当前两个树上的节点都遍历完了,返回true
if (root != null && subRoot != null && root.val == subRoot.val) { // 如果两个节点都不是空而且节点值相同
return judge(root.left, subRoot.left) && judge(root.right, subRoot.right); // 继续判断两棵树的左右子节点,并返回求逻辑与,要求左右两棵子树都要递归相同
}
return false; // 不满足上述条件的话直接返回false
}
}
注意事项
- 1.这一道题和上一道一样,涉及到两层递归。
- 2.第一层用来遍历,第二层用于条件的判断。
拓展延伸
- 拓展延伸
class Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root == null) return false;
return testSubTree(root, subRoot) || isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
private boolean testSubTree(TreeNode root, TreeNode subRoot){
if(root == null && subRoot == null) return true;
if(root == null || subRoot == null) return false;
if(root.val != subRoot.val) return false;
return testSubTree(root.left, subRoot.left) && testSubTree(root.right, subRoot.right);
}
}
9 101. 对称二叉树
思路描述
- 1.根节点自己肯定是镜像对称的,所以只需要判断左右节点是否是对称的。
- 2.镜像对称:根节点的左子树的左节点等于右子树的右节点。(左右互换)
- 3.遍历左右子树的节点,其中每一个子节点都满足条件2.
代码实现
java代码:
class Solution {
public boolean isSymmetric(TreeNode root) {
// 特殊情况处理
if(root == null || (root.left == null && root.right == null)) return true;
// 判断根节点的左右子树是否镜像对称
return testSymmetric(root.left, root.right);
}
private boolean testSymmetric(TreeNode l, TreeNode r){
// 条件判断
if(l == null && r == null) return true;
if(l == null || r == null) return false;
if(l.val != r.val) return false;
// 每一个节点都需要满足要求
return testSymmetric(l.left, r.right) && testSymmetric(l.right, r.left);
}
}
注意事项
- 和上一道题几乎一摸一样
拓展延伸
- 拓展延伸
10 111. 二叉树的最小深度
思路描述
- 1.递归的处理每一个节点,当一个节点的左子树为空但右子树不为空,或者反过来时,就取左右子树不为空的那一个子树的深度。
- 2.否则返回左右子树深度中较小的一个。
代码实现
java代码:
class Solution {
public int minDepth(TreeNode root) {
// 特殊情况处理
if (root == null) return 0;
// 遍历节点,并求当前节点的左右子树深度
int left = minDepth(root.left);
int right = minDepth(root.right);
// 如果有一个子树的深度为零,就取另一个子树的深度
if (left == 0 || right == 0) return left + right + 1;
// 否则返回左右子树中较小的深度
return Math.min(left, right) + 1;
}
}
思路描述
- 非递归方式
- 1.广度优先遍历这棵树,当遇到第一个叶子节点时,返回它的深度。
代码实现
java代码:
class Solution {
public int minDepth(TreeNode root) {
// 特殊情况处理
if(root == null) return 0;
Deque<TreeNode> queue = new LinkedList<>();// 用于暂存节点的队列
TreeNode tmp; // 用于节点的变量
queue.offer(root); // 根节点入队
queue.offer(null); // 层结束标志
int minDepth = 1; // 当前最小深度
// 开始层次(广度)遍历树
while(!queue.isEmpty()){
tmp = queue.poll(); // 出队暂存
// 如果当前节点是空节点,说明一层结束
if(tmp == null) {
minDepth++; // 最小深度增加
queue.offer(null); // 新一层的结束标识
tmp = queue.poll(); // tmp重新赋值
}
// 如果第一次遇到了叶子节点,直接返回当前的最小深度值
if(tmp.left == null && tmp.right == null) return minDepth;
// 否则非树中的空节点入队,继续遍历
if(tmp.left != null) queue.offer(tmp.left);
if(tmp.right != null) queue.offer(tmp.right);
}
// 其实这个return根本不会执行
//System.out.print("test");
return minDepth;
}
}
注意事项
- 注意事项
拓展延伸
- 递归的方式如果想到了正确的方法确实写起来简单,但是这个想法有时却很难想得到,而且递归的算法在时空间复杂度上都不是最优的。
- 所以不要过分依赖递归方式,有时普通非递归方法不仅方法容易想到,而且时空间复杂度还会更优。
- 尝试一下把以前做过的题目都用非递归方式解决一下。
11 404. 左叶子之和
思路描述
- 这不是很简单,递归或者非递归的遍历树,将左节点的值相加起来。
代码实现
java代码:
递归:
class Solution {
private int sum = 0;
public int sumOfLeftLeaves(TreeNode root) {
// 递归
if(root == null) return sum;
if(root.left != null && root.left.left == null && root.left.right == null){ // 这个判断是关键其他都简单
sum += root.left.val;
}
sumOfLeftLeaves(root.left);
sumOfLeftLeaves(root.right);
return sum;
}
}
非递归前序遍历:
class Solution {
private int sum = 0;
public int sumOfLeftLeaves(TreeNode root) {
// 非递归前序遍历
if(root == null) return sum;
Stack<TreeNode> s = new Stack();
while(!s.isEmpty() || root != null){
while(root != null){
if(root.left != null && root.left.left == null && root.left.right == null){
sum += root.left.val;
}
s.push(root);
root = root.left;
}
root = s.pop();
root = root.right;
}
return sum;
}
}
非递归中序遍历:
class Solution {
private int sum = 0;
public int sumOfLeftLeaves(TreeNode root) {
// 非递归中序遍历
if(root == null) return sum;
Stack<TreeNode> s = new Stack();
while(!s.isEmpty() || root != null){
while(root != null){
s.push(root);
root = root.left;
}
root = s.pop();
if(root.left != null && root.left.left == null && root.left.right == null){
sum += root.left.val;
}
root = root.right;
}
return sum;
}
}
非递归后序遍历:
class Solution {
private int sum = 0;
public int sumOfLeftLeaves(TreeNode root) {
// 非递归后序遍历
if(root == null) return sum;
Stack<TreeNode> s1 = new Stack();
Stack s2 = new Stack();
do{
while(root != null){
s1.push(root);
s2.push(false);
root = root.left;
}
if(!(boolean)s2.pop()){
root = s1.peek().right;
s2.push(true);
}else{
root = s1.pop();
if(root.left != null && root.left.left == null && root.left.right == null){
sum += root.left.val;
}
root = null;
}
}while(!s1.isEmpty());
return sum;
}
}
注意事项
- 注意事项
拓展延伸
树的递归与非递归三种遍历方式:
- 先序:
递归:
class Solution {
public int preOrder(TreeNode root) {
// 先序
if (root == null) return 0;
// 访问节点
// visit(root);
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
return 1;
}
}
非递归:
class Solution {
public int preOrder(TreeNode root) {
// 先序
if (root == null) return 0;
Stack<TreeNode> s1 = new Stack();
while(!s1.isEmpty() || root != null){
while(root != null){
// 访问节点
// visit(root);
System.out.print(root.val + " ");
s1.push(root);
root = root.left;
}
root = s1.pop();
root = root.right;
}
return 1;
}
}
- 中序:
递归:
class Solution {
public int preOrder(TreeNode root) {
// 中序
if (root == null) return 0;
preOrder(root.left);
// 访问节点
// visit(root);
System.out.print(root.val + " ");
preOrder(root.right);
return 1;
}
}
非递归:
class Solution {
public int inOrder(TreeNode root) {
// 中序
if (root == null) return 0;
Stack<TreeNode> s1 = new Stack();
while(!s1.isEmpty() || root != null){
while(root != null){
s1.push(root);
root = root.left;
}
root = s1.pop();
// 访问节点
// visit(root);
System.out.print(root.val + " ");
root = root.right;
}
return 1;
}
}
- 后序:
递归:
class Solution {
public int preOrder(TreeNode root) {
// 后序
if (root == null) return 0;
preOrder(root.left);
preOrder(root.right);
// 访问节点
// visit(root);
System.out.print(root.val + " ");
return 1;
}
}
非递归:
class Solution {
public int postOrder(TreeNode root) {
// 后序
if (root == null) return 0;
Stack<TreeNode> s1 = new Stack(); // 暂存节点
Stack s2 = new Stack(); // 暂存访问标志
do{
while(root != null){
s1.push(root); // 节点进站暂存
s2.push(false); // 设置右节点为未访问
root = root.left; // 向左下遍历
}
if(!(boolean)s2.pop()){ // 这个表示当前节点的右子节点未被访问过,那么就访问右子节点
root = s1.peek().right;
s2.push(true); // 并且修改当前这个节点的访问标志,表示这个节点的左右节点都已经被访问过了
}else{ // 如果当前节点的左右节点都被访问过了,说明接下来该访问根节点了
root = s1.pop(); // 出栈
// 访问节点
// visit(root);
root = null; // 将当前节点置空,那么就会继续判断栈顶结点的状态
}
}while(!s1.isEmpty());
return 1;
}
}
可以看出后序遍历方法比较特殊。
12 687. 最长同值路径
思路描述
- 遍历每一个节点,判断左右子树中具有的相同节点间的路径长度。
- 左右求得的路径长度相加,然后求最大的值。
- 左右子树的路程长度是左右子树中左右子树分支的最大连续长度。
代码实现
java代码:
双递归:
class Solution {
private int maxRouteLengeth = 0;
public int longestUnivaluePath(TreeNode root) {
if (root == null) return 0;
visit(root); // 判断节点左右深度
// 递归遍历每一个节点。
longestUnivaluePath(root.left);
longestUnivaluePath(root.right);
return maxRouteLengeth;
}
private int visit(TreeNode root){
int leftRouteLength = 0, rightRouteLength = 0;
if(root.left != null && root.left.val == root.val) { // 如果左子节点和根相同
leftRouteLength = 1 + visit(root.left); // 继续向左下遍历
}
if(root.right != null && root.right.val == root.val) { // 如果右子节点和根相同
rightRouteLength = 1 + visit(root.right); // 继续向右下遍历
}
maxRouteLengeth = (leftRouteLength + rightRouteLength) > maxRouteLengeth ? (leftRouteLength + rightRouteLength) : maxRouteLengeth; // 更新最大的路径长度
return leftRouteLength > rightRouteLength? leftRouteLength: rightRouteLength; // 返回左右子树中路径较长的那个
}
}
注意事项
- 注意事项
拓展延伸
- 自叶子节点反向向根节点遍历,这样可以减少计算量,避免重复的递归计算
class Solution {
private int maxRouteLengeth = 0;
public int longestUnivaluePath(TreeNode root) {
if (root == null) return 0;
visit(root);
return maxRouteLengeth;
}
private int visit(TreeNode root){
if(root == null) return 0;
int left = visit(root.left);
int right = visit(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;
maxRouteLengeth = Math.max(maxRouteLengeth, leftPath + rightPath);
return Math.max(leftPath, rightPath);
}
}
13 337. 打家劫舍 III
思路描述
- 双递归,遍历每一个节点,计算从它开始能够得到的最大金额。
代码实现
java代码:
class Solution {
private Map<TreeNode, Integer> map = new HashMap<>(); // Hashmap空间换时间
public int rob(TreeNode root) {
if(root == null) return 0;
if (map.containsKey(root)) return map.get(root); // 如果当前这个节点已经被检测过,则通过Hash表获取它的值
// 对于一棵树,根据题意我们只有两种局部最优偷窃方法
int val1 = root.val; // 1.一定先偷根,然后越过两个子节点,处理左右节点的子节点
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); // 2.越过根节点,处理两个子节点
int tmp = val1 > val2 ? val1 : val2; // 判断两种偷法那种收益大
map.put(root, tmp); // 将对应节点的最优值存入Hash表
return tmp;
}
}
注意事项
- 第一次做有关动态规划的题
拓展延伸
- 拓展延伸
14 671. 二叉树中第二小的节点
思路描述
- 设置两个个临时单元,用于存储最小值和第二小的值。
- 如果第二小的值和最小值相同,则表明第二小的值不存在。
- 遍历这棵树。
- 首先我们可以确定的是,如果这棵树不为空,那么根的值就是最小的节点。然后向下层次遍历这棵树,找到的最小的比根节点大的值就为第二小的值。
代码实现
java代码:
广度遍历非递归:
class Solution {
private int min; // 记录最小的树根值
private int secondMin = Integer.MAX_VALUE; // 默认第二小的节点的值
private boolean flag = false; // 这个表示有没有符合条件的节点值来作为第二小的节点
public int findSecondMinimumValue(TreeNode root) {
if(root == null) return -1; // 没有第二小的值,返回-1
min = root.val; // 记录根节点的值为最小
// 层次遍历
Queue<TreeNode> q = new LinkedList<>(); // 暂存队列
q.offer(root);
q.offer(null); // 一层遍历结束的标志
// 开始遍历
while(!q.isEmpty()){
root = q.poll();
if(root != null){ // 当前层没有结束,访问这个节点
// 对当前节点进行操作
if(root.val > min && root.val <= secondMin){ // 如果当前节点大于最小值,并且小于等于int的最大值,也就是遍历节点取比min大的最小的节点值
secondMin = root.val;
flag = true; // 说明第二小的节点进行了赋值
}
if(root.left != null) q.offer(root.left);
if(root.right != null) q.offer(root.right);
}
}
// 整棵树都遍历玩了,看看有没有对第二小的节点赋值
if(flag) return secondMin;
return -1;
}
}
递归:
class Solution {
private int min = -1; // 记录最小的树根值
private int secondMin = Integer.MAX_VALUE; // 默认第二小的节点的值
private boolean flag = false; // 这个表示有没有符合条件的节点值来作为第二小的节点
public int findSecondMinimumValue(TreeNode root) {
if(root == null) return -1;
if(min == -1) min = root.val;
if(root.val > min && root.val <= secondMin){
secondMin = root.val;
flag = true;
}
findSecondMinimumValue(root.left);
findSecondMinimumValue(root.right);
if(flag) return secondMin;
return -1;
}
}
注意事项
- 必须要将树中所有节点全部遍历,才能得到倒数第二小的值。原本想的层次遍历只要找到一个比min大但是小于secondMin的这个想法是不对的。因为可能在其他的分支上还会有跟小的节点。
拓展延伸
- 拓展延伸
15 637. 二叉树的层平均值
思路描述
- 层次遍历,没什么好说的
代码实现
java代码:
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
if(root == null) return new ArrayList<>();
List<Double> ret = new ArrayList<>(); // 返回数组
Queue<TreeNode> q = new LinkedList<>(); // 暂存队列
Double sum = 0.0; // 计算一层的和
int i = 0; // 计算一行有几个节点
q.offer(root);
q.offer(null);
while(!q.isEmpty()){
root = q.poll();
if(root != null){
// visit(root);
sum += root.val;
i++;
if(root.left != null) q.offer(root.left);
if(root.right != null) q.offer(root.right);
}else{
// level++; // 进行一层结束的操作
ret.add(sum / i);
sum = 0.0;
i = 0;
if(!q.isEmpty()){ // 如果队列没有结束,添加层结束标志
q.offer(null);
}
}
}
return ret;
}
}
注意事项
- 注意事项
拓展延伸
- 另一种层次遍历写法,这种更直观更简单点。
private int LevelOrder(TreeNode root){
if(root == null) return 0;
Queue<TreeNode> q = new LinkedList<>();
int num, i; // num用来暂存一层有多少个节点;i用来计数
q.offer(root);
while(!q.isEmpty()){
num = q.size(); // 获取队列中节点的个数
for(i = 0; i < num; i++){
root = q.poll();
// visit(root);
System.out.print(root.val + " ");
if(root.left != null) q.offer(root.left);
if(root.right != null ) q.offer(root.right);
}
// level++;
}
return 1;
}
16 513. 找树左下角的值
思路描述
1.层次遍历,每一层记下第一个节点的值,遍历结束后输出。
代码实现
java代码:
class Solution {
public int findBottomLeftValue(TreeNode root) {
int blValue = 0, size, i;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
size = q.size();
for(i = 0; i<size; i++){
root = q.poll();
if(i == 0) blValue = root.val; // visit(root);
if(root.left != null) q.offer(root.left);
if(root.right != null) q.offer(root.right);
}
// level++;
}
return blValue;
}
}
注意事项
- 注意事项
拓展延伸
- 改变节点进队的顺序这样,最后一个遍历到的节点就是左下的节点了。
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;
}
}
17 144. 二叉树的前序遍历
18 94. 二叉树的中序遍历
19 145. 二叉树的后序遍历
20 669. 修剪二叉搜索树
思路描述
- 处理每一个节点的逻辑,如果当前节点的值小于下限或者大于上限,则对应的去遍历较大或者较小另一侧节点。
- 如果当前节点满足上下限要求,则使用递归的方法遍历其左右子树。
代码实现
java代码:
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
if(root == null) return null;
// 处理每一个节点的逻辑
if(root.val < low) return trimBST(root.right, low, high);
if(root.val > high) return trimBST(root.left, low, high);
// 遍历所有节点
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
return root;
}
}
注意事项
- 注意事项
拓展延伸
- 拓展延伸
21 230. 二叉搜索树中第K小的元素
思路描述
- 中序遍历二叉搜索树得到的就是递增的数列,知道这个就好办了。
- 中序遍历二叉搜索树,到第k个时输出。
代码实现
java代码:
class Solution {
private int kval; // 返回值
private int flag; // 索引值
public int kthSmallest(TreeNode root, int k) {
if(root == null) return 0;
flag = k; // 将索引值付给成员变量
// 中序遍历
if(flag > 0){ // 如果还没有找到第k个节点
kthSmallest(root.left, flag);
}
if(--flag == 0) { // 如果索引值减少为0,说明找到了想要的节点
kval = root.val;
}
if(flag > 0){ // 如果还没有找到第k个节点
kthSmallest(root.right, flag);
}
return kval;
}
}
注意事项
- 注意事项
拓展延伸
- 递归的写法
class Solution {
public int kthSmallest(TreeNode root, int k) {
int leftCnt = count(root.left);
if (leftCnt == k - 1) return root.val; // 如果当前节点的左侧节点数目刚刚好为k-1,说明当前节点为所求节点
if (leftCnt > k - 1) return kthSmallest(root.left, k); // 如果左侧节点数大于k-1,说明要求的节点在左侧子树中,进入左侧子树继续寻找
return kthSmallest(root.right, k - leftCnt - 1); // 否则进入右侧子树寻找
}
private int count(TreeNode node) { // 用来计算以node为根的树的节点数目
if (node == null) return 0;
return 1 + count(node.left) + count(node.right);
}
}
22 538. 把二叉搜索树转换为累加树
思路描述
- 给出二叉搜索树的根节点,使每个节点 node 的新值等于原树中大于或等于node.val的值之和。
- 也就是说把这个节点的节点值变成其右侧的所有节点的值得和,再加上它自己。
- 这个节点处理顺序也就是右中左喽,逆后序遍历。
代码实现
java代码:
class Solution {
private int sum = 0;
public TreeNode convertBST(TreeNode root) {
if(root == null) return root;
convertBST(root.right);
// System.out.print(root.val + " ");
sum += root.val;
root.val = sum;
convertBST(root.left);
return root;
}
}
注意事项
- 注意事项
拓展延伸
- 逆后序遍历法,遍历数
23 235. 二叉搜索树的最近公共祖先
思路描述
- 首先判断一下两个节点的值与根节点值得大小关系,如果他们统一大于或者小于根节点,则这两个节点在同侧。
- 否则他们两个异侧,则最近公共祖先为当前的根节点。
- 就这样向左侧或者向右侧遍历。
代码实现
java代码:
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return root;
if ((p.val >= root.val && root.val >= q.val) || (p.val <= root.val && root.val <= q.val)) { // 如果p和q分列两边
return root;
} else {
if (p.val < root.val) { // 如果都在左边
return lowestCommonAncestor(root.left, p, q);
} else {
return lowestCommonAncestor(root.right, p, q); // 如果都在右边
}
}
}
}
注意事项
- 注意事项
拓展延伸
- 简洁的写法
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;
}
}
24 236. 二叉树的最近公共祖先
思路描述
- 这道题和上一道题的不同点是二叉树由排序树换成了普通二叉树。
- 首先层次遍历。
- 层次遍历的同时,先不进行节点的访问,直到访问到p和q其中一个,然后退出层次遍历。
- 然后以队列中的节点为根遍历树,找到一棵树中同时有p和q,即为所求。
- 上面想法错了
- 双递归
- 第一个递归遍历树中节点,这道题的关键点在于当前这个递归时的树的遍历顺序,必须要用后序遍历,以实现从小网上寻找公共祖先,以免找的祖先不是最近的。
- 第二个递归找两个子节点。
- 但是这个方法时空复杂度是O(n2)O(n2),但是运行的结果和O(n)是一样的。
代码实现
官方写法:
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || ( root == p || root == q)){
return root; // 如果当前的节点是某一个子节点,就返回这个节点,否则返回null
}
// 递归判断当前节点的左右子树中有没有子节点
TreeNode l = lowestCommonAncestor(root.left, p, q);
TreeNode r = lowestCommonAncestor(root.right, p, q);
// p和q分布在当前root的两侧子树上那么最近公共祖先就是当前root
if(l != null && r != null){
return root;
}
return l != null ? l : r; // 否则,将非空的那个子节点上提
}
}
自己写的
java代码:
class Solution {
TreeNode ans = null;
int cnt = 0; // 当前找到的子节点的数目
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (cnt == 2) return ans; // 判断当前有没有找到共同的祖先节点,如果找到了就不用再向后判断节点了
if (root == null) return ans;
// 后序遍历,从低向上
lowestCommonAncestor(root.left, p, q);
lowestCommonAncestor(root.right, p, q);
if (cnt == 2) return ans; // 判断当前有没有找到共同的祖先节点,如果找到了就不用再向后判断节点了
// 单节点操作
judge(root, p, q); // 判断当前节点是否是共同祖先
if (cnt == 2) { //如果经过上面的判断,我们发现这个root就是共同祖先
ans = root; // 那么我们就把这个root保存
} else { // 否则我们重新把cnt置为0
cnt = 0;
}
return ans;
}
private void judge(TreeNode root, TreeNode p, TreeNode q) { // 以当前的root为根,遍历这棵子树中是否有我们找的两个节点
if (cnt == 2) return;
if (root == null) return;
// 先序遍历
// 单节点操作
if (root.val == p.val || root.val == q.val) { // 因为自己也可以是自己的祖先,所以直接用自己判断是不是等于目标节点
cnt++;
}
judge(root.left, p, q);
judge(root.right, p, q);
}
}
注意事项
- 这道题思路是对的但是在写程序的过程中,在程序的终止条件上绊了好久,一直找不到正确的程序终止条件判断应该写的位置。不过最终还是找到了,记一下以便后续查阅。
- 对于树的递归,我们要想中断递归过程,一般都要借助于类中的成员变量作为标志,而检测这个标志是否发生了变化的代码所处的位置应该写到对单个节点进行操作的代码段之前,换言之也就是可能会再此导致标志发生变化的代码之前。
拓展延伸
- 试了试先序递归的写法,也能做出来,但是时间消耗的非常多,大概是因为先序递归是从上往下的顺序,这样就导致了我们必须将全部的节点遍历完才能得到答案,所以耗时较多。而上面写后序遍历方法,一定程度上是从下往上(其实大概是从左下往右这么一个方向)的遍历,所以只要找到了一个节点符合祖先节点的条件那么他就一定是我们所求的节点,不用遍历全部节点了,所时间上有所节省。
先序遍历代码:
class Solution {
private TreeNode ance = null;
private int sumSons = 0;
private boolean flag = false;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 递归遍历树的所有节点
if(root == null) return null;
// 先序递归
// 单节点处理
// 递归前标志位重置
sumSons = 0;
flag = false;
if(findAnce(root, p , q)) ance = root;
// if(ance != null) return ance;
lowestCommonAncestor(root.left, p, q);
lowestCommonAncestor(root.right, p, q);
return ance;
}
private boolean findAnce(TreeNode root, TreeNode p, TreeNode q){
// 以传入的树中节点为根,遍历这棵树找两个子节点
if(root == null) return flag;
// 先序递归
// 单节点处理
if(root == p || root == q) sumSons++;
if(sumSons == 2) flag = true;
if(flag) return flag;
findAnce(root.left, p, q);
findAnce(root.right, p, q);
return flag;
}
}
- 再试一下层次遍历的想法:层次遍历到其中的一个所求子节点后就停止层次遍历,然后在递归遍历此时队列中的节点,祖先节点必在其中,这个方法是真正的从下往上。我以为时间会变短呢,结果还是长了点,但是空间消耗少了一点。
class Solution {
private TreeNode ance = null;
private int sumSons = 0;
private boolean flag = false;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 层次遍历树的所有节点,到找到p或者q
if(root == null) return null;
Queue<TreeNode> q1 = new LinkedList<>();
int levelSize = 0, i;
Stack<TreeNode> s = new Stack<>();
q1.offer(root);
while(!q1.isEmpty()){
levelSize = q1.size();
for(i = 0; i<levelSize; i++){
root = q1.poll();
s.push(root);
// 是否找到了p或者q
if(root == p || root == q){
flag = true;
break;
}
if(root.left != null) q1.offer(root.left);
if(root.right != null) q1.offer(root.right);
}
// level++
if(flag) break;
}
// 遍历栈中节点
flag = false; // 标记重新赋值,这次代表是否找了的祖先
while(!flag && !s.isEmpty()){
// 单节点处理
// 递归前标志位重置
sumSons = 0;
root = s.pop();
if(findAnce(root, p , q)) ance = root;
}
return ance;
}
private boolean findAnce(TreeNode root, TreeNode p, TreeNode q){
// 以传入的树中节点为根,遍历这棵树找两个子节点
if(root == null) return flag;
// 先序递归
// 单节点处理
if(root == p || root == q) sumSons++;
if(sumSons == 2) flag = true;
if(flag) return flag;
findAnce(root.left, p, q);
findAnce(root.right, p, q);
return flag;
}
}
25 108. 将有序数组转换为二叉搜索树
思路描述
- 平衡二叉排序树AVL,已经给定了有序的数组,那么我们只需要取中然后造树就好了。
- 每次取数组的中间值,作为根节点,然后两侧的值分别是左右子树的值,就这样递归的执行。
代码实现
java代码:
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return estabBST(nums, 0, nums.length - 1);
}
private TreeNode estabBST(int[] nums, int low, int high){
if(low <= high){
int mid = low + ((high - low) >> 1);
TreeNode root = new TreeNode(nums[mid]);
root.left = estabBST(nums, low, mid - 1); // 取中后记得移动一个位置
root.right = estabBST(nums, mid + 1, high);
return root;
}else{
return null;
}
}
}
注意事项
- 取中后记得移动一个位置,高位减一,低位加一。这一点在排序那里应该多多注意。
root.left = estabBST(nums, low, (low + high) / 2 - 1); // 取中后记得移动一个位置
root.right = estabBST(nums, (low + high) / 2 + 1, high);
拓展延伸
- 拓展延伸
26 109. 有序链表转换二叉搜索树
思路描述
- 思路和上一道题相同,只不过链表和列表取中的方法有些不同。
- 链表要通过快慢指针遍历链表来取。
- 想要对某个节点进行操作,我们最好获取到这个节点的前一个节点,这样对于单链表的操作会很方便。
代码实现
java代码:
class Solution {
public TreeNode sortedListToBST(ListNode head) {
// 预处理
if(head == null) return null;
if(head.next == null) return new TreeNode(head.val);
// 新建树的根节点
TreeNode root = new TreeNode();
// 新建链表的中间节点,以及中间节点的后一个节点
ListNode midPre = new ListNode();
ListNode midNode = new ListNode();
midPre = getMid(head); // 取链表中间节点的前一个节点
root.val = midPre.next.val; // 根节点赋值
midNode = midPre.next;
midPre.next = null; // 断开链表,分成左右两个子树
// 递归
root.left = sortedListToBST(head);
root.right = sortedListToBST(midNode.next);
return root;
}
private ListNode getMid(ListNode head){
ListNode pre = new ListNode();
ListNode slow = new ListNode();
ListNode fast = new ListNode();
pre = head;
slow = head;
fast = head.next;
while(fast != null && fast.next != null){
pre = slow;
slow = slow.next;
fast = fast.next.next;
}
//System.out.println(slow.val);
return pre;
}
}
注意事项
- 想要对某个节点进行操作,我们最好获取到这个节点的前一个节点,这样对于单链表的操作会很方便。
拓展延伸
- 拓展延伸
27 653. 两数之和 IV - 输入 BST
思路描述
- 最直接的想法:双递归,找两个节点相加的和等于所求值。但是这样没有利用到BST的特点。
- 进行简单判断:如果所给的值小于等于根节点,那么想要求的两个树节点一定在左子树中。(这个方法可太蠢了)
代码实现
java代码:
class Solution {
private boolean flag = false;
private boolean flag2 = true;
TreeNode r = new TreeNode();
public boolean findTarget(TreeNode root, int k) {
if(flag) return flag;
if(root == null) return flag;
// 使 r 一直指向树的根
if(flag2){
r = root;
flag2 = false;
}
// 单节点操作
singleNodeOp(root, k, r);
findTarget(root.left, k);
findTarget(root.right, k);
return flag;
}
private void singleNodeOp(TreeNode root, int k, TreeNode r){
if(flag) return;
if(r == null) return;
if(root.val + r.val == k && root != r) { // 如果找到了两个不同的节点值之和符合条件
flag = true;
return;
} else if (root.val + r.val > k) { // 如果当前这个组合大于目标值,我们利用排序树的特性向较小的左侧遍历
singleNodeOp(root, k, r.left);
} else { // 否则向较大的右侧遍历
singleNodeOp(root, k, r.right);
}
return;
}
}
注意事项
- 注意事项
拓展延伸
- 我们把树中序遍历成列表,然后用双指针,两个两个的相加。
- 两个指针一个指向最小值,一个指向最大值,判断与目标值k的大小关系,来移动大小指针。
class Solution {
public boolean findTarget(TreeNode root, int k) {
List<Integer> treeList = new ArrayList<>();
inOrder(root, treeList);
int i = 0,j = treeList.size() - 1;
while(i < j){
int sum = treeList.get(i) + treeList.get(j);
// 利用排序关系,减小时间复杂度
if(sum < k) i++; // 如果小于k,较小值增大
if(sum > k) j--; // 如果大于k,较大值减小
if(sum == k) return true;
}
return false;
}
private void inOrder(TreeNode root, List<Integer> tree){
if(root == null) return;
inOrder(root.left, tree);
tree.add(root.val);
inOrder(root.right, tree);
return;
}
}
28 530. 二叉搜索树的最小绝对差
思路描述
- 双递归,遍历所有节点,一一相减求最小差值,时间复杂度n2,空间复杂度2n
- 再试试另一种思路,先把树遍历出来放到列表中,然后双指针,相邻之间的数字相减找最小。时间复杂度2n,空间复杂度n。
代码实现
java代码:
class Solution {
private int min = -1;
private boolean flag = true;
private TreeNode r = new TreeNode();
public int getMinimumDifference(TreeNode root) {
if(root == null) return min == -1 ? 0: min;
if(flag){ // 保留树的根节点
r = root;
flag =false;
}
getDiff(root, r);
getMinimumDifference(root.left);
getMinimumDifference(root.right);
return min;
}
private void getDiff(TreeNode root, TreeNode r){
if(r == null) return;
if(root != r){ // 不是同一节点
int diff = Math.abs(root.val - r.val);
if(min < 0){ // 第一次赋值
min = diff;
}else{
min = min < diff ? min : diff; // 取小
}
if(root.val <= r.val){ // 减枝
getDiff(root, r.left);
}else{
getDiff(root, r.right);
}
}else{ // 如果是同一节点则左右都要遍历。
getDiff(root, r.left);
getDiff(root, r.right);
}
return;
}
}
第二种思路:
class Solution {
private List<Integer> nums = new ArrayList<>();
public int getMinimumDifference(TreeNode root) {
if(root == null) return 0;
getList(nums, root);
int minDiff = -1, i = 0, j = 1;
while(j < nums.size()){
if(minDiff < 0){
minDiff = nums.get(j) - nums.get(i);
}else{
minDiff = minDiff < nums.get(j) - nums.get(i) ? minDiff : nums.get(j) - nums.get(i);
}
i++;
j++;
}
return minDiff;
}
private void getList(List<Integer> nums, TreeNode root){
if(root == null) return;
getList(nums, root.left);
nums.add(root.val);
getList(nums, root.right);
return;
}
}
注意事项
- 注意事项
拓展延伸
- 直接在中序遍历时记录前一个节点的值就可以计算相邻两个节点之间的差值了,不需要将树的值单独存出来,然后再遍历。时间复杂度n,空间复杂度n。
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;
inOrder(node.right);
}
}
29 501. 二叉搜索树中的众数
思路描述
- 中序遍历,前后两个数字相等的话计数++,不等的话重新计数。
- 每次迭代单个节点时,判断一下众数是否发生了变化。一趟遍历结束就可以得到结果。时间复杂度n,空间复杂度n。
代码实现
java代码:
class Solution {
private int count = 1;
private int preCount = 1;
private TreeNode preNode = null; // 这里一定要置null
List<Integer> list = new ArrayList<>();
public int[] findMode(TreeNode root) {
inOrder(root);
int[] l = new int[list.size()];
int i = 0;
for(int num : list){ // 迭代
l[i++] = num;
}
return l;
}
private void inOrder(TreeNode root){
if(root == null) return;
inOrder(root.left);
if(preNode != null){ // 如果前一个节点不为空,和当前节点进行比较是否相同
if(preNode.val == root.val) count++; // 相同++
else count = 1; // 不同新的数字,重新开始计数
}
if(count > preCount){ // 如果,新的数字出现的次数大于之前的最大次数
list.clear(); // 清空list,重新添加数字
list.add(root.val);
preCount = count; // 重置最大计数
}else if(count == preCount){ // 如果相同,则这个节点中的数也是众数
list.add(root.val);
}
preNode = root; // 前置节点更新
inOrder(root.right);
return;
}
}
注意事项
- TreeNode在new出来时是有值的,并不默认为空,必须要手动赋值为null。
拓展延伸
- 拓展延伸
30 116. 填充每个节点的下一个右侧节点指针
思路描述
- 层次遍历
- 时空复杂度O(n), O(n)
代码实现
class Solution {
public Node connect(Node root) {
if (root == null) return root;
// 层次遍历
Queue<Node> q = new LinkedList<>();
Node pre = new Node();
Node tmp = new Node();
int size = 0;
// 单独处理root
root.next = null;
if (root.left != null) q.offer(root.left);
if (root.right != null) q.offer(root.right);
while (!q.isEmpty()) {
size = q.size();
pre = q.poll();
for (int i = 1; i < size; i++) {
if (pre.left != null) q.offer(pre.left);
if (pre.right != null) q.offer(pre.right);
//System.out.print(pre.val);
pre.next = q.poll();
pre = pre.next;
}
if (pre.left != null) q.offer(pre.left);
if (pre.right != null) q.offer(pre.right);
//System.out.println(pre.val);
pre.next = null;
}
return root;
}
}
注意事项
- 注意事项
拓展延伸
- 空间复杂度为O(n)O(1)的方法
class Solution {
public Node connect(Node root) {
if (root == null) {
return root;
}
// 从根节点开始
Node leftmost = root;
while (leftmost.left != null) {
// 遍历这一层节点组织成的链表,为下一层的节点更新 next 指针
Node head = leftmost;
while (head != null) {
// CONNECTION 1
head.left.next = head.right;
// CONNECTION 2
if (head.next != null) {
head.right.next = head.next.left;
}
// 指针向后移动
head = head.next;
}
// 去下一层的最左的节点
leftmost = leftmost.left;
}
return root;
}
}
31 117. 填充每个节点的下一个右侧节点指针 II
思路描述
- 首先层次遍历可以做,只不过空间复杂度是n
- 使用已经建立的next指针可以将空间复杂度将为1
- 时空复杂度n,1
代码实现
class Solution {
Node start;
Node tail;
public Node connect(Node root) {
if (root == null || (root.left == null && root.right == null)) {
return root;
}
Node tmp = root; // 根节点的next一定是null,所以直接构建第二层
while (tmp != null) {
start = null; // 每一层以next指针相连形成的链表的头
tail = null; // 尾插法构建链表
while (tmp != null) {
if (tmp.left != null) {
processNode(tmp.left); // 处理节点
}
if (tmp.right != null) {
processNode(tmp.right);
}
tmp = tmp.next; // 沿着next链表横向移动,将下一层使用next连起来
}
tmp = start; // 下一层最左边的节点
}
return root;
}
private void processNode(Node n) {
if (start == null) {
start = n; // start每一层只记录一次
}
if (tail != null) {
tail.next = n; // 尾插法
}
tail = n; // 更新链表尾
}
}
注意事项
- 二
拓展延伸
- 同
32 剑指 Offer 32 - III. 从上到下打印二叉树 III
思路描述
- 层次遍历,然后判断当前层是否要逆序
- 时空复杂度n,n
代码实现
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Deque<TreeNode> queue = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
if(root != null) queue.add(root);
while(!queue.isEmpty()) {
LinkedList<Integer> tmp = new LinkedList<>();
if ((res.size() & 1) == 0) { // 判断层数奇偶
for(int i = queue.size(); i > 0; i--) {
TreeNode node = queue.poll();
tmp.addLast(node.val); // 偶数层 -> 往tmp尾部添加
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
} else {
for(int i = queue.size(); i > 0; i--) {
TreeNode node = queue.poll();
tmp.addFirst(node.val); // 奇数层 -> 往tmp头部添加
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
}
res.add(tmp);
}
return res;
}
}
注意事项
- 二
拓展延伸
- List可以使用Arraylist实例化,也可以使用LinkedList实例化。两者的区别就是顺序表和链表的区别。
33 剑指 Offer 26. 树的子结构
思路描述
- 这道题和 572. 另一棵树的子树类似,解题思路一致。
- 但是具体细节上有一点区别,主要是判断一个空树是不是另一颗树的子树,以及下面这种情况的区别:
A = [1,2,3,4]
B = [2]
这道题的会认为B是A的子树,而572则认为不是。 - 时空复杂度n,n
代码实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
boolean flag = false; // 表示两棵树没有子树关系
public boolean isSubStructure(TreeNode A, TreeNode B) {
if (flag) return flag; // 递归终止条件
if (A == null) return false; // 如果A为空
if (B == null) return false; // 如果B为空
flag = judge(A, B); // 判断当前起点的A和原始B是否有子树关系
isSubStructure(A.left, B); // 遍历A树找可能的起点
isSubStructure(A.right, B);
return flag;
}
private boolean judge(TreeNode A, TreeNode B) {
if (B == null) { // 如果B树此时为空了,无论A是不是空,都会认为此时B是A的子树
return true;
} else if (A == null) { // 如A为空了,但B不为空,则不是
return false;
}
if (A.val == B.val) {
return judge(A.left, B.left) && judge(A.right, B.right); // 递归的判断两颗子树
}
return false;
}
}
注意事项
- 二
拓展延伸
- L
34 剑指 Offer 36. 二叉搜索树与双向链表
思路描述
- 这道题很有意思,猛地一下手不知道要干嘛,但是仔细一想很简单。
- 首先我们知道,二叉排序树的中序遍历得到的就是成序的序列,所以在此基础上我们就可以得到数据的成序序列。
- 而我们只需要特殊的维护一下遍历过程的pre指针,并且在一开始赋值一下我们需要返回的双向链表的head指针
- 然后在遍历过程中构造我们的双向链表就可以了。
- 时空复杂度O(n) O(1)
代码实现
class Solution {
Node head, pre; // 双向链表的头指针head和遍历过程中的pre指针
public Node treeToDoublyList(Node root) {
if (root == null) {
return root;
}
makeDoublyList(root);
pre.right = head; // 修改最后一个节点的右指向,指向头结点
head.left = pre; // 最后修改head指针的左指向,指向最后的节点
return head;
}
private void makeDoublyList(Node cur) {
if (cur == null) {
return;
}
makeDoublyList(cur.left); // 首先向左遍历找到二叉树中最小的节点,也就是头结点
if (pre == null) { // 第一次运行到这里,pre是空
head = cur; // 头结点赋值
} else {
pre.right = cur; // 后面再运行到这里pre已经有值了,此时修改pre的right使其指向当前遍历的cur节点
cur.left = pre; // 修改当前节点的前向指针
}
pre = cur; // 修改pre
makeDoublyList(cur.right);
}
}
注意事项
- 二
拓展延伸
- L
35 剑指 Offer 07. 重建二叉树
思路描述
- 前序遍历的递归结构为:[root, [subTreeLeft], [subTreeRight]];
- 中序遍历的递归结构为:[[subTreeLeft], root, [subTreeRight]]
- 时空复杂度O(n) O(n)
代码实现
class Solution {
HashMap<Integer, Integer> map = new HashMap<>(); // 为了方便检索
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null) return new TreeNode(); // 特殊情况
int n = preorder.length;
for (int i = 0; i < n; i++) {
map.put(inorder[i], i); // 将中序遍历的数组建map
}
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
private TreeNode myBuildTree(int[] preorder, int[] inorder, int preL, int preR, int inL, int inR) {
if (preL > preR) return null; // 如果先序遍历的左子树节点个数为0
int rootVal = preorder[preL]; // 每个子树的先序遍历第一个就是当前子树的根
TreeNode root = new TreeNode(rootVal); // 构造根节点
int rootInIdx = map.get(rootVal); // 获取到中序遍历的根节点的索引,用于划分子树的节点个数
int leftSubTreeSize = rootInIdx - inL; // 获取到的左子树的节点个数
// 递归的构建左右子树
// 先序遍历中从根节点的索引 preL + 1 开始到索引 preL + leftSubTreeSize 这部分数字为先序遍历中以 preL 为根的左子树节点
// 同理中序遍历中从最左侧 inL 到根节点索引 rootInIdx 这一部分的数字是以先序遍历中 preL 为根的左子树的节点。
root.left = myBuildTree(preorder, inorder, preL + 1, preL + leftSubTreeSize, inL, rootInIdx - 1);
root.right = myBuildTree(preorder, inorder, preL + 1 + leftSubTreeSize, preR, rootInIdx + 1, inR);
return root;
}
}
注意事项
- 二
拓展延伸
- L
36 剑指 Offer 33. 二叉搜索树的后序遍历序列
思路描述
- 后序遍历的递归结构为:[[leftSubTree], [rightSubTree], root]
- 一个序列是二叉搜索树后序遍历序列的条件:左子树中的节点值一定都小于root,右子树的中的节点值一定都大于root
- 由此递归的区分左右子树的序列,然后判断是否满足条件。
- 时空复杂度O(n) O(n)
代码实现
class Solution {
public boolean verifyPostorder(int[] postorder) {
return judge(postorder, 0, postorder.length - 1);
}
private boolean judge(int[] postorder, int l, int r) {
if (l >= r) return true;
int i = l, rootVal = postorder[r]; // 划分当前子树的左右子树
while (postorder[i] < rootVal) {
i++;
}
int m = i; // [l ~ m - 1] 是左子树
while (postorder[i] > rootVal) {
i++;
} // [m ~ r - 1] 是右子树
// 如果 i == r 了,说明以当前节点为根的子树,满足左子树都小于根节点,右子树都大于根节点
// 然后递归的判断左右子树
// 使用短路特性终止递归
return i == r && judge(postorder, i, m - 1) && judge(postorder, m, r - 1);
}
}
注意事项
- 二
拓展延伸
- L
37 剑指 Offer 37. 序列化二叉树
思路描述
- 先将树中节点以字符串的形式保存,然后再按照一定的规则复原二叉树。
- 这个规则可以仿照剑指 Offer 07. 重建二叉树这个,以前序和中序来复原二叉树。
- 但是我采用的方式是使用层次遍历的方式序列化二叉树,然后在反序列化。相比于根据前序和中序的方式进行复原减少了序列化后二叉树数据占用的空间大小。
- [1,2,3,null,null,4,5,6]
- 我们发现按照如所举例子的的层序遍历的循序,反序列化的规则符合是下面这样的步骤:
- 设两个指针 i = 0, j = 1
- 从根开始,第一个数据后面的两个数据,一定是它的两个孩子。处理完这个位置,更新指针i = 1, j = 3
- 类似的,以 i 位置处的数据为父节点,那么 j 和 j + 1 处 的两个数据一定是 i 的 两个子节点,更新i = i + 1, j = j + 2
- 如果 i 位置处的数据为空,那么更新i = i + 1,j 不变。
- 直到 j >= 数据的长度
- 我们使用一个队列保存处理过的、已经形成树节点的节点,然后使用出队的动作表示 i 的变化,然后再维护一个索引变量来遍历我们的构造的序列化数据。
- 时空复杂度O(n) O(n / 4)(队列中最多保存树最后一层的所有父节点,也就是这棵树倒数第二层的节点个数,而一棵树最后一层的节点个数是n / 2,所以倒数第二层的个数是n / 4)
代码实现
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
return decodeByLevelOrder(root); // 构建层序遍历的序列化树的数据
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if (data == null) return null;
// System.out.println(data);
int len = data.length(); // 序列化数据长度
Deque<TreeNode> q = new LinkedList<>(); // 节点暂存队列
char[] treeArr = data.toCharArray(); // 字符数组
int[] tmp = new int[2]; // 初始化临时数组,第一个数字表示当前节点的val,第二个数字表示当前处理到了序列化数据的哪一位
tmp = getVal(treeArr, 0, len); // 获取根节点的val,并更新tmp数组
TreeNode root = new TreeNode(tmp[0]); // 初始化树根
q.offer(root); // 进队
TreeNode fath; // 记录当前处理节点的父节点
int i = tmp[1];
while (i < len) {
if (q.isEmpty()) break;
fath = q.poll(); // 取出父节点
if (treeArr[i] != ' ') { // 如果当前处理的节点不为空
tmp = getVal(treeArr, i, len); // 获取当前所处理节点的val,并修改tmp数组
TreeNode ls = new TreeNode(tmp[0]); // 创建左孩子
fath.left = ls;// 连接左孩子
q.offer(ls); // 进队
i = tmp[1]; // 要处理的data的下一位索引
} else {
i++; // 如果左孩子为空,那么我们需要手动的移动到下一个要处理的位置
}
if (i >= len) break;
if (treeArr[i] != ' ') {
tmp = getVal(treeArr, i, len);
TreeNode rs = new TreeNode(tmp[0]);
fath.right = rs;
q.offer(rs);
i = tmp[1]; // 要处理的data的下一位索引
} else {
i++; // 如果右孩子为空,那么我们需要手动的移动到下一个要处理的位置
}
}
return root; // 返回根节点
}
private String decodeByLevelOrder(TreeNode root) {
if (root == null) return null;
StringBuilder sb = new StringBuilder();
Deque<TreeNode> q = new LinkedList<>(); // 层序遍历
q.offer(root);
int size;
TreeNode tmp;
while (!q.isEmpty()) {
size = q.size();
for (int i = 0; i < size; i++) {
tmp = q.poll();
if (tmp != null) {
sb.append(tmp.val + ","); // 获取节点数据 + 隔离符号
q.offer(tmp.left);
q.offer(tmp.right);
} else {
sb.append(' '); // 空格表示节点为空,如果是空格就没有隔离符号,减少数据量的同时,不影响操作
}
}
}
return sb.toString().trim(); // 取出数据尾部多余空格,减少序列化的数据大小
}
private int[] getVal(char[] treeArr, int idx, int n) { // 将符号化的数据转回数字
int ans = 0, i, flag = 1;
if (treeArr[idx] == '-') { // 判断当前val的正负号
flag = -1;
idx++;
}
for (i = idx; i < n && treeArr[i] != ','; i++) { // 获取val的数值,每一个节点的val是以逗号隔开的
ans = ans * 10 + treeArr[i] - '0';
}
return new int[]{ans * flag, ++i}; // 返回
}
}
- 更优秀简明的写法
public class Codec {
final char SEP = ','; // 分割符
final char NULL = '#'; // 标志空节点
int start = 0; // 用于遍历序列化的树字符串
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuilder serial = new StringBuilder(); // 用于构造树的字符串
preorder(root, serial); // 前序遍历二叉树,进行序列化
return serial.toString(); // 转成字符串,这是题目要求
}
void preorder(TreeNode root, StringBuilder serial) { // 前序遍历的过程
if (root == null) {
serial.append(NULL).append(SEP); // 如果当前节点为空,添加空节点标志和分隔符
} else {
serial.append(root.val).append(SEP); // 如果不是空,添加当前节点的值和分隔符到树串中
preorder(root.left, serial); // 递归
preorder(root.right, serial);
}
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
// 1,2,#,#,3,4,#,#,5,#,#,
if (start >= data.length() || data.charAt(start) == NULL) { // 如果当前的已经遍历过所有的树串,或者当前要遍历的节点是空节点
start += 2; // 当前节点是空节点,指针向后移动两个位置(#,)到下一个要构造的节点处
return null; // 那么就返回空节点
}
int val = 0; // 当前节点的值
// 判断正负号
int sign = 1;
if (data.charAt(start) == '-') {
sign = -1;
start++;
}
while (data.charAt(start) != SEP) { // 获取当前节点的值
val = val * 10 + data.charAt(start) - '0';
start++;
}
start++; // 越过分隔符
TreeNode root = new TreeNode(val * sign); // 新建根节点
root.left = deserialize(data); // 递归调用
root.right = deserialize(data); // 递归调用
return root;
}
}
注意事项
- 二
拓展延伸
- L
38 662. 二叉树最大宽度
思路描述
- d
代码实现
class Solution {
public int widthOfBinaryTree(TreeNode root) {
if(root == null) return 0;
Deque<TreeNode> queue = new LinkedList<>();
//使用节点的值来记录节点在二叉树上的位置
root.val = 0;
queue.add(root);
int res = Integer.MIN_VALUE;
while(!queue.isEmpty()){
int n = queue.size();
//队列结尾减去开头的值加一即为当前层的宽度
res = Math.max(res, queue.getLast().val - queue.getFirst().val + 1);
for(int i = 0; i < n; i++){
TreeNode node = queue.poll();
if(node.left != null){
node.left.val = node.val * 2;
queue.add(node.left);
}
if(node.right != null){
node.right.val = node.val * 2 + 1;
queue.add(node.right);
}
}
}
return res;
}
}
注意事项
- 二
拓展延伸
- L
39 124. 二叉树中的最大路径和
思路描述
- d
代码实现
class Solution {
int ans = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
getAns(root);
return ans;
}
private int getAns(TreeNode root) {
if (root == null) return 0;
// 后序遍历,自底向上,判断左右分支的数值和大小
int l = Math.max(getAns(root.left), 0); // 如果是负数的话,肯定不能要
int r = Math.max(getAns(root.right), 0);
ans = Math.max(ans, root.val + l + r); // 判断以当前这个节点为根的树,左右子树 + 根节点的路径大小
return root.val + Math.max(l, r); // 返回根节点的值于较大一边子树的和
}
}
注意事项
- 二
拓展延伸
- L
40 129. 求根节点到叶节点数字之和
思路描述
- 时空复杂度O(n)O(n)
代码实现
class Solution {
int sum = 0;
public int sumNumbers(TreeNode root) {
if (root == null) return sum;
addNum(root, 0);
return sum;
}
private void addNum(TreeNode root, int tmp) {
if (root == null) { // 递归跳出
return;
}
if (root.left == null && root.right == null) { // 单点操作
sum += tmp * 10 + root.val;
return;
}
tmp = tmp * 10 + root.val;
addNum(root.left, tmp);
addNum(root.right, tmp);
}
}
注意事项
- 二
拓展延伸
- L
41 450. 删除二叉搜索树中的节点
思路描述
- 时空复杂度O(logn)O(logn)
代码实现
// 方法一:捋一下思路,删除二叉平衡树种的一个节点,我们需要的就是找到这个节点的右子树中的最小值来,也就是右子树中最左边的节点
// 然后直接把要删除节点的左子树接到"最左边节点"的左边,然后将要删除节点的右孩子返回,就完成了对要删除节点的删除。
// 但是这种方式可能会让二叉搜索树由原来的平衡变得不平衡
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) {
return root;
}
if (root.val < key) {
root.right = deleteNode(root.right, key); // 要删除的节点在当前根节点的右侧
} else if (root.val > key) {
root.left = deleteNode(root.left, key); // 要删除的节点在当前根节点的左侧
} else { // 当前节点就是要删除的节点
if (root.left == null) { // 如果当前节点的左子树为空,那么直接返回当前节点的右子树,相当于直接删除了这个节点
return root.right;
} else if (root.right == null) { // 同理
return root.left;
} else { // 如果当前节点的左右子树都不为空
TreeNode leftMost = root.right;
while (leftMost.left != null) { // 找到当前节点的右子树下的最左节点
leftMost = leftMost.left;
}
leftMost.left = root.left; // 将要删除节点的左子树更新为其右子树中最左节点的左子树
// root.left = null; // 左子树已经链到应该存在的地方了,将原来的位置置空,或者直接进行下面的操作
root = root.right; // 经过上面的操作,要删除节点的左子树已经被备份了,相当于要删除结点的左子树为空,所以退化成了直接返回要删除节点的右子树的情况
}
}
return root; // 返回
}
}
// 这种方法不会影响树本身的平衡性
// 捋一下思路,删除二叉平衡树种的一个节点,我们需要的就是找到这个节点的右子树中的最小值来代替它,也就是右子树中最左边的节点
// 同时如果右子树中最左边的节点有自己的右子树,那么我们还要将它的右子树接到其父节点的左子树上。
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) {
return root;
}
if (root.val < key) {
root.right = deleteNode(root.right, key); // 要删除的节点在当前根节点的右侧
} else if (root.val > key) {
root.left = deleteNode(root.left, key); // 要删除的节点在当前根节点的左侧
} else { // 当前节点就是要删除的节点
if (root.left == null) { // 如果当前节点的左子树为空,那么直接返回当前节点的右子树,相当于直接删除了这个节点
return root.right;
} else if (root.right == null) { // 同理
return root.left;
} else { // 如果当前节点的左右子树都不为空
TreeNode pre = root; // 用于寻找第一个大于要删除结点的父节点
TreeNode leftMost = root.right; // 用于寻找右子树中的最左节点,也就是第一个大于要删除节点的节点
while (leftMost.left != null) { // 找到当前节点的右子树下的最左节点,以及最左节点的父节点
if (pre.right == leftMost) { // 父节点的寻找要慢leftMost一步
pre = pre.right; // 先要向右一步
} else {
pre = pre.left; // 然后才能一直向左
}
leftMost = leftMost.left;
}
root.val = leftMost.val;
if (pre == root) { // 如果要删除的节点就是“最左节点”的父节点
pre.right = leftMost.right;
} else {
pre.left = leftMost.right;
}
}
}
return root; // 返回
}
}
注意事项
- 二
拓展延伸
- L
42 98. 验证二叉搜索树
思路描述
- 时空复杂度O(n)O(n)
代码实现
class Solution {
public boolean isValidBST(TreeNode root) {
return judge(root, Long.MIN_VALUE, Long.MAX_VALUE); // 定义一个上下界,使用long是因为其中有等于Integer最大值的测试用例
}
private boolean judge(TreeNode root, long low, long high) {
if (root == null) return true; // 如果当前节点为空,返回true
if (root.val > low && root.val < high) { // 如果当前节点处于它应处的上下界之间
return judge(root.left, low, root.val) && judge(root.right, root.val, high); // 递归判断左右子树是否满足条件,注意修改上下界
} else {
return false; // 如果当前节点不满足条件,返回false
}
}
}
注意事项
- 二
拓展延伸
- L
43 337. 打家劫舍 III
思路描述
代码实现
// 思路整理
// 对于每个数中的节点每次我们都有两种选择方式,选择当前节点,或者不选择当前节点
// 特别的我们不能连续的选择父节点和子节点,所以说当我们选择了一个父节点后,我们一定不能选择他的子节点
class Solution {
public int rob(TreeNode root) {
int[] robSum = doRob(root);
return Math.max(robSum[0], robSum[1]); //
}
private int[] doRob(TreeNode root) {
if (root == null) {
return new int[]{0, 0};
}
int[] l = doRob(root.left); // l[0]表示我选择root的left子节点后能得到的值;l[1]表示不选择root的left子节点转而选择更深层的子节点
int[] r = doRob(root.right);
int selectRoot = root.val + l[1] + r[1]; // selectRoot表示我们选择了当前的root节点,所以我们只能选择l[1]和r[1],不选择当前root的左右子节点转而选择更深层的节点
int notSelectRoot = Math.max(l[0], l[1]) + Math.max(r[0], r[1]); // notSelectRoot表示我们不选择当前的root节点,而可能选择root的左右子节点,至于选不选左右子节点,我们需要查看选择了子节点和不选择子节点的价值哪个比较大。
return new int[]{selectRoot, notSelectRoot};
}
}
注意事项
- 注意事项
拓展延伸
- 空