二叉树的绝大部分题目都可以直接通过递归+遍历解决
以下,必须掌握:
树的高度-递归
前中后序遍历
层序遍历
翻转二叉树
二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
思路
首先,理解下什么是最近公共祖先?
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
深度尽可能大,指的是树的结点越往下越好
自己想的思路
如果p是q的祖先节点,那么p是要找的最近公共祖先节点
如果q是p的祖先节点,那么q是要找的最近公共祖先节点
如果某一节点的左边右边分别有p和q,则该节点就是要找到最近公共祖先节点
如果p和q都在某一个节点的左边
则递归调用该节点的左子树,返回值不为空;
递归调用该节点的右子树,返回值为空。
如果p和q都在某一个节点的右边
则递归调用该节点的左子树,返回值为空;
递归调用该节点的右子树,返回值不为空。
递归
各种情况分析:
-
边界条件判断,如果root == null 则直接返回null
-
如果p根节点 或者 q根节点 则直接返回root
-
如果p,q都在root的左边,则返回left
-
如果p,q都在root的右边,则返回right
-
如果p在root的左边,q在root的右边,则返回root
-
如果p在root的右边,q在root的左边,则返回root
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
//以root为根节点的二叉树中,查找p、q的最近公共祖先
//1. 如果p、q都在于这棵二叉树中,则可以成功返回它们的最近公共祖先
//2. 如果p、q都不存在于这棵二叉树中,则返回null
//3. 如果只有p存在于这棵二叉树中,则返回p
//4. 如果只有q存在于这棵二叉树中,则返回q
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//边界值判断
if(root == null) return root;
//某一个结点本身就是root,则直接返回root
if(root == p || root == q) return root;
//以root.left为根节点的二叉树中,查找p、q的最近公共祖先
TreeNode left = lowestCommonAncestor(root.left, p, q);
//以root.right为根节点的二叉树中,查找p、q的最近公共祖先
TreeNode right = lowestCommonAncestor(root.right, p, q);
//root的左子树里面找到了这两个值,root的右子树里面没有找到这两个值
//返回left
if(left != null && right == null){
return left;
}
//root的左子树里面没有找到这两个值,root的右子树里面找到了这两个值
//返回right
if(left == null && right != null){
return right;
}
//为何left和right可以都不会null?
if(left != null && right != null){
return root;
}
return null;
// if(left != null && right != null) return root;
// return (left != null) ? left : right;
}
}
需要注意的是下面这行代码:
if(left != null && right != null){
return root;
}
为何left和right可以都不为null?
按照道理来讲,如果p和q分别在root的左子树和右子树,根据函数定义
以root为根节点的二叉树中,查找p、q的最近公共祖先
那么,此时应该返回的是left = null; right = null;最后root = null
并不满足我们之前想的:如果p和q分别在root的左右两边,我们定义将返回root
因此,在lowestCommonAncestor这个函数中,我们定义:
如果只有p存在这棵二叉树中,则返回p
如果只有q存在这棵二叉树中,则返回q
其实是代码中,这段代码起了作用:
//某一个结点本身就是root,则直接返回root
if(root == p || root == q) return root;
因此,可以存在left != null && right != null情况的存在
重点:
理解lowestCommonAncestor函数的含义:
以root为根节点的二叉树中,查找p、q的最近公共祖先
- 如果p、q都在于这棵二叉树中,则可以成功返回它们的最近公共祖先
- 如果p、q都不存在于这棵二叉树中,则返回null
- 如果只有p存在于这棵二叉树中,则返回p
- 如果只有q存在于这棵二叉树中,则返回q
仔细想想,其实,在
if(root == p || root == q) return root;
的时候,已经将3、4的情况包括进去了
也就是,3、4情况返回的肯定是值,而不是null
其他写法
leetcode上有人这样写,写法比较简单,运行下也是正确的
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);
if (left == null) return right;
if (right == null) return left;
return root;
}
}
该题其实还可以从后序遍历的角度去分析
恢复二叉搜索树
给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?
示例 1:
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。
示例 2:
输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
提示:
树上节点的数目在范围 [2, 1000] 内
-231 <= Node.val <= 231 - 1
思路
二叉搜索树的基本概念与特性:
任意一个节点的值都大于其左子树所有节点的值
任意一个节点的值都小于其右子树所有节点的值
它的左右子树也是一棵二叉搜索树
二叉搜索树的中序遍历是按:从小到大排序
这样,我们可以把二叉搜索树进行中序遍历,遍历完毕后查找逆序对,然后交互其位置即可
存在逆序对有两种情况:
逆序对两个数挨着
逆序对两个数不挨着
对于挨着的情况,直接交互即可
没有挨着的情况,会存在交换到前面的数,大于后面好几个数;交换到后面的数,会小于后面的好几个数。这样,找到第一个逆序对的前一个值,找到第二个逆序对的后一个值,交换即可。
第一个错误节点:第一个逆序对中的较大节点
第二个错误节点:第二个逆序对中的较小节点
一开始想到的是数遍历转换为数组,然后在数组中找出错误节点,交换错误节点从而得到正确的排序数组,再将排序数组转化为二叉搜索树。麻烦了
在中序遍历的时候,不需要转换数组,直接找出错误节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
private TreeNode preNode;
//第一个逆序对中的较大节点
private TreeNode firstMaxNode;
//第二个逆序对中的较小节点
private TreeNode secondMinNode;
public void recoverTree(TreeNode root) {
//边界值判断
if(root == null) return;
//后序遍历
postOrder(root);
//交换两个错误节点
int temp = firstMaxNode.val;
firstMaxNode.val = secondMinNode.val;
secondMinNode.val = temp;
}
//后序遍历
private void postOrder(TreeNode root)
{
//递归的结束条件
if(root == null) return;
postOrder(root.left);
//找出错误节点
if(preNode != null && preNode.val > root.val)
{
//找到第二个逆序对的后一个值
secondMinNode = root;
//找到第一个逆序对的前一个值(也就是,只要前面的值,后面再有,不需要了)
if(firstMaxNode != null) return;
firstMaxNode = preNode;
}
//注意:解题的时候写反了
preNode = root;
postOrder(root.right);
}
}
交换两个错误节点,我理解的是树的节点交换,想复杂了
其实,直接把值给交换了就可以
该程序的时间复杂度为O(n)
空间复杂度为O(h),h为树的高度
如果树是一个左斜树,或者右斜树,则空间复杂度为O(n)
如何优化为空间复杂度为O(1)
这个,就需要一个新的遍历方法:Morris遍历
有兴趣的可以了解下,这里不做叙述