目录
一、二叉树的修改与改造问题
1、翻转二叉树
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
这道题目大家第一次做可能会觉得它很简单,左换右,一直递归不就行了
但如果沉下心来思考这道问题的本质 我相信大家一定有所收获!
之前我们介绍了各种遍历二叉树的方式,如果要翻转这颗二叉树 我们应该采取哪一种遍历方式呢?
其实这道题采用前,中,后都可以 只要在遍历时交换左右节点即可
但是中序的时候有些麻烦 因为 我们左子树的节点已经完成了翻转 而此时左右交换,中序遍历恰好需要到右子树遍历 这就导致原先的左子树被翻转了两次 而右子树被交换到了左子树 还没有完成翻转!
前序遍历代码:
public TreeNode invertTree(TreeNode root) {
if(root==null){//root为Null的时候我们什么都不需要做
return null;
}
TreeNode temp=root.left;//交换左右节点
root.left=root.right;
root.right=temp;
invertTree(root.left);
invertTree(root.right);
return root;
}
后序遍历代码:
public TreeNode invertTree(TreeNode root) {
if(root==null){//root为Null的时候我们什么都不需要做
return null;
}
invertTree(root.left);
invertTree(root.right);
TreeNode temp=root.left;//交换左右节点
root.left=root.right;
root.right=temp;
return root;
}
中序遍历实现交换的代码也很简单,不过是 左 中 右 的顺序改为 左 中 左 因为交换过后右子树被交换到了左子树 我们需要对左子树进行处理
public TreeNode invertTree(TreeNode root) {
if(root==null){//root为Null的时候我们什么都不需要做
return null;
}
invertTree(root.left);
TreeNode temp=root.left;//交换左右节点
root.left=root.right;
root.right=temp;
invertTree(root.left);
return root;
}
层序遍历的代码也是一样的 只要在遍历到结点时 交换它的左右子树就可以了
注意:很多同学写熟了层序遍历之后 经常把从队列中弹出的元素赋值给root,这样做本身是没有任何问题的,但本题要求最后返回根节点 而经过层序遍历后 我们的root已经变为了队列中最后一个结点 因此我们一开始就要记录一个根节点
public TreeNode invertTree(TreeNode root) {
if(root==null){//root为Null的时候我们什么都不需要做
return null;
}
Deque<TreeNode> queue=new LinkedList<>();
queue.offer(root);
TreeNode res=root;
while (!queue.isEmpty()){
int size=queue.size();
for (int i=0;i<size;i++){
root=queue.poll();
TreeNode temp=root.left;//交换左右节点
root.left=root.right;
root.right=temp;
if (root.left!=null){
queue.offer(root.left);
}
if (root.right!=null){
queue.offer(root.right);
}
}
}
return res;
}
这道题当然还可以用非递归的写法来解,本质上其实还是前中后的迭代写法,对迭代遍历印象不深的同学建议看我的上一篇文章,这里就不再过多赘述。
2、从中序和后序序列构造二叉树
首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。
如果让我们肉眼看两个序列,画一颗二叉树的话,应该分分钟都可以画出来。
我们看一下这棵树的中序 和后序遍历的数组:
来看一下一共分几步:
-
第一步:如果数组大小为零的话,说明是空节点了。
-
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
-
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
-
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
-
第五步:切割后序数组,切成后序左数组和后序右数组
-
第六步:递归处理左区间和右区间
-
public TreeNode buildTree(int[] inorder, int[] postorder) { int is=0; int ie=inorder.length-1; int ps=0; int pe=postorder.length-1; TreeNode root=buildTree(inorder,postorder,is,ie,ps,pe); return root; } public TreeNode buildTree(int[] inorder,int [] postorder,int is,int ie,int ps,int pe){ if (ie<is||pe<ps){//数组中没有元素,不再需要构造 return null; } int rootVal=postorder[pe];//后序的最后一个元素 int ri=0;//在中序中寻找后序最后一个元素的 下标 for (int i=0;i<inorder.length;i++){ //遍历中序数组 寻找下标 if (inorder[i]==rootVal){ ri=i; } } TreeNode root=new TreeNode(rootVal);//新建的根节点,我们在此需要把两个数组都划分为左右两块 //左子树的中序下标 is为is ie为ri-1 左子树的总长度为 ri-is 左子树的后序下标为 ps是ps pe是 ps+左子树总长度减一 也就是 // ps+ri-is-1 pe为 //右子树的中序下标 is为ri+1 ie为 ie 右子树的后序下标为 ps为左子树的ps+1 也就是 ps+ri-is pe为pe-1 root.left=buildTree(inorder,postorder,is,ri-1,ps,ps+ri-is-1); root.right=buildTree(inorder,postorder,ri+1,ie,ps+ri-is,pe-1); return root;//返回构造好的根节点 }
但是这个代码还存在一些问题,由于我们要拿后序数组中最后一个元素在中序中查找(每一个元素) 这样的效率是很低的 因此我们在一开始就把所有的元素和它在中序中的下标存在hash表里 要用的时候直接取!
-
HashMap<Integer,Integer> map=new HashMap<>(); public TreeNode buildTree(int[] inorder, int[] postorder) { int is=0; int ie=inorder.length-1; int ps=0; int pe=postorder.length-1; for (int i=0;i<inorder.length;i++){ map.put(inorder[i],i); } TreeNode root=buildTree(inorder,postorder,is,ie,ps,pe); return root; } public TreeNode buildTree(int[] inorder,int [] postorder,int is,int ie,int ps,int pe){ if (ie<is||pe<ps){//数组中没有元素,不再需要构造 return null; } int rootVal=postorder[pe];//后序的最后一个元素 int ri=map.get(rootVal);//在中序中寻找后序最后一个元素的 下标 TreeNode root=new TreeNode(rootVal);//新建的根节点,我们在此需要把两个数组都划分为左右两块 //左子树的中序下标 is为is ie为ri-1 左子树的总长度为 ri-is 左子树的后序下标为 ps是ps pe是 ps+左子树总长度减一 也就是 // ps+ri-is-1 pe为 //右子树的中序下标 is为ri+1 ie为 ie 右子树的后序下标为 ps为左子树的ps+1 也就是 ps+ri-is pe为pe-1 root.left=buildTree(inorder,postorder,is,ri-1,ps,ps+ri-is-1); root.right=buildTree(inorder,postorder,ri+1,ie,ps+ri-is,pe-1); return root;//返回构造好的根节点 }
这道题做完后,大家可以尝试从前序和中序序列构造二叉树 本质上和本题没有任何区别,无非是后序从后找根,前序从前找根
-
HashMap<Integer,Integer> map=new HashMap<>(); public TreeNode buildTree(int[] preorder, int[] inorder) { //通过前序中序其实和中序后序没有任何区别 //中序后序是取后序的最后一个元素 //前序中序自然是取前序的第一个元素 for (int i=0;i<inorder.length;i++){//老样子,先把中序的值和下标存在hash表 map.put(inorder[i],i); } int ps=0; int pe=preorder.length-1; int is=0; int ie=inorder.length-1; return buildTree(preorder,inorder,ps,pe,is,ie); } public TreeNode buildTree(int[] preorder,int[] inorder,int ps,int pe,int is,int ie){ if (pe<ps||ie<is){ return null; } int rootVal=preorder[ps];//前序的第一个元素 int ri=map.get(rootVal); TreeNode root=new TreeNode(rootVal); //对root的左子树和右子树进行构造 //左子树:前序:ps=ps+1 通过ri获得前序数组的长度 ri pe=ps+ri-ps-1 中序:is=is ie=ri-1 //右子树:前序:ps=左子树的pe+1 ps=ps+ri pe=pe 中序等于is=ri+1 ie=ie root.left=buildTree(preorder,inorder,ps+1,ps+ri-is,is,ri-1); root.right=buildTree(preorder,inorder,ps+ri-is+1,pe,ri+1,ie); return root; }
3、最大二叉树
这道题一看问题的描述 ,就知道和上一道从前序 中序构造二叉树的问题本质没有任何区别,都是要分割数组,不断递归。
- 树的递归很多时候都可以套路解决,就一个模版,递归套路三部曲:
- 找终止条件:当l>r时,说明数组中已经没元素了,自然当前返回的节点为null。
- 每一级递归返回的信息是什么:返回的应该是当前已经构造好了最大二叉树的root节点。
- 一次递归做了什么:找当前范围为[l,r]的数组中的最大值作为root节点,然后将数组划分成[l,bond-1]和[bond+1,r]两段,并分别构造成root的左右两棵子最大二叉树。
public class Solution { public TreeNode constructMaximumBinaryTree(int[] nums) { return helper(nums,0,nums.length-1); } public TreeNode helper(int[] nums,int s,int e){ if (e<s){ return null;//当前数组没有元素,当然返回null } int max=Integer.MIN_VALUE;//定义一个最小的max,每次都在数组中寻找最大值作为根节点 int maxIndex=s;//最大值下标 for (int i=s;i<=e;i++){//在 s~e 左闭右闭 寻找最大值 if (nums[i]>max){ maxIndex=i; max=nums[i]; } } TreeNode root=new TreeNode(max);//递归的构造左右子树 root.left=helper(nums,s,maxIndex-1); root.right=helper(nums,maxIndex+1,e); return root; } }
4、合并二叉树
这道题十分简单,我们只需要确保两颗二叉树的遍历方式是相同的,在遍历到一个结点时,新建一个结点替代这两个结点,然后分别遍历左右子树即可。
这里仅给出前序遍历的代码(中序,后序只是顺序上的不一致)
public class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1==null&&root2==null){//如果两颗树都是空 那么就返回空
return null;
}
if (root1==null){//只有一棵树不为空时 返回不为空的那颗树
return root2;
}
if (root2==null){
return root1;
}
TreeNode node=new TreeNode(root1.val+root2.val);//新建结点 然后构建左右子树 本质上是前序遍历
node.left=mergeTrees(root1.left,root2.left);
node.right=mergeTrees(root1.right,root2.right);
return node;
}
}
二、二叉搜索树问题
在我们遇到二叉搜索树问题时,我们必须要记住一点:二叉搜索树的中序遍历是有序的,这一点是我们求解二叉搜索树的关键!
二叉搜索树是一个有序树:
-
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
-
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
-
它的左、右子树也分别为二叉搜索树
这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。
1、二叉搜索树中树的搜索
1)递归法:
-
确定递归函数的参数和返回值
递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。
代码如下:
TreeNode* searchBST(TreeNode* root, int val)
-
确定终止条件
如果root为空,或者找到这个数值了,就返回root节点。
if (root == NULL || root->val == val) return root;
-
确定单层递归的逻辑
看看二叉搜索树的单层递归逻辑有何不同。
因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。
如果root->val > val,搜索左子树,如果root->val < val,就搜索右子树,最后如果都没有搜索到,就返回NULL。
代码如下:
if (root->val > val) return searchBST(root->left, val); // 注意这里加了return
if (root->val < val) return searchBST(root->right, val);
return NULL;
这里可能会疑惑,在递归遍历的时候,什么时候直接return 递归函数的返回值,什么时候不用加这个 return呢。
我们在之前讲了,如果要搜索一条边,递归函数就要加返回值,这里也是一样的道理。
因为搜索到目标节点了,就要立即return了,这样才是找到节点就返回(搜索某一条边),如果不加return,就是遍历整棵树了。
整体代码如下:
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if(root==null||root.val==val) return root;
if (root.val<val) return searchBST(root.right,val);
return searchBST(root.left,val);
}
}
2)迭代法:
提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。
对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。
对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。
而对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。
例如要搜索元素为3的节点,我们不需要搜索其他节点,也不需要做回溯,查找的路径已经规划好了。
中间节点如果大于3就向左走,如果小于3就向右走,如图:
所以迭代法代码如下:
public class Solution {
public TreeNode searchBST(TreeNode root, int val) {
while (root!=null){
if (val>root.val){
root=root.right;
}
if (val<root.val){
root=root.left;
}
return root;
}
return null;
}
}
2、验证二叉搜索树
写到这道题的时候,很多同学直接想到 我只要遍历每个结点 保证左小于它 右大于它 不就好了
这样的思路看似完美 但其实有很大问题 因为二叉搜索树要求左子树所有结点都小于它 而右子树所有结点都大于它
我们可以对上述思路很轻易的举出反例:
6明显大于10,所以不能出现在右子树
因此我们想到利用二叉搜索树进行中序遍历 定义前一个pre 只要当前结点<=它我们就返回false,否则每个节点都遍历完成 确认无误后我们返回true
这道题其实有很多坑 很多同学写下如下的代码:
public class Solution {
int pre=Integer.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root==null) return true;
isValidBST(root.left);
if (root.val<=pre) return false;
pre=root.val;
return isValidBST(root.right);
}
}
这代码其实有很多问题 他真正能检测出一棵树是不是二叉搜索树吗?并不能!为什么?
这的确是中序遍历 但是 ,假如左子树已经不是二叉搜索树了 而我们返回的却永远是右子树的检测!因此我们需要定义一个变量将左子树的检测保存起来!
public class Solution {
int pre=Integer.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root==null) return true;
boolean left=isValidBST(root.left);
if (root.val<=pre) return false;
pre=root.val;
boolean right=isValidBST(root.right);
return left&&right;
}
}
但仍然存在问题,官方的测试用例中有第一个结点的值为integer的最小值的可能,那我们换成long的最小值不就好了,确实可以这么做,但假如我们第一个结点恰好是long的最小值呢?
所以这种方案缺乏严密性,我们应把pre定义为前驱结点 默认他是null
TreeNode pre=null;
public boolean isValidBST(TreeNode root) {
if (root==null) return true;
boolean left=isValidBST(root.left);
if (pre==null||root.val>pre.val){
pre=root;
}else {
return false;
}
boolean right=isValidBST(root.right);
return left&&right;
}
3、二叉搜索树的最小绝对差
这道题看起来十分复杂,不同结点之间的最小差值,那么我需要找每一种结点的匹配项 这个复杂度稳稳地O(N^2) 但其实我们发现本题是一颗二叉搜索树 它的中序遍历一定是升序地 而最小地差值一定出现在两个相邻地元素
public class Solution {
public int min=Integer.MAX_VALUE;//定义最小差值
public TreeNode pre=null;//定义前一个结点
public int getMinimumDifference(TreeNode root) {
if (root==null){
return -1;//本质上我们什么都不需要做
}
getMinimumDifference(root.left);
if (pre==null){//中序遍历地第一个结点
pre=root;
}else {
min=Math.min(min,root.val-pre.val);
pre=root;
}
getMinimumDifference(root.right);
return min;
}
}
4、二叉搜索树中的众数
这道题看起来似乎要用到hashmap存储对应的值和出现的次数然后再逐一比较出现的次数大小 最后返回众数 但还是由于它是二叉搜索树,所以中序遍历是有序的 既然有序 那么相同的数 肯定是连在一起的 因此我们很好统计数量
public class Solution {
List<Integer> answer=new ArrayList<>();//用于加入元素 最后返回数组
int base,count,max;
public int[] findMode(TreeNode root) {
dfs(root);
int[] res=new int[answer.size()];//把list中的元素加到数组中
for (int i=0;i<answer.size();i++){
res[i]=answer.get(i);
}
return res;
}
public void dfs(TreeNode root){//本质上是中序遍历
if (root==null) return;
dfs(root.left);
upDate(root.val);
dfs(root.right);
}
public void upDate(int val){
if (val==base){//还是统计当前值的个数
count++;
}else {//新的值了
count=1;
base=val;
}
if (count==max){//相等的时候需要把元素加进去
answer.add(base);
}
if (count>max){//大于需要清空 然后加进去
max=count;//更新最大值
answer.clear();
answer.add(base);
}
}
}
5、把二叉搜索树转为累加树
一看到累加树,相信很多小伙伴都会疑惑:如何累加?遇到一个节点,然后在遍历其他节点累加?怎么一想这么麻烦呢。
然后再发现这是一颗二叉搜索树,二叉搜索树啊,这是有序的啊。
那么有序的元素如果求累加呢?
其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。
为什么变成数组就是感觉简单了呢?
因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。
那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了。
本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。
- 递归函数参数以及返回值
这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。
同时需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了。
代码如下:
int pre; // 记录前一个节点的数值
void traversal(TreeNode* cur)
- 确定终止条件
遇空就终止。
if (cur == NULL) return;
- 确定单层递归的逻辑
注意**要右中左来遍历二叉树**, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。
class Solution {
TreeNode pre=null;
public TreeNode convertBST(TreeNode root) {
if (root==null){
return null;
}
convertBST(root.right);
if (pre==null){
//root的val不变
}else {
root.val+=pre.val;
}
pre=root;
convertBST(root.left);
return root;
}
}