算法学习之二叉树

这下终于到我们的重量级的内容了,二叉树!这一章要比前面的章节都要长得长得长得多,因此我们要做好打长期战的准备。

二叉树的理论基础

在学习二叉树之前,我们可以学习下二叉树的一些相关知识

在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。

满二叉树

满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。

如图所示:

这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。

完全二叉树

什么是完全二叉树?

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。

大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。

我来举一个典型的例子如题:

相信不少同学最后一个二叉树是不是完全二叉树都中招了。

之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。

二叉搜索树

前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树

下面这两棵树都是搜索树

平衡二叉搜索树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

如图:

最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。

C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。

所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!

二叉树的存储方式

二叉树可以链式存储,也可以顺序存储。

那么链式存储方式就用指针, 顺序存储的方式就是用数组。

顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。

链式存储如图:

链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?

其实就是用数组来存储二叉树,顺序存储的方式如图:

用数组来存储二叉树如何遍历的呢?

如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。

所以大家要了解,用数组依然可以表示二叉树。

二叉树的遍历方式

关于二叉树的遍历方式,要知道二叉树遍历的基本方式都有哪些。

一些同学用做了很多二叉树的题目了,可能知道前中后序遍历,可能知道层序遍历,但是却没有框架。

我这里把二叉树的几种遍历方式列出来,大家就可以一一串起来了。

二叉树主要有两种遍历方式:

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。
  2. 广度优先遍历:一层一层的去遍历。

这两种遍历是图论中最基本的两种遍历方式,后面在介绍图论的时候 还会介绍到。

那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:

  • 深度优先遍历
  • 前序遍历(递归法,迭代法)
  • 中序遍历(递归法,迭代法)
  • 后序遍历(递归法,迭代法)
  • 广度优先遍历
  • 层次遍历(迭代法)

在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。

这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。

看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

大家可以对着如下图,看看自己理解的前后中序有没有问题。

最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。

之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。

而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。

这里其实我们又了解了栈与队列的一个应用场景了。

具体的实现我们后面都会讲的,这里大家先要清楚这些理论基础。

二叉树的定义

这里要提醒大家要注意二叉树节点定义的书写方式。

在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。

因为我们在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼!

总结

二叉树是一种基础数据结构,在算法面试中都是常客,也是众多数据结构的基石。

本篇我们介绍了二叉树的种类、存储方式、遍历方式以及定义,比较全面的介绍了二叉树各个方面的重点,帮助大家扫一遍基础。

说到二叉树,就不得不说递归,很多同学对递归都是又熟悉又陌生,递归的代码一般很简短,但每次都是一看就会,一写就废

最后我们来看看结点类的代码

kotlin 复制代码
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;
    }
}

翻转二叉树和对称二叉树

翻转二叉树

先来看题目

同样的,我们也有递归和迭代两种解法,先来看看递归的代码

ini 复制代码
class Solution {
    public TreeNode invertTree(TreeNode root) {
        dfs(root);
        return root;
    }

    private void dfs(TreeNode root) {
        if(root==null){
            return;
        }
        if(root.left!=null && root.right!=null){
            TreeNode node = root.left;
            root.left=root.right;
            root.right=node;
        }else if(root.right==null){
            root.right=root.left;
            root.left=null;
        }else {
            root.left=root.right;
            root.right=null;
        }
        dfs(root.left);
        dfs(root.right);
    }
}

递归代码的思路非常简单,我们将情况分为三类,第一种情况是左右均不为空,此时我们反转两边的左右子树,第二种情况是左为空右不为空,此时我们将右赋为左,同时我们将右赋为null,右为空左不为空则反过来。这里由于我们首先已经判断过左右一定不为空了,所以我们后面的判断只需要拿出其中的一个条件就可以进入判断了,不需要把另外的条件也拿出来,因为判断不到。

不过事实上,我们这个代码还可以进一步完善,一个简单想法就是我们可以不必反这么多条件, 每到一个结点就让其左右子树交换就可以了,请看代码

ini 复制代码
class Solution {
    public TreeNode invertTree(TreeNode root) {
        dfs(root);
        return root;
    }

    private void dfs(TreeNode root) {
        if(root==null){
            return;
        }
        TreeNode node;
        node=root.left;
        root.left=root.right;
        root.right=node;
        dfs(root.left);
        dfs(root.right);
    }
}

还可以将dfs方法也省略掉,进一步优化我们的递归代码

ini 复制代码
class Solution {
    public TreeNode invertTree(TreeNode root) {
        if(root==null) return null;
        TreeNode node;
        node=root.left;
        root.left=root.right;
        root.right=node;
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }
}

然后我们来看看迭代代码

ini 复制代码
class Solution {
    public TreeNode invertTree(TreeNode root) {
        if(root==null) return null;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            while (size-->0){
                TreeNode node = deque.pop();
                if(node==null) continue;
                TreeNode temp;
                temp = node.left;
                node.left=node.right;
                node.right=temp;
                deque.add(node.left);
                deque.add(node.right);
            }
        }
        return root;
    }
}

迭代代码的思路很简单,就是利用层序遍历,每到一个结点就反转其左右子节点,没啥值得说的。

对称二叉树

接着我们来学习对称二叉树,先看题目

先来看看递归代码

typescript 复制代码
class Solution {
    boolean judge = true;
    public boolean isSymmetric(TreeNode root) {
        dfs(root.left,root.right);
        return judge;
    }

    private void dfs(TreeNode left, TreeNode right) {
        if(judge==false || left==null && right==null) return;
        if(left==null || right==null || left.val!=right.val){
            judge=false;
            return;
        }
        dfs(left.right,right.left);
        dfs(left.left,right.right);
    }
}

我们这里新创造了一个dfs函数,这个函数的作用在于我们每次要传入左右结点,我们将对称分为两种一种,一种是左右两个子树都在往左右扩大的方向走,另一种是都在往缩小的方向走,我们通过这两个分步来做到模拟是否对称的递归,然后每次我们比较左右的值,如果左右都为空则无事发生,如果有一个为空那么说明另一个不为空,此时必然不符合条件,最后再判断值是否相等,不相等也是错的,将我们的judge值赋为false,最后我们能够得到我们所需要的答案。

我们还有直接递归的版本,不需要给对应的judge赋值,请看代码

kotlin 复制代码
class Solution {
    public boolean isSymmetric(TreeNode root) {
        return dfs(root.left,root.right);
    }

    private boolean dfs(TreeNode left, TreeNode right) {
        if(left==null && right==null) return true;
        if(right==null || left==null || left.val!=right.val) return false;
        return dfs(left.left,right.right) && dfs(left.right,right.left);
    }
}

最后我们来看看迭代的代码

ini 复制代码
class Solution {
    public boolean isSymmetric(TreeNode root) {
        Deque<TreeNode> dequeLeft = new LinkedList<>();
        Deque<TreeNode> dequeRight = new LinkedList<>();
        dequeLeft.add(root.left);dequeRight.add(root.right);
        while (!dequeLeft.isEmpty() && !dequeRight.isEmpty()){
            TreeNode nodeLeft = dequeLeft.pollLast();
            TreeNode nodeRight = dequeRight.pollLast();
            if(nodeLeft == null && nodeRight==null) continue;
            if(nodeLeft==null || nodeRight==null || nodeLeft.val!=nodeRight.val) return false;
            dequeLeft.add(nodeLeft.left);
            dequeLeft.add(nodeLeft.right);
            dequeRight.add(nodeRight.right);
            dequeRight.add(nodeRight.left);
        }
        return dequeLeft.isEmpty() && dequeRight.isEmpty();
    }
}

迭代的代码是利用两个栈来模拟左右遍历结点的过程,其本质思路和我们之前的递归代码差不多其实。

这两道题给我们的总结时什么?第一点总结是,我们可以将我们的情况普遍化,就像是在翻转二叉树里,其实根本没有必要分这么多的情况,直接嗯翻转就可以了。第二个启示是我们如果想要模拟某个对比的功能,我们可以自己创建一个新的方法去模拟,就比如在对称二叉树里,如果我们直接传root,而非left和right,那么我们肯定很难去模拟对称的情况,同时我们可以将一个复杂的过程简化为一个最基本的步骤然后去实现这个基本逻辑,利用递归去实现整体过程的对比,当然,这个本来就属于是递归的必要步骤和思想。最后一点是我们可以利用前面的已判断结果来简化我们后面的判断条件,就像我们在这两题里,每次都先判断是否为空,只要判断过其都不为空,那么剩下的一个空一个不为空的情况就可以只判断一个结果来实现整体判断、

接下来我们再来做做这题的进阶题目,请看题目

我们做这题的思路其实跟上一题的思路差不多,如果我们想歪了那就不好整,但是思路对了的话,那么这题就很简单。我们的解题的核心思想基于三点,那就是如果一棵树是另一颗树的子树,那么其有三种情况,第一种其该子树就是他自身,第二种是其为该结点的左子树,第三种是右子树,那么我们就可以根据这个思路,拿我们的每一个结点去和可能的子树比较,直到比较出相同的为止

那么我们的代码可以写成如下形式

kotlin 复制代码
class Solution {
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.pollLast();
            boolean judge = isSameTree(node,subRoot);
            if(judge) return true;
            if(node.left!=null) deque.add(node.left);
            if(node.right!=null) deque.add(node.right);
        }
        return false;
    }

    public boolean isSameTree(TreeNode p, TreeNode q) {
        if(p==null && q==null) return true;
        if(p!=null && q==null || p==null || p.val!=q.val) return false;
        return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
    }
}

我们这个代码的思路大体就是这样的,不断拿结点去和可能的子树比较,直到所有的结点比较完毕。这里采用了递归和迭代组合的方式,实际上我们也可以把第一个迭代也改成递归,请看代码

kotlin 复制代码
class Solution {
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if(root==null) return false;
        return isSameTree(root,subRoot) || isSubtree(root.left,subRoot) || isSubtree(root.right,subRoot);
    }

    public boolean isSameTree(TreeNode p, TreeNode q) {
        if(p==null && q==null) return true;
        if(p!=null && q==null || p==null || p.val!=q.val) return false;
        return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
    }
}

可以看到我们这里代码就精简很多了,我们这里代码是每次都进入判断代码,然后得到结果之后若为真就直接返回,若为否我们则递归执行我们的原来的判断例程,令其往左和往右前进,这样就能达到跟迭代一样的效果了,而且我们的代码还精简了不少

二叉树的递归和迭代遍历

二叉树的递归遍历

关于递归,我们不能总是随心而动,随刃而行。瞎几把做,这样我们总是容易做错,而且没有规律去做也不利于我们在思维上的成长。因此我们要学习一套递归上的方法论,请看下文

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

下面我们就以一道前序遍历的题目来讲解下我们的递归思路,我们的题目是第144题。在本题里,我们要通过递归实现二叉树的前序遍历,那么我们先确定我们递归函数的参数和返回值,首先我们的参数必然是一个结点,然后由于题目需要的是List集合里存放对应的值,因此我们递归的时候直接把值放到List集合中就可以了,不需要什么返回值。接着我们要确定终止条件,我们的终止条件当然就是当我们的结点到达叶结点终止,最后我们要确定单层递归的逻辑,我们单层递归的逻辑是我们每遍历到一个新结点,就将这个节点的值放到集合中,然后继续进行递归,由于前序遍历的顺序是根左右,因此我们总是先取出值,然后先往左递归,接着往右递归。

请看前序遍历的代码:

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root==null) return list;
        list.add(root.val);
        preorderTraversal(root.left);
        preorderTraversal(root.right);
        return list;
    }
}

同样我们可以实现二叉树的中序遍历的代码:

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root==null) return list;
        inorderTraversal(root.left);
        list.add(root.val);
        inorderTraversal(root.right);
        return list;
    }
}

也可以实现二叉树的后序遍历

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        if(root==null) return list;
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        list.add(root.val);
        return list;
    }
}

二叉树的迭代遍历

当然,递归属于是小试牛刀了,大家都会觉得太简单,没有难度,那么现在我们就来试试迭代遍历。前来做做前序遍历的迭代题目

在迭代遍历上,我们可以回忆下我们是怎么处理层序遍历的,那时候我们借助了队列,借助队列这个数据结构通过不断地存取我们的结点类,最终实现层序遍历,这里我们是利用到了队列先进先出的特性。而我们在前序遍历上,我们当然也要借助其他数据结构来辅助解题,这个数据结构就是栈。我们要借助栈先进后出的原理,来完成我们的前序遍历。我们每次让结点入栈,然后弹栈之后获取结点值,接着让其右节点先入栈,左节点后入栈,这样由于栈先进后出的性质,我们总是可以先处理左节点,后处理右节点,这正好是我们所需要的前序遍历的顺序

我们可以写入的代码如下(这里我们用双端队列实现栈)

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new ArrayDeque<>();
        deque.addFirst(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.pollFirst();
            list.add(node.val);
            if(node.right!=null) deque.addFirst(node.right);
            if(node.left!=null) deque.addFirst(node.left);
        }
        return list;
    }
}

接着我们来实现我们的中序遍历,有的同学可能会觉得我还就那个改改我的代码顺序,那就完成中序遍历了,还就那个简单,但实际上不行,要问为什么,因为迭代和遍历不一样,改变顺序没有什么意义,结果还是前序遍历,屁用没有。要知道,遍历时我们改变我们的顺序,是可以很成功的改变我们的代码的逻辑的,但是迭代不同,我们简单改变其顺序,是不能和递归一样顺序完成我们代码逻辑的改变的,我们需要更加大的改动。先来看看中序遍历的题目吧

我们要解决这个题目,我们首先要搞明白我们迭代的时候到底发生了什么,其实我们迭代的时候有两个动作,分别是

  1. 处理:将元素放进result数组中
  2. 访问:遍历节点

我们实现前序遍历的时候,我们的遍历顺序和我们处理和访问的顺序正好是一样的。我们总是先处理我们的结点,然后我们接着访问我们的结点,前序遍历的顺序是根左右,我们处理时是先拿根,后面去遍历结点,我们这里处理和访问的顺序和我们遍历的顺序正好一致,因此我们的代码可以比较简洁,也可以比较简单的实现。但是到了中序遍历那可就不一样了,因为中序遍历里,我们的顺序是左根右,那我们应该先访问到最左的叶子结点,然后取值,接着处理中间结点,然后处理右节点。这时我们处理和访问的顺序就和遍历的顺序不重合了,此时我们要想办法达到先去访问左子树的叶结点,之后再去处理我们的结点。

我们可以将我们的访问和处理分开,我们让我们的栈专门去负责我们的访问工作,同时将我们的处理情况特殊化,我们每次循环都先让我们的结点进入到左节点,然后只要左节点为空(对应左),我们再处理我们的队列中的结点(对应根),同时让我们的结点往右前进,这个动作对应我们的中序遍历里最后的右的动作(对应右),那么按照这个思路,我们可以构造代码如下

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new ArrayDeque<>();
        while (root!=null || !deque.isEmpty()){
            if(root!=null){
                deque.add(root);
                root=root.left;
            }else {
                root = deque.pollLast();
                list.add(root.val);
                root=root.right;
            }
        }
        return list;
    }
}

这行代码里我们要理解的是,我们这里先进行了特判,其实迭代法理论上都要进行特判,因为其无法将根结点为0的特殊情况普通化。然后我们这里不需要先将根结点压入到我们的队列中,因为我们这里再循环条件里先进行了root!=null的判断,这种处理相当于是将第一次的压入的动作也放到了循环中了,然后每次压入时,我们都是压入本结点,然后让结点往左前进(只要结点不为空),一旦结点为空,此时说明我们的左节点为null,我们就让判断的结点成为我们的从队列中弹出的结点,我们队列中弹出的结点就是我们的逻辑意义上的中间结点,然后往右前进一位就代表了最后的右的动作。

我们这里要注意,在中序遍历中,我们的root本身作为while的条件之一,同时最开始的时候不往栈中添加任何结点,所有添加结点的动作都在if成立的情况下完成,且我们总是先加入结点,后让我们的结点往左子树前进。而在else情况中,我们会让我们的结点变成栈中出来的结点并进行处理,然后令其往右前进,注意是往右前进而不是将右子节点添加到我们的栈中,这点要搞清楚。

这个逻辑意义上的左根右可能比较难理解,其实就是我们总是先处理左节点,到了空的时候也是一种左节点的处理,处理完之后就到了中间节点,然后往右节点上走,之所以采用这种有些抽象的方式去理解是因为这种方式能更好地解释我们的代码。

最后就是这个思路本身可能比较难想,没关系,想不到,现在学过了,以后起码就有点思路了

最后我们来解决后序遍历的问题

其实关于二叉树的后序遍历,我们有一个比较讨巧的方法,就是我们知道,我们前序遍历的代码实现的遍历是根左右,那么我们只要稍加修改,这个实现的遍历就能编程根右左,然后我们只要反转我们集合里的元素,就能够得到我们所需要的左右根的集合了!

按照这个思路我们可以构造代码如下

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new ArrayDeque<>();
        deque.add(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.pollLast();
            list.add(node.val);
            if(node.left!=null) deque.add(node.left);
            if(node.right!=null) deque.add(node.right);
        }
        Collections.reverse(list);
        return list;
    }
}

这个思路虽然可行,但是它的效率属实不咋地,而且未免是有些投机取巧了,这根本就不是合格的迭代,它亵渎了迭代!后续我们是要对这个代码进行改造的,现在我们先这样吧

二叉树的统一迭代法

当然,我们这样左边一下,右边一下,每一题用的思路都不一样肯定不行,现在我们来学习统一的迭代方式的代码。先来看看我们的解题思路

这里我们要注意的是,如果我们的队列是以ArrayDeque实现的,那么其是不可以存入null的,否则会抛出异常。但是用链表实现是可以的。如果我们用前者实现,那么我们的标记元素应该更改为自己创建的一个元素,这样才不会发生异常。

请看前序遍历的代码

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.peekLast();
            if(node!=null){
                deque.pollLast();
                if(node.right!=null) deque.add(node.right);
                if(node.left!=null) deque.add(node.left);
                deque.add(node);
                deque.add(null);
            }else {
                deque.pollLast();
                node = deque.pollLast();
                list.add(node.val);
            }
        }
        return list;
    }
}

我们前序遍历是根左右,因此我们重新置入结点的方式是右左根。这里面的逻辑或许比较难理解,又或者是,理解了,但是想不到,但是我们理解不了归理解不了,记住还是可以得是吧,我们可以干脆记住这个代码,以后看到迭代就写出来,换换顺序就完了

接着是中序遍历的代码:

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.peekLast();
            if(node!=null){
                deque.pollLast();
                if(node.right!=null) deque.add(node.right);
                deque.add(node);
                deque.add(null);
                if(node.left!=null) deque.add(node.left);
            }else {
                deque.pollLast();
                node = deque.pollLast();
                list.add(node.val);
            }
        }
        return list;
    }
}

因为中序遍历的顺序是左根右,因此我们这里存放的顺序是右根左

最后是后序遍历的代码:

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.peekLast();
            if(node!=null){
                deque.pollLast();
                deque.add(node);
                deque.add(null);
                if(node.right!=null) deque.add(node.right);
                if(node.left!=null) deque.add(node.left);
            }else {
                deque.pollLast();
                node = deque.pollLast();
                list.add(node.val);
            }
        }
        return list;
    }
}

其实整体感觉下来,这种迭代方式,其实这个标记的作用就是用于模拟递归过程。其实对于二叉树而言,很多时候也的确是递归要远远好于迭代的,所以我们能用递归就尽量用递归,因为事实上,递归的确很方便。

然后对于这个统一迭代法,我们要明白的是,在统一迭代法里是用null作为标记的,每次循环我们判断我们的头结点是否为空,若为空则处理结点,不为空则按我们所需的顺序放置结点,这里我们主要记住一点,我们在循环中构建ifelse,我们总是在遇到空节点时再处理结点,否则我们都只对结点进行对应的出栈和令其子节点入栈的动作。

二叉树的构造和最大二叉树

从中序与后序遍历序列构造二叉树

这题我们要解,就要利用中序遍历与后序遍历得到的结果数组来构造我们所需要的二叉树,我们的思路如下

可以看到我们这里最重要的思想就是分割我们的数组,将我们的两个数组进行一定规则的切割,最后构造我们所需要的二叉树。这里我们最需要注意的是,由于我们递归处理时会产生非常多的区间,那么就会存在边界值的问题,我们要小心处理这些边界值问题,不然就会产生各种各样的问题,我们在本题中一律采用左闭右开的方式来处理我们的区间

这里我们先切割中序数组,之所以要切割中序数组,是因为切割点在后续数组的最后一个元素,我们使用这个元素来切割中序数组的,所以我们要先切割中序数组。最后我们可以构造我们的代码如下

ini 复制代码
class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        return dfs(inorder,0,inorder.length,postorder,0,postorder.length);
    }

    private TreeNode dfs(int[] inorder, int inLeft, int inRight, int[] postorder, int poLeft, int poRight) {
        if(inRight-inLeft<1){
            return null;
        }
        if(inRight-inLeft==1){
            return new TreeNode(inorder[inLeft]);
        }
        int val = postorder[poRight-1];
        TreeNode root = new TreeNode(val);
        int index = inLeft;
        for (int i = inLeft+1; i < inRight; i++) {
            if(val==inorder[i]){
                index=i;
                break;
            }
        }
        root.left = dfs(inorder,inLeft,index,postorder,poLeft,poLeft+(index-inLeft));
        root.right = dfs(inorder,index+1,inRight,postorder,poLeft+(index-inLeft),poRight-1);
        return root;
    }
}

我们这里重点要学习的是我们构建我们代码的思路,首先我们要构建新的递归函数,然后我们前面要给予其对应的处理方法,首先是当左右边界根本就不包含结点时,我们令其返回null,然后是当只有一个值时,我们让其返回对应的中序遍历的数组的第一个值,此时说明我们中序遍历的数组正好只有一个值,而那个值就是要返回的值。

每次递归我们创建后序遍历的数组的最后一个值作为结点,然后用该值去寻找在中序数组里的对应下标,接着对其进行分割,递归处理这些区间。这里我们可以看到我们循环遍历查找对应值的方式,我们默认第一个值就是我们所需要的值,然后我们进行遍历,我们这里起始的值+1是因为第一个值我们不需要进行遍历,而我们的i的值为inLeft,结尾总是inRight是因为我的边界是动态变化的,所以我们寻找值的区间也要进行一个动态的变化,如果传入0或者是其他的固定值,会报错。

我们可以看到我们令我们的结点的左子树为同时处理中序和后序切分后的左区间,而右子树为右区间。这里我们要着重讲一下我们递归传入的参数里面的学问,首先我们每次传入中序的数组之和,我们inLeft传入,这是没问题的,之后其边界值传入的index,这里是因为我们采用了左闭右开的形式因此我们可以这么做,index是不会被使用到的,接着我们后序数组的起始区间是poLeft+(index-inLeft),这里poLeft是我们的起始位置,而后面的index-inLeft则是我们通过指定下标减去其起始位置得到的,我们得到结尾的方式是开头边界加上中间距离得到的。而其下的第二个分割的右子树的区间,我们的index之所以要+1,是因为我们是左闭右开,左边的边界会被用到,而用于分割的下标是不能被重复使用的,因为其已经被生成结点了,我们不能使用,后面的inRight就是其中序数组的右边界值。而后面的后序数组,我们传入的左边界就是我们前一个的边界值,然后右边界就是最开始的边界-1,这是以为最开始的值我们已经用于创建结点了,所以这里的值也不能用了,这很好理解。

当然,的确这种代码要我们去想到,有点为难人,但是起码这种分割思路我们可以学习,比如说最前面的对边界的处理和传入边界时的细节处理等,后续我们还会有题目用到这个思路。

而至于左闭右闭的代码,各位有兴趣就自己去实现了,这里就不去演示了(其实是因为自己试了试也写不出来)

从前序与中序遍历序列构造二叉树

这题是上一题的进阶题目,其思路本质上和上一题的大差不差的,不同的是这里需要使用左闭右闭的方式来进行代码的构造会比较好,实际上我个人去尝试了使用左闭右开去构造,但是属实构造不出来,寄了

  • 最大二叉树

解决这题的方法和我们上面的思路是十分相同的,也是通过不断划分区间最后构造出我们想要的二叉树,具体的构造规则题目都已经告诉我们了,所以说其实这个还是挺好做的。

先来看看递归代码

ini 复制代码
class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        return dfs(nums,0, nums.length-1);
    }

    private TreeNode dfs(int[] nums, int start, int end) {
        if(end < start){
            return null;
        }
        if(end==start){
            return new TreeNode(nums[start]);
        }
        int index = start;
        int target = nums[start];
        for (int i = start+1; i <= end; i++) {
            if(nums[i]>target){
                target=nums[i];
                index=i;
            }
        }
        TreeNode root = new TreeNode(target);
        root.left = dfs(nums,start,index-1);
        root.right = dfs(nums,index+1,end);
        return root;
    }
}

这份代码本身挺简单的,没啥值得说的,自己看看应该也能看明白了。本题使用迭代来做的难度比较高,因此这里就不带着大家用迭代做一遍了。

十道二叉树题目

接下来,我们即将化身叶问,一次打十个!我们要利用一个层序遍历的迭代思路,通过改造达成一次解决十道题的成就。不过有一点我们要记住,那就是我们这里的重点在于递归的内容,递归的理解才是比较重要的,因为迭代太简单了。

二叉树的层序遍历

先来看第一题

这题最简单的做法当然是迭代,以前我们也做过了,利用队列辅助解题,可以轻易实现层序遍历,请看代码

ini 复制代码
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    public List<List<Integer>> levelOrder(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            List<Integer> integers = new LinkedList<>();
            while (size-->0){
                TreeNode node = deque.pop();
                integers.add(node.val);
                if(node.left!=null) deque.add(node.left);
                if(node.right!=null) deque.add(node.right);
            }
            list.add(integers);
        }
        return list;
    }
}

然后让我们来看看递归的代码

scss 复制代码
class Solution {
    public List<List<Integer>> resList = new ArrayList<List<Integer>>();

    public List<List<Integer>> levelOrder(TreeNode root) {
        checkFun01(root,0);

        return resList;
    }

    //DFS--递归方式
    public void checkFun01(TreeNode node, Integer deep) {
        if (node == null) return;
        deep++;

        if (resList.size() < deep) {
            //当层级增加时,list的Item也增加,利用list的索引值进行层级界定
            List<Integer> item = new ArrayList<Integer>();
            resList.add(item);
        }
        resList.get(deep - 1).add(node.val);

        checkFun01(node.left, deep);
        checkFun01(node.right, deep);
    }
}

我们可以看到这里递归的代码利用的deep参数来实现我们的层序遍历逻辑,这里第一点重要的是构造了新的代码用于递归,这个属于是基本技巧了。然后最妙的在于这个deep变量,初始时为0,意味着根结点的高度为1且初始时我们是在根结点的前一位的,每次到下一个结点时就让高度+1,然后判断我们的集合大小是否大于高度,小于则说明内部对应的集合没有创建出来,那么我们就创建并添加到我们的集合中,然后在利用deep集合的代表高度的特点,每次添加时都取出对应高度的集合并且将集合添加到其中,然后我们的递归顺序是从左到右,所以递归时先遍历左节点,后遍历右节点。

然后我们来看看第二题

对于这一题,有一个取巧的办法就是先获得正常的遍历代码,然后反转结果就可以了。我们来看看代码

ini 复制代码
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            List<Integer> integers = new LinkedList<>();
            while (size-->0){
                TreeNode node = deque.pop();
                integers.add(node.val);
                if(node.left!=null) deque.add(node.left);
                if(node.right!=null) deque.add(node.right);
            }
            list.add(integers);
        }
        Collections.reverse(list);
        return list;
    }
}

官方的题解代码调用了list集合里的add的重载方法,该方法就是可以将元素添加到指定位置,如果该位置有元素就会将该位置的元素挤到后面去,其实本质说实话和我们上面这个差不多。

接下来我们来看看这个递归的代码

scss 复制代码
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        dfs(root,0);
        Collections.reverse(list);
        return list;
    }

    private void dfs(TreeNode root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(list.size()<deep){
            list.add(new LinkedList<>());
        }
        list.get(deep-1).add(root.val);
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

这递归的代码就不多说了,没啥值得说的

然后我们来看看第三题

二叉树的右视图

我们先来看看迭代代码

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> rightSideView(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            while (size>0){
                 TreeNode node = deque.pop();
                 if(size==1){
                     list.add(node.val);
                 }
                if(node.left!=null) deque.add(node.left);
                if(node.right!=null) deque.add(node.right);
                size--;
            }
        }
        return list;
    }
}

迭代的逻辑是每次取每层的最后一个结点并将其加入到对应的集合中,也是比较容易实现的。

接下来我们来看看递归的代码

scss 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> rightSideView(TreeNode root) {
        dfs(root,0);
        return list;
    }

    private void dfs(TreeNode root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(list.size()==deep-1){
            list.add(root.val);
        }
        dfs(root.right,deep);
        dfs(root.left,deep);
    }
}

这个递归的代码非常妙,虽然其总体逻辑和我们上面的差不多。其精妙之处的第一点在于其巧妙在于第13行代码,其利用了size大小的==的特点,保证了我们的结点总是只能加第一个,而且我们的将遍历顺序改成先从右遍历,再从左遍历,这就保证了每次都先取到右边的结点,这样就能获得我们的右边结点。同样的,我们先从左边遍历的话,也能获得全部的左边结点。

现在我们来看看第四题

二叉树的层平均值

先来看看迭代的代码

ini 复制代码
class Solution {
    List<Double> list = new LinkedList<>();
    public List<Double> averageOfLevels(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            int times = size;
            Double sum = 0.0;
            while (size-->0){
                TreeNode node = deque.pop();
                sum+=node.val;
                if(node.left!=null) deque.add(node.left);
                if(node.right!=null) deque.add(node.right);
            }
            sum/=times;
            list.add(sum);
        }
        return list;
    }
}

我们迭代的思路就是获得其全部的值然后除于他的总数罢了再加到我们的集合中,再来看看递归的代码

scss 复制代码
class Solution {
    List<Double> list = new LinkedList<>();
    List<Double> times = new LinkedList<>();
    public List<Double> averageOfLevels(TreeNode root) {
        dfs(root,0);
        for (int i = 0; i < list.size(); i++) {
            list.set(i,list.get(i)/times.get(i));
        }
        return list;
    }

    private void dfs(TreeNode root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(list.size()<deep){
            list.add(root.val*1.0);
            times.add(1.0);
        }else {
            list.set(deep-1,list.get(deep-1)+ root.val*1.0);
            times.set(deep-1,times.get(deep-1)+1.0);
        }
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

这里我们的递归利用了list里的set方法,可以利用该方法修改list中的值,我们创建两个list集合,一个用于记录总和,一个用于记录加入的次数,利用递归将讲个集合记录完毕,然后将两个集合进行对应的处理,就可以得到我们想要的结果

然后我们来看看第五题

N叉树的层序遍历

我们先来看看迭代的代码

ini 复制代码
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    public List<List<Integer>> levelOrder(Node root) {
        if(root==null) return list;
        Deque<Node> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            List<Integer> list = new LinkedList<>();
            while (size-->0){
                Node node = deque.pop();
                list.add(node.val);
                deque.addAll(node.children);
            }
            this.list.add(list);
        }
        return list;
    }
}

迭代的代码其逻辑其实还是没啥不同,就是两个while循环,每次循环其下一层的结点全部添加到队尾

然后我们来看看递归的代码

scss 复制代码
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    public List<List<Integer>> levelOrder(Node root) {
        dfs(root,0);
        return list;
    }

    private void dfs(Node root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(list.size()<deep){
            list.add(new LinkedList<>());
        }
        list.get(deep-1).add(root.val);
        for (Node node:root.children) {
            dfs(node,deep);
        }
    }
}

递归代码的逻辑跟之前的也差不多,无非是递归代码变成了利用foreach来进行递归罢了

然后我们来看看第六题

在每个树行中找最大值

先来看看迭代代码:

ini 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> largestValues(TreeNode root) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            int max = Integer.MIN_VALUE;
            while (size-->0){
                TreeNode node = deque.pop();
                max = Math.max(node.val,max);
                if (node.left!=null) deque.add(node.left);
                if (node.right!=null) deque.add(node.right);
            }
            list.add(max);
        }
        return list;
    }
}

迭代代码的思路时每次遍历时取到每层的最大值,然后将该最大值加入到集合中,最后得到我们需要的集合

再来看看递归代码

scss 复制代码
class Solution {
    List<Integer> list = new LinkedList<>();
    public List<Integer> largestValues(TreeNode root) {
        dfs(root,0);
        return list;
    }

    private void dfs(TreeNode root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(list.size()<deep){
            list.add(root.val);
        }else {
            list.set(deep-1,Math.max(list.get(deep-1),root.val));
        }
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

递归代码的逻辑也和之前一样,只是这一每次都将最大值添加到我们的对应集合中

接着我们来看看第七题

填充每个结点的下一个右侧结点指针

我们先来看看迭代代码:

ini 复制代码
class Solution {
    public Node connect(Node root) {
        if(root==null) return null;
        Node ans = root;
        Deque<Node> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            while (size-->0){
                Node node = deque.pop();
                if(size!=0){
                    Node node2 = deque.peek();
                    node.next=node2;
                }else {
                    node.next=null;
                }
                if(node.left!=null) deque.add(node.left);
                if(node.right!=null) deque.add(node.right);
            }
        }
        return ans;
    }
}

迭代代码利用了队列实现,每次循环获得队列的第一个结点和下一个结点,每层每层的处理,只要没到达该层的最后一个结点,那么就不断弹出结点并令其指向下一个结点,如果到了,那么就将该结点赋为null,最后可以完成我们的修改

接下来我们来看看递归代码

ini 复制代码
class Solution {

    Node[] nodes = new Node[13];
    public Node connect(Node root) {
        dfs(root,0);
        return root;
    }
    
    private void dfs(Node root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(nodes[deep-1]!=null){
            nodes[deep-1].next=root;
        }
        nodes[deep-1]=root;
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

递归代码创建了一个结点数组辅助解题,其思路是结点数组每次保存对应层的第一个结点值,如果该层还没有结点,那么就将该结点加入队列,若已经有结点了,那么就将该数组中的结点的next指向下一个结点,并将该结点更新到数组中,deep在这里的作用是指代层数,用于将结点正确加入到对应的数组中。其实这样做有种用数组去模拟栈的这样一种感觉

不过我们上面的递归代码还是以层为思路进行的遍历的,实际上,我们还有更加精妙的递归思路,请看代码

ini 复制代码
class Solution {
    public Node connect(Node root) {
        if(root == null || root.left == null)
            return root;
        root.left.next = root.right;
        if(root.next != null){
            root.right.next = root.next.left;
        }
        connect(root.left);
        connect(root.right);
        return root;
    }
}

这个递归代码的精妙在于,其第三行代码直接判断自身和左节点是否为空来决定要不要结束,能这样做是因为题目规定了其为完全二叉树。接着其直接进行了左子节点的next指向右子节点的动作,这是基本的操作。但是接下来才是最精妙的部分,其直接通过该结点查看其next下结点是否为空,若不为空就直接进行该next下结点的左子节点被其自身的右子节点的next指向的动作。这里运用了两点来保证这个代码能够正确执行,一是这是个完全二叉树,这就表明了只要root的next不为空,则其必然有左子节点,二是由于第5个代码的执行,保证了next的指向不总是为null,可谓是精妙之至。无敌我只能说!

然后我们来看看第八题

与第七题不同的是,这题没有完全二叉树的条件了,而且有进阶解法,需要用常数空间来完成

我们先来看看迭代的代码

ini 复制代码
class Solution {
    public Node connect(Node root) {
        if(root==null) return null;
        Node ans = root;
        Deque<Node> deque = new LinkedList<>();
        deque.add(root);
        while (!deque.isEmpty()){
            int size = deque.size();
            while (size-->0){
                Node node = deque.pop();
                if(size!=0){
                    Node node2 = deque.peek();
                    node.next=node2;
                }else {
                    node.next=null;
                }
                if(node.left!=null) deque.add(node.left);
                if(node.right!=null) deque.add(node.right);
            }
        }
        return ans;
    }
}

迭代的代码其实和上一个一模一样了,因为根本都不需要更换代码其实,没有必要我只能说。

接下来我们来看看递归的代码

ini 复制代码
class Solution {

    Node[] nodes = new Node[1000];
    public Node connect(Node root) {
        dfs(root,0);
        return root;
    }

    private void dfs(Node root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(nodes[deep-1]!=null){
            nodes[deep-1].next=root;
        }
        nodes[deep-1]=root;
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

递归的代码同样也是可以用的,不过我们的数组的长度要加大。可以看到唯一输麻了的只有最精妙的递归解法,但是即使如此,我们还是有非常精妙的解法,请看代码

scss 复制代码
class Solution {
    public Node connect(Node root) {
        if(root==null) return root;
        if(root.left!=null && root.right!=null){
            root.left.next=root.right;
        }
        if(root.left!=null && root.right==null){
            root.left.next=getNext(root.next);
        }
        if(root.right!=null)
            root.right.next=getNext(root.next);
        connect(root.right);
        connect(root.left);
        return root;
    }

    public Node getNext(Node root){
        if(root==null) return null;
        if(root.left!=null) return root.left;
        if(root.right!=null) return root.right;
        if(root.next!=null) return getNext(root.next);
        return null;
    }
}

不过这个就只做了解了,不解释了,没什么必要,有兴趣的自己去看看吧

然后我们来看看第九题

二叉树的最大深度

先来看看迭代代码

ini 复制代码
class Solution {
    public int maxDepth(TreeNode root) {
        if(root==null) return 0;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        int depth = 0;
        while (!deque.isEmpty()){
            int size = deque.size();
            while (size-->0){
                TreeNode node = deque.pop();
                if(size==0){
                    depth++;
                }
                if(node.left!=null) deque.add(node.left);
                if(node.right!=null) deque.add(node.right);
            }
        }
        return depth;
    }
}

迭代代码其实就是每次遍历每一层,当每一层到达最后一个结点时添加让层数+1,最后返回层数

然后我们看看递归的代码

ini 复制代码
class Solution {
    int maxDepth = 0;

    public int maxDepth(TreeNode root) {
        dfs(root, 0);
        return maxDepth;
    }

    public void dfs(TreeNode root, int deep) {
        if (root == null){
            return;
        }
        deep++;
        maxDepth = Math.max(deep,maxDepth);
        dfs(root.left, deep);
        dfs(root.right,deep);
    }
}

递归的代码本身也没有太多变化,也是不断记录我们的最大深度,最后返回这个最大深度。最开始我们解题还卡死了,就是因为出现了返回值int,搞得无所适从,其实我们的记录最大深度的变量可以定义为成员变量的,没必要非要用deep返回一个我们所需要的变量来

最后这个代码其实可以在一行内解决,请看代码

typescript 复制代码
class Solution {
    public int maxDepth(TreeNode root) {
        return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }
}

这行代码的原理就是利用了三元运算符,设叶结点的高度为0,每次只要能成功进入一个,就让高度+1,最后返回高度最高的数、

不过其实我们这里还可以利用回溯来做,回溯其实也是递归的一种,不过其是会返回最后面的信息给上一层结点,然后上一层结点会利用这个信息进行判断,来看看回溯的代码

ini 复制代码
class Solution {
    int ans;
    public int maxDepth(TreeNode root) {
        ans = 0;
        if(root==null) return ans;
        getDepth(root,1);
        return ans;
    }

    private void getDepth(TreeNode root, int deep) {
        ans = Math.max(ans,deep);
        if(root.left==null && root.right==null){
            return;
        }
        if(root.left!=null){
            deep++;
            getDepth(root.left,deep);
            deep--;
        }
        if(root.right!=null){
            deep++;
            getDepth(root.right,deep);
            deep--;
        }
        return;
    }
}

可以看到我们这里先将deep++,因为我们要令深度+1,但是当我们回来的时候,我们又需要让深度-1,这是因为回溯算法要求我们所需要的东西在原本的那一层里是不变的,我们进入下一次时执行了+1的操作,那么返回时就要进行-1的动作。实际上这个代码还可以改良,就是将++和--的两个代码合并,请看合并的代码

scss 复制代码
class Solution {
    int ans;
    public int maxDepth(TreeNode root) {
        ans = 0;
        if(root==null) return ans;
        getDepth(root,1);
        return ans;
    }

    private void getDepth(TreeNode root, int deep) {
        ans = Math.max(ans,deep);
        if(root.left==null && root.right==null){
            return;
        }
        if(root.left!=null){
            getDepth(root.left,deep+1);
        }
        if(root.right!=null){
            getDepth(root.right,deep+1);
        }
    }
}

可以看到合并的代码就是将+1的动作放到我们的递归代码中了,这就相当于是我们将代码+1传入,当回来时+1的动作又不复存在了,这就将两个代码合并起来了,就很棒。

我们这里讲解回溯,我们可以先做了解,后续我学习回溯的时候,也会用大致这样的思路来做,并且会更加深入和细致。

接下来我们来看看最后一题

二叉树的最小深度

我们同样先来看看迭代代码

ini 复制代码
class Solution {
    public int minDepth(TreeNode root) {
        if(root==null) return 0;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        int minDepth = 1;
        int ans = Integer.MAX_VALUE;
        root.val=minDepth;
        while (!deque.isEmpty()){
            int size = deque.size();
            while (size-->0){
                TreeNode node = deque.pop();
                if(node.left==null && node.right==null){
                    ans=Math.min(ans,node.val);
                }
                if(node.left!=null) {
                    node.left.val=minDepth+1;
                    deque.add(node.left);
                }
                if(node.right!=null) {
                    node.right.val=minDepth+1;
                    deque.add(node.right);
                }
            }
            minDepth++;
        }
        return ans;
    }
}

迭代代码采用的是将全部树的值全部赋为高度的方式,然后每次比较叶子结点的值来获取最小高度

接着来看看递归的代码

ini 复制代码
class Solution {
    int maxDepth = Integer.MAX_VALUE;
    public int minDepth(TreeNode root) {
        if(root==null){
            return 0;
        }
        dfs(root,0);
        return maxDepth;
    }

    private void dfs(TreeNode root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(root.left==null && root.right==null){
            maxDepth =Math.min(maxDepth,deep);
            return;
        }
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

递归采用的方式是每次前进让deep代表的深度+1,然后每次到叶子结点就对我们的最短深度进行维护,最后得到我们所需要的结果

这题和上一题的本质思路没什么不同,这题给我们的提示是,我们求某些值的方式未必要用其返回的结果来获得,我们可以在这个函数本身对我们的值进行赋值,然后再返回该值,这样可以有效降低难度

OK,那么到此为止,我们就终于把层序遍历给搞定了,真正实现了,打十个,而且还是用两种方式打

二叉树的回溯法

本章节我们来学习二叉树的回溯解法相关的内容,那么什么是回溯呢?其实回溯就是递归的一种。不过回溯是递归的过程中,我们后面的递归过程用用到前面的递归过程的结果,一般来说,回溯要求我们进入新的递归例程时要将参数变化,而从中退出时要将该变化复原

完全二叉树的节点个数

先看题目

我们这题的重点在于要相处一个时间复杂度比线性阶更快的算法,这肯定要利用到其一定是完全二叉树的性质。当然,在做这题之前我们当然要先知道到底什么是完全二叉树,我们先来复习下完全二叉树的定义,请看下图

完全二叉树回顾

首先完全二叉树是除地城结点可能没填满外,其他每次结点都达到最大值。而且最下面一层的结点全部都集中在左边位置,不会集中到右边,也不会出现左边有一部分右边也有一部分的情况。然后我们来看看完全二叉树在本题中的一些性质

完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。我们先来看看情况一的图

然后来看看情况二的图

那么我们的基本思路就是不断去判断每个结点的的左右子树,看看其是不是满二叉树,若是我们就将对应的满二叉树的结点数量直接用公式计算出来,若不是我们就继续去寻找满二叉树。根据这个思路,我们可以写出我们的递归代码如下

ini 复制代码
class Solution {
    public int countNodes(TreeNode root) {
        if(root==null){
            return 0;
        }
        int leftDepth = deep(root.left);
        int rightDepth = deep(root.right);
        if(leftDepth==rightDepth){
            return (int) (Math.pow(2,leftDepth)+countNodes(root.right));
        }else {
            return (int) (Math.pow(2,rightDepth)+countNodes(root.left));
        }
    }

    private int deep(TreeNode root) {
        int deep = 0;
        while (root!=null){
            deep++;
            root=root.left;
        }
        return deep;
    }
}

我们这里采用递归和迭代并用的方式来构造我们的代码,我们这里先获得一个结点左右子树的深度,我们这里获取深度的方式都是不断往左子树中走的,这里由于完全二叉树具有底层结点总是往左靠的性质,所以我们可以通过这种方式可以保证我们总是能到达最大深度。若两者的深度相等,说明该结点的左子树是完全二叉树,此时我们就对应计算左边的二叉树结点数量并进入右子节点继续执行该代码,若不相等则必定是右小于左,此时其右子树必然是完全二叉树,此时我们对应计算右子树的代码并进入左子树中继续执行递归例程。

当然我们也可以用迭代来实现,请看代码

ini 复制代码
class Solution {
    public int countNodes(TreeNode root) {
        if(root==null){
            return 0;
        }
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        int sum = 0;
        while (!deque.isEmpty()){
            TreeNode node = deque.pop();
            if(node==null){
                continue;
            }
            int left = deep(node.left);
            int right = deep(node.right);
            if(left==right){
                sum+= (int) Math.pow(2,left);
                deque.add(node.right);
            }else {
                sum+= (int) Math.pow(2,right);
                deque.add(node.left);
            }
        }
        return sum;
    }

    private int deep(TreeNode root) {
        int deep = 0;
        while (root!=null){
            deep++;
            root=root.left;
        }
        return deep;
    }
}

迭代利用的是层序遍历结合我们的之前的判断方式来进行逻辑判断,其内部逻辑和递归的差不多,这里就不重复提及了。

不过我们这里有一点美中不足,那就是我们这里采用的是调用函数来进行的幂运算,稍显复杂,实际上我们可以通过位运算来直接一步达成我们的目的,不但代码美观,效率还会更高,请看代码

csharp 复制代码
class Solution {
    /**
     * 针对完全二叉树的解法
     *
     * 满二叉树的结点数为:2^depth - 1
     */
    public int countNodes(TreeNode root) {
        if(root == null) {
            return 0;
        }
        int leftDepth = getDepth(root.left);
        int rightDepth = getDepth(root.right);
        if (leftDepth == rightDepth) {// 左子树是满二叉树
            // 2^leftDepth其实是 (2^leftDepth - 1) + 1 ,左子树 + 根结点
            return (1 << leftDepth) + countNodes(root.right);
        } else {// 右子树是满二叉树
            return (1 << rightDepth) + countNodes(root.left);
        }
    }

    private int getDepth(TreeNode root) {
        int depth = 0;
        while (root != null) {
            root = root.left;
            depth++;
        }
        return depth;
    }
}

平衡二叉树

先来看看题目

本题我们要接触到回溯,但是在那之前,我们先来复习下高度和深度的定义,请看下图

有了这些知识之后我们正式来解开这题,首先我们用递归的方式来解开,我们要运用我们的递归三部曲,我们首先确定我们的递归参数和返回值,我们的递归参数毫无疑问的是我们的结点,而我们的返回值则是int类型的值,其代表了我们当前结点到根结点的高度。然后我们要明确我们的终止条件,我们这里的终止条件是当我们的结点为空时,我们返回0,代表最初的高度。最后我们要明确我们的每层递归的逻辑,我们每层递归都去判断其左右子树的高度,如果高度大于1,那么就说明其不符合条件,我们就返回-1代表其不符合条件,如果其符合条件,我们就将这个高度往上传递,同时越往上走越增加高度

根据这么思路,我们可以写出我们的代码如下

scss 复制代码
class Solution {
    public boolean isBalanced(TreeNode root) {
        return dfs(root)!=-1;
    }

    private int dfs(TreeNode root) {
        if(root==null) return 0;
        int left = dfs(root.left);
        if(left==-1) return -1;
        int right = dfs(root.right);
        if(right==-1)  return -1;
        return Math.abs(left-right)<=1 ? 1 + Math.max(left,right) : -1;
    }
}

我们这里判断其是否为真只要判断最后返回的值是否为-1就完了,第9行和第11行的-1的判断条件是为了尽早终止不必要的递归,提高效率,同时也是为了防止错判。最后我们进行一个判断,判断左右高度之差是否小于1,若小于1则说明符合条件,我们就令高度+1,同时返回左右子树中的较大者,其能代表最大高度,若不符合则直接返回-1就完了。

注意一点的是我们这里用到了回溯,可以看到我们运用回溯的方式,我们的回溯往往是要求我们的参数传入时要进行改动,传回时要复原这个改动,我们这里就采用直接往返回值里+1的方式隐藏了这个步骤,而其也有同样的效果。我们这里的回溯主要体现在,我们最后进行判断的条件是会往上呈递的,然后上面的条件判断时是会用上。这是回溯的基本思路,不过实际上我们这题就算我们令其复原也是可以的,因为我们是自底向上传递我们的高度的,不断传递直到到了最顶就停止,根本不会回到复原处,所以无所谓。但是,我们的回溯法的规范是要求我们做出复原这一步的,所以平时就别乱搞,好好把复原这一步也整上。

然后我们来看看我们迭代的代码,我们这里迭代的代码比较难,而且说实话效率也比较低,只做一个了解就可以了。

ini 复制代码
class Solution {
    /**
     * 迭代法,效率较低,计算高度时会重复遍历
     * 时间复杂度:O(n^2)
     */
    public boolean isBalanced(TreeNode root) {
        if (root == null) {
            return true;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode pre = null;
        while (root!= null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            TreeNode inNode = stack.peek();
            // 右结点为null或已经遍历过
            if (inNode.right == null || inNode.right == pre) {
                // 比较左右子树的高度差,输出
                if (Math.abs(getHeight(inNode.left) - getHeight(inNode.right)) > 1) {
                    return false;
                }
                stack.pop();
                pre = inNode;
                root = null;// 当前结点下,没有要遍历的结点了
            } else {
                root = inNode.right;// 右结点还没遍历,遍历右结点
            }
        }
        return true;
    }

    /**
     * 层序遍历,求结点的高度
     */
    public int getHeight(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Deque<TreeNode> deque = new LinkedList<>();
        deque.offer(root);
        int depth = 0;
        while (!deque.isEmpty()) {
            int size = deque.size();
            depth++;
            for (int i = 0; i < size; i++) {
                TreeNode poll = deque.poll();
                if (poll.left != null) {
                    deque.offer(poll.left);
                }
                if (poll.right != null) {
                    deque.offer(poll.right);
                }
            }
        }
        return depth;
    }
}

在迭代的代码里我们定义了两个方法,一个方法用于遍历结点,另一个方法是用于求当前结点的高度,这里每次进入一个结点都要进行重复遍历,说实话效率很差,而且内部的方法也不容易理解,有兴趣的自己看看吧。

值得一提的是这个方法其实还可以优化,那就是这个方法其实本质也是在模拟递归的逻辑,其也是自底向上开始计算高度是否正确的,因此我们可以将计算高度的方法稍微修改,不用每次都计算高度,而是每次计算出一个高度就将这个高度的值赋给对应的结点,然后其上一个结点只要去两个结点中的较大者就可以直接获得当前结点的高度了,具体请看代码

ini 复制代码
class Solution {
    /**
     * 优化迭代法,针对暴力迭代法的getHeight方法做优化,利用TreeNode.val来保存当前结点的高度,这样就不会有重复遍历
     * 获取高度算法时间复杂度可以降到O(1),总的时间复杂度降为O(n)。
     * 时间复杂度:O(n)
     */
    public boolean isBalanced(TreeNode root) {
        if (root == null) {
            return true;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode pre = null;
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            TreeNode inNode = stack.peek();
            // 右结点为null或已经遍历过
            if (inNode.right == null || inNode.right == pre) {
                // 输出
                if (Math.abs(getHeight(inNode.left) - getHeight(inNode.right)) > 1) {
                    return false;
                }
                stack.pop();
                pre = inNode;
            } else {
                root = inNode.right;// 右结点还没遍历,遍历右结点
            }
        }
        return true;
    }

    /**
     * 求结点的高度
     */
    public int getHeight(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int leftHeight = root.left != null ? root.left.val : 0;
        int rightHeight = root.right != null ? root.right.val : 0;
        int height = Math.max(leftHeight, rightHeight) + 1;
        root.val = height;// 用TreeNode.val来保存当前结点的高度
        return height;
    }
}

我们接着来看最后一题

二叉树的所有路径

这题很显然要使用回溯法来做,由于他很简单,我们甚至不需要进行过多的分析,请看其回溯代码

typescript 复制代码
class Solution {
    List<String> list = new ArrayList<>();
    public List<String> binaryTreePaths(TreeNode root) {
        dfs(root,"");
        return list;
    }

    private void dfs(TreeNode root,String s) {
        if(root==null){
            return;
        }
        if(root.left!=null){
            dfs(root.left,s+root.val+"->");
        }
        if(root.right!=null){
            dfs(root.right,s+root.val+"->");
        }
        if(root.left==null && root.right==null){
            list.add(s+root.val);
        }
    }
}

我们这里的递归就要用到复原后的参数了,如果我们这题还搞什么直接对参数本身进行变化,那么就会寄

接着我们来看看迭代的代码

arduino 复制代码
class Solution {
    /**
     * 迭代法
     */
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> result = new ArrayList<>();
        if (root == null){
            return result;
        }
        Stack<Object> stack = new Stack<>();
        // 节点和路径同时入栈
        stack.push(root);
        stack.push(root.val + "");
        while (!stack.isEmpty()) {
            // 节点和路径同时出栈
            String path = (String) stack.pop();
            TreeNode node = (TreeNode) stack.pop();
            // 若找到叶子节点
            if (node.left == null && node.right == null) {
                result.add(path);
            }
            //右子节点不为空
            if (node.right != null) {
                stack.push(node.right);
                stack.push(path + "->" + node.right.val);
            }
            //左子节点不为空
            if (node.left != null) {
                stack.push(node.left);
                stack.push(path + "->" + node.left.val);
            }
        }
        return result;
    }
}

迭代代码利用的是前序遍历的方式来进行对每一个叶结点路径的获得,这里新创建一个栈用于获取记录之前的路径。我个人已经很尽力去尝试用统一迭代法来实现这里的迭代的逻辑了,但总是实现不了,实在没法了,所以我们就暂时先记住这个迭代代码吧

左叶子之和

注意我们这里要的是左叶子之和,而不是左边的最左边的结点,这是两回事,具体请看下面的例子

所以我们判断结点是不是左叶子,不是通过最左边结点的有无来判断,而是通过判断父节点来判断其左孩子是不是左叶子,具体请看代码

ini 复制代码
class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if (root == null) return 0;
        int leftValue = sumOfLeftLeaves(root.left);    // 左
        int rightValue = sumOfLeftLeaves(root.right);  // 右

        int midValue = 0;
        if (root.left != null && root.left.left == null && root.left.right == null) {
            midValue = root.left.val;
        }
        int sum = midValue + leftValue + rightValue;  // 中
        return sum;
    }
}

上面这行代码说实话比较难理解,实际解题的时候应该也不可能会构造这么难理解的代码。但是我们有一个简单版本的,那就是我们既然是要通过获得其左叶子之和,那么我们可以将所有左叶子的值加起来,而右叶子则不加,如果判断其是左叶子还是右叶子可以通过简单的前序遍历来判断,那我们如何确定到底要不要加呢?我们可以通过传入一个布尔变量来确定,如果是左叶子,我们就传入true,如果是右,那么就传入false,其递归逻辑是每层都判断其是否为左叶子,具体请看代码

java 复制代码
class Solution {
    int sum = 0;
    public int sumOfLeftLeaves(TreeNode root) {
        dfs(root,false);
        return sum;
    }

    private void dfs(TreeNode root,boolean judge) {
        if(root.left==null && root.right==null && judge) sum+=root.val;
        if(root.left!=null) dfs(root.left,true);
        if(root.right!=null) dfs(root.right,false);
    }
}

这样我们就能够很简单的获得所有的左叶子之和了,而且也易于理解

最后我们再来补充一个使用迭代的做法,多构建一个栈用于保存true和false就行了

ini 复制代码
class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if(root==null) return 0;
        Deque<TreeNode> deque = new LinkedList<>();
        Deque<Boolean> dequeBool = new LinkedList<>();
        deque.add(root);
        dequeBool.add(false);
        int sum = 0;
        while (!deque.isEmpty()){
            TreeNode node = deque.pollLast();
            boolean judge = dequeBool.pollLast();
            if(node.right!=null){
                deque.add(node.right);
                dequeBool.add(false);
            }
            if(node.left!=null) {
                deque.add(node.left);
                dequeBool.add(true);
            }
            if(judge && node.left==null && node.right==null){
                sum+=node.val;
            }
        }
        return sum;
    }
}

另外还有一点值得一提的是,采用统一迭代法难以解决这种问题,似乎统一迭代法只适合三种遍历本身,而难以去增加其他变量在其中的运用,总之以后遇上这种题目我反正不用统一迭代的代码去做了

寻找数组左下角的值

我们要注意我们这题和之前的题目的区别,我们这题是求左下角的值,而非左节点,因此按照上一题的方法来做是行不通的。我们有一个简单想法就是每次我们都取其层数的第一个结点,而不取其第二个结点,同时我们让我们的递归总是优先往左递归,这样就可以实现我们的目的。而我们可以创建一个足够长的数组,令其索引代表高度,这样只要我们的树的高度不超过这个索引,我们就可以获取到每次的最左边的元素

ini 复制代码
class Solution {
    int[] ans = new int[]{0,0};
    int[] deeps = new int[10000];
    public int findBottomLeftValue(TreeNode root) {
        dfs(root,0);
        return ans[0];
    }

    private void dfs(TreeNode root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(deep>=ans[1] && deeps[deep]==0){
            ans[0]=root.val;
            ans[1]=deep;
        }
        deeps[deep]++;
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

虽然这个方法是可行的,但其是不够好的,因为总是要创建一个用于表示的高度的数组,浪费空间,而且还不具有普适性,所以我们要对我们的代码进行改造。实际上,我们大可不必采用这么这么迂回的方式,我们既然每次都要最左边的值,那么我们只要每次让我们的递归优先从左边走,然后判断其是不是第一次进入的,若是则将值更新,若不是则不更新即可,同时为了保证第一个结点也能被更新到,我们可以将最初的深入设置为-1,根结点的深度设置为0,请看代码

scss 复制代码
class Solution {
    int[] ans = new int[]{0,-1};
    public int findBottomLeftValue(TreeNode root) {
        dfs(root,0);
        return ans[0];
    }

    private void dfs(TreeNode root, int deep) {
        if(root==null){
            return;
        }
        deep++;
        if(root.left==null && root.right==null){
            if(deep>ans[1]){
                ans[0]=root.val;
                ans[1]=deep;
            }
        }
        dfs(root.left,deep);
        dfs(root.right,deep);
    }
}

用迭代法来做也是一样的,无非是多创建一个栈用于保存深度

ini 复制代码
class Solution {
    int[] ans = new int[]{0,-1};
    public int findBottomLeftValue(TreeNode root) {
        Deque<TreeNode> deque = new LinkedList<>();
        Deque<Integer> dequeDeep = new LinkedList<>();
        deque.add(root);
        dequeDeep.add(0);
        while (!deque.isEmpty()){
            TreeNode node = deque.pollLast();
            int deep = dequeDeep.pollLast();
            if(node.left==null && node.right==null ){
                if(deep>ans[1]){
                    ans[0]=node.val;
                    ans[1]=deep;
                }
            }
            if(node.right!=null){
                deque.add(node.right);
                dequeDeep.add(deep+1);
            }
            if(node.left!=null){
                deque.add(node.left);
                dequeDeep.add(deep+1);
            }
        }
        return ans[0];
    }
}

不过我们也可以不去多建造这么一个栈来保存深度,我们可以通过层序遍历来实现我们的需求,进入一层将最左边的,也就是第一个结点更新,否则就不更新,这个逻辑更加容易实现,效率也更高

ini 复制代码
//迭代法
class Solution {

    public int findBottomLeftValue(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int res = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode poll = queue.poll();
                if (i == 0) {
                    res = poll.val;
                }
                if (poll.left != null) {
                    queue.offer(poll.left);
                }
                if (poll.right != null) {
                    queue.offer(poll.right);
                }
            }
        }
        return res;
    }
}

路径总和

这个比较简单了,先来看看递归的代码

java 复制代码
class Solution {
    boolean ans=false;
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if(root==null) return ans;
        dfs(root,targetSum-root.val);
        return ans;
    }

    private void dfs(TreeNode root, int targetSum) {
        if(root.left==null && root.right==null && targetSum==0){
            ans=true;
            return;
        }
        if(root.left!=null){
            dfs(root.left,targetSum-root.left.val);
        }
        if(root.right!=null){
            dfs(root.right,targetSum-root.right.val);
        }
    }
}

这个递归的逻辑非常简单,这个就不多讲了,随便看都看得懂了,接下来看看迭代的代码

ini 复制代码
class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if(root==null) return false;
        Deque<TreeNode> deque = new LinkedList<>();
        Deque<Integer> dequeSum = new LinkedList<>();
        deque.add(root);
        dequeSum.add(targetSum);
        while (!deque.isEmpty()){
            TreeNode node = deque.pollLast();
            int target = dequeSum.pollLast();
            if(target-node.val==0 && node.left==null && node.right==null){
                return true;
            }
            if(node.left!=null){
                deque.add(node.left);
                dequeSum.add(target-node.val);
            }
            if(node.right!=null){
                deque.add(node.right);
                dequeSum.add(target-node.val);
            }
        }
        return false;
    }
}

同样是多创造一个栈用于计算值是否为零

  • 路径总和II

其实就是前面的plus版本,我们先来看看递归代码

scss 复制代码
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        if(root==null){
            return list;
        }
        List<Integer> list = new LinkedList<>();
        list.add(root.val);
        dfs(root,targetSum-root.val,list);
        return this.list;
    }

    private void dfs(TreeNode root, int targetSum, List<Integer> list) {
        if(root.left==null && root.right==null && targetSum==0){
            List<Integer> list1 = new LinkedList<>(list);
            this.list.add(list1);
            return;
        }
        if(root.left!=null){
            list.add(root.left.val);
            dfs(root.left,targetSum-root.left.val,list);

            list.remove(list.size()-1);
        }
        if(root.right!=null){
            list.add(root.right.val);
            dfs(root.right,targetSum-root.right.val,list);
            list.remove(list.size()-1);
        }
    }
}

我们这里的逻辑和上一题的是差不多的,同样是运用的回溯,不过值得一提的是,由于集合添加进去之后我们的原来的集合指向还是指向该集合,这样回溯会造成我们的目标集合发生变动,所以我们只能添加一个同样的新集合上去,没法了属于是

接下来来看看迭代代码

ini 复制代码
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        if(root==null) return list;
        Deque<TreeNode> deque = new LinkedList<>();
        deque.add(root);
        Deque<List<Integer>> dequeList = new LinkedList<>();
        dequeList.add(new LinkedList<>());
        Deque<Integer> dequeSum = new LinkedList<>();
        dequeSum.add(targetSum);
        while (!deque.isEmpty()){
            TreeNode node = deque.pollLast();
            List<Integer> list = dequeList.pollLast();
            int target = dequeSum.pollLast();
            if(target- node.val==0 && node.left==null && node.right==null){
                list.add(node.val);
                this.list.add(new LinkedList<>(list));
                list.remove(list.size()-1);
            }
            if(node.left!=null){
                deque.add(node.left);
                list.add(node.val);
                dequeList.add(new LinkedList<>(list));
                list.remove(list.size()-1);
                dequeSum.add(target-node.val);
            }
            if(node.right!=null){
                deque.add(node.right);
                list.add(node.val);
                dequeList.add(new LinkedList<>(list));
                list.remove(list.size()-1);
                dequeSum.add(target-node.val);
            }
        }
        return list;
    }
}

迭代代码的思路好递归的还是差不多,递归里多了什么参数,我们就多创建什么栈来对应模拟就可以了,虽然说效率的确是不咋地就是了

二叉树前置结点的递归使用

本节我们来学习二叉树的进阶递归,学习递归的进阶用法。第一题我们先来看看一道比较常规的和本节内容关系不大的题目,先来小试牛刀。

二叉搜索树中的搜索

这个题目还是非常简单的,首先我们来看看递归

kotlin 复制代码
class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if(root==null){
            return null;
        }
        if(root.val==val){
            return root;
        }
        if(root.val>val){
            return searchBST(root.left,val);
        }else {
            return searchBST(root.right,val);
        }
    }
}

然后我们来看看迭代,这里我们是使用完成的前序遍历达成目的

kotlin 复制代码
class Solution {
    // 迭代,普通二叉树
    public TreeNode searchBST(TreeNode root, int val) {
        if (root == null || root.val == val) {
            return root;
        }
        Deque<TreeNode> deque = new ArrayDeque<>();
        deque.add(root);
        while (!deque.isEmpty()) {
            TreeNode node = deque.pollLast();
            if (node.val == val) {
                return node;
            }
            if (node.right != null) {
                deque.add(node.right);
            }
            if (node.left != null) {
                deque.add(node.left);
            }
        }
        return null;
    }
}

最后其实我们可以使用简单的迭代完成本体,连栈都不需要,只需要我们利用二叉搜索树的性质就可以了。

kotlin 复制代码
class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if(root==null) return null;
        while (root!=null){
            if(root.val>val){
                root=root.left;
            }else if(root.val<val){
                root=root.right;
            }else {
                return root;
            }
        }
        return null;
    }
}

验证二叉搜索树

到这里我们就正式进入我们的进阶递归的学习,我们这里要学习的一个经典方法就是通过在成员变量中定义一个结点类,然后每次递归令其保存上一个结点,通过上一个结点与递归进入的当前结点进行比较来达成我们的比较目的。我们先来分析下本题

初次做本题,我们容易陷入一个误区,就是我们只是单纯地比较当前结点的左右子树的逻辑是否成立,如何全部成立我们就认为其是二叉搜索树,否则就不是,这个思路看起来似乎没有问题,但实际是不可行的。具体请看下图的例子

可以看到上面的二叉树完全符合我们的逻辑,但其实它不是一颗二叉搜索树。那么我们应该如何解决这题呢?其实我们可以使用二叉搜索树的逻辑,我们总是先向左前进,一旦前进到底部,我们就开始保存上一个结点的值,只要上一个结点有值,我们就将上一个结点与当前结点进行比较,如果我们的上一个结点比我们的当前结点还要大或者是相等,那么就说明其不是二叉搜索树,按照这种基本逻辑遍历完整棵树就可以了,同时每次到了最左的结点并处理完后,我们就往右子树上前进一位,通过这种方式来实现所有的结点的遍历,且我们的基本逻辑总是定在上一个结点和左边的结点的比较上。这种想法有点类似于我们的中序遍历的迭代法的思路,实际上我们本题用迭代法来做的时候还真的就是用中序遍历的迭代代码来做。

那么最后我们还要解决一个问题,那就是空树是二叉搜索树吗?答案是肯定的,二叉搜索树可以为空

那么我们可以写入我们的递归代码如下

ini 复制代码
class Solution {
    TreeNode node;
    public boolean isValidBST(TreeNode root) {
        if(root==null){
            return true;
        }
        boolean left = isValidBST(root.left);
        if(node!=null && node.val>=root.val){
            return false;
        }
        node=root;
        boolean right = isValidBST(root.right);
        return  left && right;
    }
}

我们来看看我们的递归代码,我们首先是定义了一个名为node的成员变量用于记录上一个结点。然后我们的终止条件是当结点为空时,我们返回true。然后我们每次都先往左子树上前进,先前进到底部,然后一旦底部没有值,我们就返回true,然后程序回到第一个结点,也就是最左边的结点,到达最左节点时,我们的node是没有值的,会跳过比较,然后会给对应的结点记录上该结点的值,这里我们的node设置在比较之后的含金量就体现出来了。然后往右搜索,接着同样会进行先往左子结点前进的动作,这时我们的的往先往左递归的代码的含金量也体现出来了,到了第二个最左结点之后会进行同样的比较。然后我们在最后判断左右的两个判断是否同时为真,若有一个不为真就说明这不是二叉搜索树,此时结束递归。

不过我们这份代码其实可以优化,最简单的来说,如果我们的左边的判断已经不为真了,那么我们就没必要继续进行递归的判断,因此我们可以将我们的代码修改如下

kotlin 复制代码
class Solution {
    TreeNode node;
    public boolean isValidBST(TreeNode root) {
        if(root==null){
            return true;
        }
        boolean left = isValidBST(root.left);
        if(!left){
            return false;
        }
        if(node!=null && node.val>=root.val){
            return false;
        }
        node=root;
        boolean right = isValidBST(root.right);
        return right;
    }
}

上面这份代码就相当于是做了简单的一个剪枝,但是这一份代码其实还可以做进一步的简化,具体请看下面的究极简化后的版本

kotlin 复制代码
class Solution {
    TreeNode node;
    public boolean isValidBST(TreeNode root) {
        if(root==null) return true;
        boolean left = isValidBST(root.left);
        if(!left) return false;
        if(node!=null && node.val>=root.val) return false;
        node=root;
        return isValidBST(root.right);
    }
}

值得一提的是这一份代码的逻辑我们要牢牢记住,后续的题目我们还会用到这种逻辑来解题。接下来我们来看看迭代的版本的代码,我们这里总是先往左前进,然后往右边走,这其实就对应我们迭代里的中序遍历的代码,所以我们这里就用中序遍历的代码来完成本题,请看代码

ini 复制代码
class Solution {
    TreeNode node;
    public boolean isValidBST(TreeNode root) {
        if(root==null){
            return true;
        }
        Deque<TreeNode> deque = new ArrayDeque<>();
        while (root!=null || !deque.isEmpty()){
            if(root!=null){
                deque.add(root);
                root=root.left;
            }else {
                root=deque.pollLast();
                if(node!=null && node.val>=root.val){
                    return false;
                }
                node=root;
                root=root.right;
            }
        }
        return true;
    }
}

基本逻辑和递归的差不多,这里就不再赘述了

二叉搜索树的最小绝对差

那么经过了上一题的学习之后,我们就拿这一题来小试牛刀,首先我们来看看我们递归的代码

ini 复制代码
class Solution {
    TreeNode node;
    int ans = Integer.MAX_VALUE;
    public int getMinimumDifference(TreeNode root) {
        dfs(root);
        return ans;
    }

    private void dfs(TreeNode root) {
        if(root==null){
            return;
        }
        dfs(root.left);
        if(node!=null && root.val-node.val<ans){
            ans=root.val-node.val;
        }
        node=root;
        dfs(root.right);
    }
}

可以看到我们这里递归的主体思路跟上一题的一样,不过我们这里对我们的逻辑进行了进一步的简化,我们让我们的递归比较只通过上一个结点和这个节点的差值进行比较,最终得到我们想要的结果。不过实际上我们这份代码还能再改良,具体如下

ini 复制代码
class Solution {
    TreeNode node;
    int ans = Integer.MAX_VALUE;
    public int getMinimumDifference(TreeNode root) {
        if(root==null){
            return 0;
        }
        getMinimumDifference(root.left);
        if(node!=null && root.val-node.val<ans){
            ans=root.val-node.val;
        }
        node=root;
        getMinimumDifference(root.right);
        return ans;
    }
}

可以看到我们这个代码变得更加简洁了,我们这里采用的方法就是不获得其对应的返回值,利用这种方式来实现我们的目的。

最后我们来看看我们的迭代的代码

ini 复制代码
class Solution {
    TreeNode node;
    int ans = Integer.MAX_VALUE;
    public int getMinimumDifference(TreeNode root) {
        Deque<TreeNode> deque = new ArrayDeque<>();
        while (root!=null || !deque.isEmpty()){
            if(root!=null){
                deque.add(root);
                root=root.left;
            }else {
                root=deque.pollLast();
                if(node!=null && root.val-node.val<ans){
                    ans=root.val-node.val;
                }
                node=root;
                root=root.right;
            }
        }
        return ans;
    }
}

二叉搜索树中的众数

本节我们来学习我们的前置结点的递归使用的进一步使用法,首先关于这一题,我们的一个简单想法就是直接记录我们树中的结点数,然后遍历出出现次数最多的结点,然后将这些结点返回,那么我们可以构造其代码如下

ini 复制代码
class Solution {
    Map<Integer,Integer> map = new HashMap<>();
    public int[] findMode(TreeNode root) {
        dfs(root);
        List<Integer> list = new ArrayList<>();
        int max = Integer.MIN_VALUE;
        for (Integer i:map.keySet()) {
            if(map.get(i)>max){
                list=new ArrayList<>();
                list.add(i);
                max=map.get(i);
            }else if(map.get(i)==max){
                list.add(i);
            }
        }
        int[] arr = new int[list.size()];
        int index = 0;
        for (Integer i:list) {
            arr[index++]=i;
        }
        return arr;
    }

    private void dfs(TreeNode root) {
        if(root==null){
            return;
        }
        map.put(root.val,map.getOrDefault(root.val,0)+1);
        dfs(root.left);
        dfs(root.right);
    }
}

这份代码的好处是只要是二叉树就可以使用,但是其缺点是它的效率属实不算高,我们这里其实还给了其为二叉搜索树的条件的,因此我们需要使用我们其特点,结合前置结点递归法来实现一次遍历就达成目的的算法,请看其代码

ini 复制代码
class Solution {
    TreeNode node;
    int maxC;
    int count;
    List<Integer> list = new ArrayList<>();
    public int[] findMode(TreeNode root) {
        dfs(root);
        int[] arr = new int[list.size()];
        for (int i = 0; i < arr.length; i++) {
            arr[i]=list.get(i);
        }
        return arr;
    }

    private void dfs(TreeNode root) {
        if(root==null){
            return;
        }
        dfs(root.left);
        if(node!=null && node.val!=root.val){
            count=1;
        }else {
            count++;
        }
        if(count>maxC){
            list.clear();
            list.add(root.val);
            maxC=count;
        }else if(count==maxC){
            list.add(root.val);
        }
        node=root;
        dfs(root.right);
    }
}

我们这里其实也可以对我们的前置结点递归法进行一个总结了,前置节点法的核心思想在于回溯,我们总是会让我们的结点先进入到我们的尾部,在回溯的过程中我们对其处理,这个过程里,我们有一些流程是固定的,首先,我们总是会先放置我们的递归结束的条件判断代码,这个代码一般是结点为null时结束方法,然后总是会优先往左递归,接着进行对应的处理,处理时我们总是会运用到node!=null作为我们的比较条件之一,然后在处理结束后我们总是让我们的node去记录上一个结点,然后让我们的结点往右子树上前进一位。

那么我们接着要解决的问题就是如何进行对应的处理,在这个过程中我们首先需要一些用于判断的变量,这些变量我们都会将其放置到我们的成员变量中,这样便于我们的调用。我们这里注意看我们的处理过程,我们这里是判断当我们的node==null或者是我们记录的值和当前值不想等时,我们重置我们的计数,当其相等时,我们就令其增加一位。这里精妙的一点在于如果前置结点没有记录时,我们就立刻让我们的计数增加1,其动作代表的意义是我们将当前结点记录下来,这样结合下面的代码就可以实现对首次进入的结点的处理。我们以后的思路的一个重要一点也在于此,就是我们要确定我们的node不存在时,也就是我们第一次进入结点时我们的处理方式

最后我们的处理是进行对当前计数和最大计数的比较,如果超越最大计数我们就调用我们的集合的清空方法来清空我们的集合,然后我们将该值添加到集合中,接着更新最大计数,如果相等我们就继续再集合里添加我们对应的值。

这里我们的一个重要的思想在于,我们是一边统计一边添加的,不断更新维护对应的值,而不是数到最大之后我们才进行添加,这一点是我们的核心思想。

最后我们来看看我们的迭代的代码,这里用到了中序遍历,但是逻辑是一样的,这里就不赘述了

ini 复制代码
class Solution {
    TreeNode node;
    int maxC;
    int count;
    List<Integer> list = new ArrayList<>();
    public int[] findMode(TreeNode root) {
        Deque<TreeNode> deque = new ArrayDeque<>();
        while (root!=null || !deque.isEmpty()){
            while (root!=null){
                deque.add(root);
                root=root.left;
            }
            root = deque.pollLast();
            if(node!=null && node.val!= root.val){
                count=1;
            }else {
                count++;
            }
            if(count>maxC){
                list.clear();
                list.add(root.val);
                maxC=count;
            }else if(count==maxC){
                list.add(root.val);
            }
            node=root;
            root=root.right;
        }

        int[] arr = new int[list.size()];
        for (int i = 0; i < arr.length; i++) {
            arr[i]=list.get(i);
        }
        return arr;
    }
}

最后我们不妨来总结下我们学习过的这么多的树的异同

二叉树回溯进阶

本章节我们来学习进一步的回溯应用,以前的题目只能说是轻量级的,现在我们就要进入我们的重量级的题目的学习了

二叉树的最近公共祖先

首先关于这题,我们如果直接做,那么我们肯定很难,但是我们可以将这种题目一步步分解,这样就比较好做。首先,我们来确定最简单的一种其最近公共祖先的情况,就是如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。这个很好理解,我们的示例1就是这种情况,然后接下来我们要解决第二种情况,就是节点本身p(q),它拥有一个子孙节点q(p)。此时,我们作为参数的结点本身就是题目中的最近公共祖先,

我们的递归逻辑很简单,首先我们先递归处理所有的结点,每次递归我们寻找该结点是否为空或者是我们所设定的两个结点,如果是,我们就将该结点返回,具体为如果结点为空,返回空节点,不为空且与目标结点相等,则返回目标结点,都不满足,则继续递归。

这里我们的逻辑其实是回溯,我们是先往下递归到底层,最后全部递归完毕之后我们再来对其进行相应的逻辑处理。这里我们如果遇到了目标结点我们是要返回这个节点的,所以我们肯定需要返回值,这个返回值就是结点本身。我们这里可以再来复习一下我们之前学习过的,我们做递归时什么时候需要返回值什么时候不需要,需要与否又应该要怎样处理。

然后我们要解决的问题是,我们是要搜索一棵树呢,还是要搜索一条边呢?不过在哪之前,我们可以先来介绍下搜索一棵树和搜索一条边时我们的递归处理的代码的不同

容易看出在递归函数有返回值的情况下:如果要搜索一条边且递归函数返回值不为空,立刻返回,如果搜索整个树,则用一个变量left、right接住返回值,这然后eft、right后续还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(就是回溯)。

我们这里会选择第二种做法,为什么要选择这种做法呢?直观来看,似乎我们只要找一条边的答案然后返回这个答案就可以了,类似于下图

但最终我们还是选择了搜索所有的树,这是因为如果我们选择搜索一棵树,就做对应的逻辑处理,那么其就会返回一个只包含一棵所需结点的父结点给我们,而这个节点不一定是我们所需要的目标结点,而且我们的判断其是否为最近公共父节点的方式是通过两个搜索得到的结果最后来共同判断的,如果我们选择第一种方式,那么我们就无法进行这样的判断了,因为这时我们只有一个我们所需的目标结点,我们自然无法做对应的判断。

那么最后我们来做回溯的处理,我们在每一个结点中都去寻找其对应的结点,然后返回我们所需要的目标结点,问题是我们应该要怎么返回呢?这里面的逻辑处理是我们现在要实现的东西。这里同样要使用到分步思考的想法,首先我们来解决最简单的一种情况,如果left 和 right都不为空,说明此时root就是最近公共节点。这个很好理解,就不多提了。问题是,如果left为空,right不为空时,我们应该怎么办呢?答案其实是如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然。这是为什么呢?请看下图

具体我们来看第十的结点,此时其左边搜索不到,但是右边搜索到了一个合适的结点7,此时就返回右节点7,这也是为什么我们当一边为空时,我们要返回另一边的结点,这个内部逻辑要搞清楚

当然还有最后一种情况,就是左右都为空时,此时我们返回空就完了,这个很好理解。

最后我们来看看们的完整的递归回溯过程,如下图所示,可以看到我们是通过回溯完成的搜索,且无论我们搜索到答案与否,我们总是要对所有结点进行搜索,并且将结果返回给最初的结点

那么我们可以构造我们的代码如下

kotlin 复制代码
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null){
            return null;
        }
        if(root==p){
            return p;
        }
        if(root==q){
            return q;
        }
        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);

        if(left==null && right==null){
            return null;
        }else if(left!=null && right==null){
            return left;
        }else if(left==null && right!=null){
            return right;
        }else {
            return root;
        }
    }
}

这一份代码还可以继续精简,其精简代码如下,就是将最初的结束条件总和到了一起

kotlin 复制代码
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 && right == null) { // 若未找到节点 p 或 q
            return null;
        }else if(left == null && right != null) { // 若找到一个节点
            return right;
        }else if(left != null && right == null) { // 若找到一个节点
            return left;
        }else { // 若找到两个节点
            return root;
        }
    }
}

最后是一个没啥意义的只追求短的代码

sql 复制代码
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 && right==null) return null;
        else if(left!=null && right==null) return left;
        else if(left == null) return right;
        else return root;
    }
}

这个思路,看多了自然也就理清了。这个时候有的同学会说,你这谁寄吧会啊,我承认,的确未必做了这题就会以后的题目,但起码我们能学个思路不是,下次遇上这种题目起码有个大概感觉是吧

迭代的代码就不给了,迭代我不知道怎么搞出这种回溯的效果来说实话。

二叉搜索树的最近公共祖先

解决完了第一题之后,我们现在来解决其简单版本的题目,虽然说是简单版本,但如果希望效率达到百分百,那还是不容易的。我们这里要充分利用其实二叉搜索树的特性,那么在二叉搜索树的情况下,其最近的公共父节点又是哪个呢?其实只要节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。根据这个思路,我们很容易就可以做出我们的递归代码如下

kotlin 复制代码
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;
    }
}

如果要加深理解的话,我们不妨对着这张图来看看,自己演示一遍就能理解了

最后我们来看看其迭代的代码,非常好理解这个,不多说了

kotlin 复制代码
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null || root==p || root==q){
            return root;
        }
        Deque<TreeNode> deque = new ArrayDeque<>();
        deque.add(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.pollLast();
            if(node.val<p.val && node.val<q.val){
                deque.add(node.right);
            }else if(node.val> p.val && node.val> q.val){
                deque.add(node.left);
            }else {
                return node;
            }
        }
        return root;
    }
}

二叉树搜索树中的插入操作

这个其实没什么特别好说的,这个很好做其实,几乎没什么难度可言,直接看代码吧

首先我们来看看递归代码

kotlin 复制代码
class Solution {
    int val;
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if(root==null){
            return new TreeNode(val);
        }
        this.val=val;
        dfs(root);
        return root;
    }

    private void dfs(TreeNode root) {
        if(root==null){
            return;
        }
        if(root.val<val){
            if(root.right!=null){
                dfs(root.right);
            }else {
                root.right=new TreeNode(val);
            }
        }else {
            if(root.left!=null){
                dfs(root.left);
            }else {
                root.left=new TreeNode(val);
            }
        }
    }
}

然后我们来看看迭代的代码

kotlin 复制代码
class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if(root==null){
            return new TreeNode(val);
        }
        TreeNode node = root;
        while (true){
            if(node.val<val){
                if(node.right!=null){
                    node=node.right;
                }else {
                    node.right=new TreeNode(val);
                    break;
                }
            }else {
                if(node.left!=null){
                    node=node.left;
                }else {
                    node.left=new TreeNode(val);
                    break;
                }
            }
        }
        return root;
    }
}

最后是通过构造新的二叉树来完成本题的代码

scss 复制代码
class Solution {
    TreeNode ans = null;
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if(root==null){
            return new TreeNode(val);
        }
        Deque<TreeNode> deque = new ArrayDeque<>();
        deque.add(root);
        while (!deque.isEmpty()){
            TreeNode node = deque.poll();
            ans = dfs(ans, node.val);
            if(node.left!=null){
                deque.add(node.left);
            }
            if(node.right!=null){
                deque.add(node.right);
            }
        }
        dfs(ans,val);
        return ans;
    }

    private TreeNode dfs(TreeNode node, int val) {
        if(node==null){
            return new TreeNode(val);
        }
        if(node.val<val){
            node.right=dfs(node.right,val);
        }else {
            node.left=dfs(node.left,val);
        }
        return node;
    }
}

删除二叉搜索树中的结点

对于这一题,我们的做法是要分情况讨论,具体分的情况如下

这五种情况就是囊括了我们删除结点时能遇到的所有情况了,对于第五种情况,可能光看文字可能会有点难以理解,我们来看个动图演示吧

那么最终我们可以写入我们的代码如下

ini 复制代码
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return root;
        if (root.val == key) {
            if (root.left == null) {
                return root.right;
            } else if (root.right == null) {
                return root.left;
            } else {
                TreeNode cur = root.right;
                while (cur.left != null) {
                    cur = cur.left;
                }
                cur.left = root.left;
                root = root.right;
                return root;
            }
        }
        if (root.val > key) root.left = deleteNode(root.left, key);
        if (root.val < key) root.right = deleteNode(root.right, key);
        return root;
    }
}

可以看到我们这里对我们的情况进行了分类讨论,并且进行了分类处理,利用其二叉搜索树的特性来决定我们的递归的走向

但是实际上,二叉搜索树不止一种,我们实际上还有另外一种处理本题的第五种情况的方式,就是将当前结点的值改为右子树的最左子树的结点值,然后将右子树的最左子树删除。根据这个思路,我们可以写入我们的代码如下

ini 复制代码
class Solution {
    int val;
    public TreeNode deleteNode(TreeNode root, int key) {
        val=key;
        return dfs(root);
    }

    private TreeNode dfs(TreeNode root) {
        if(root==null){
            return null;
        }
        if(root.val<val){
            root.right=dfs(root.right);
        }else if(root.val>val){
            root.left=dfs(root.left);
        }else {
            if(root.left==null && root.right==null){
                return null;
            }else if(root.left==null){
                return root.right;
            }else if(root.right==null){
                return root.left;
            }else {
                TreeNode right = root.right;
                while (right.left!=null){
                    right=right.left;
                }
                root.val= right.val;
                int num = val;
                val= root.val;
                root.right=dfs(root.right);
                val=num;
            }
        }
        return root;
    }
}

可以看到我们这里处理第五种情况时将对应结点删除的方式就是再次调用原函数就完了,同时为了让其能够寻找对应的值进行了一个值的改变和回调,如果我们采用传值的方式来进行递归的话,都不用进行值的特殊处理。如下所示

ini 复制代码
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        root = delete(root,key);
        return root;
    }

    private TreeNode delete(TreeNode root, int key) {
        if (root == null) return null;

        if (root.val > key) {
            root.left = delete(root.left,key);
        } else if (root.val < key) {
            root.right = delete(root.right,key);
        } else {
            if (root.left == null) return root.right;
            if (root.right == null) return root.left;
            TreeNode tmp = root.right;
            while (tmp.left != null) {
                tmp = tmp.left;
            }
            root.val = tmp.val;
            root.right = delete(root.right,tmp.val);
        }
        return root;
    }
}

修建二叉搜索树

我们这里一题简单来说就是删除的题目的进阶版本,以前我们是删除对应的结点,这回我们是要将不在范围内的结点删除掉而已。我们的一个简单想法就是遇到不在范围内的结点我们就返回null,否则就继续搜索,这个想法虽然很简单,但这是行不通的,具体请看下面的例子

如果我们是遇上不符合的内容就直接返回的话,那么最终我们的结点就是3-4,啥都没了,这肯定是不符合要求的。

其实我们不需要对我们的所有结点进行修改,如果我们遇上了不符合的结点,我们就直接跳过这个节点,将其子节点赋予给其上一个结点就行了。换言之,我们做本题并不需要我们的去做手动的删除,我们只需要改变树中结点的指向,令所有不符合的结点不被指向就行了。

那么按照这个思路我们可以构造我们的代码如下

kotlin 复制代码
class Solution {
    int low,high;
    public TreeNode trimBST(TreeNode root, int low, int high) {
        this.low=low;this.high=high;
        return dfs(root);
    }

    private TreeNode dfs(TreeNode root) {
        if(root==null){
            return null;
        }
        if(root.val<low){
            return dfs(root.right);
        }
        if(root.val>high){
            return dfs(root.left);
        }
        root.left=dfs(root.left);
        root.right=dfs(root.right);
        return root;
    }
}

我们这里的递归逻辑是先对结点为空时,我们就令其返回空,这是停止条件,若结点不属于边界返回内,我们就令其返回边界值内的结点,所以我们这里的递归是当前结点比low小我们就递归处理右子树,反之则是左子树。最后如果属于在其中,我们就不做什么处理,直接递归处理器左右子树,最后返回该结点

将有序数组转换为二叉搜索树

我们这一题要是想不明白那就寄了,要是想明白了那其实不难。我们这题本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。我们只要确定我们的递归处理的逻辑就可以实现对所有的结点进行一个创建的过程了,我们这里有两种递归方式,一种是左闭右闭一种是左闭右开,每次我们都是先令其获得中间的结点,然后左右分别递归获得对应的结点就行了,左闭右闭和左闭右开的方式分别对应的是每次递归处理时选择的结点不同,其最大的不同就在于数组长度为偶数时,决定我们选择左边的结点来创建还是右边的结点来创建,但无论是那种方式,都是可以通过的,因为我们的二叉平衡搜索树本身就是不止一个的

那么我们可以写入我们的左闭右闭的代码如下

ini 复制代码
class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        return dfs(null,nums,0,nums.length-1);
    }

    private TreeNode dfs(TreeNode node, int[] nums, int len, int right) {
        if(len>right){
            return null;
        }
        int mid = len+(right-len)/2;
        TreeNode root = new TreeNode(nums[mid]);
        root.left=dfs(root.left,nums,len,mid-1);
        root.right=dfs(root.right,nums,mid+1,right);
        return root;
    }
}

然后我们写入左闭右开的代码如下

ini 复制代码
class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        return dfs(nums,0,nums.length);
    }

    private TreeNode dfs(int[] nums, int len, int right) {
        if(len>=right){
            return null;
        }
        int mid = len+(right-len)/2;
        TreeNode root = new TreeNode(nums[mid]);
        root.left=dfs(nums,len,mid);
        root.right=dfs(nums,mid+1,right);
        return root;
    }
}

把二叉搜索树转换为累加树

这题我们的经典想法就是利用回溯来做,先获得左右然后再处理中间结点,然而这种思路就寄了,因为这里的递归逻辑其实是先处理右子树,再处理中间结点,最后再处理左子树。我们用回溯压根做不到这样的,如果我们用参数传参来进行递归的话,那又不知道怎么样将值传到我们的递归的左子树中去,就突出一个痛苦,那我们应该要怎么办呢?答案是利用成员变量,我们这里只能使用成员变量来承接我们的值,来实现我们的右中左的递归思路,其他的方法都做不到(反正我是试不出来)

那么我们可以写入我们的递归代码如下

ini 复制代码
class Solution {
    int sum = 0;
    public TreeNode convertBST(TreeNode root) {
        dfs(root);
        return root;
    }

    private void dfs(TreeNode root) {
        if(root==null){
            return;
        }
        dfs(root.right);
        sum+= root.val;
        root.val=sum;
        dfs(root.left);
    }
}

那么到此为止,我们的二叉树的题目就算是全部学习完了,可喜可贺可喜可贺

面试题,后继者

最后我们来做一道有关于二叉树的面试题,关于这题,我只能说这位更是重量级

相关推荐
2401_895521346 小时前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare7 小时前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL7 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本9 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
yhole12 小时前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉12 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
白毛大侠12 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet13 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin
大阿明13 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端
Binary-Jeff13 小时前
Spring 创建 Bean 的关键流程
java·开发语言·前端·spring boot·后端·spring·学习方法