数据结构之树专题

基础知识

树的概念

参考博客1
参考博客2
参考博客3
参考博客4
树(tree)包含 n(n≥0) 个节点(当 n=0 时,称为空树)。其中非空树是由n(n≥1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。另外,也可以说树是由根节点和若干颗子树构成的。
在这里插入图片描述
相关术语

  • 结点:使用树结构存储的每一个数据元素都被称为“结点”。图中,数据元素 A 就是一个结点;
  • 父结点(双亲结点)、子结点和兄弟结点:对于图中的结点 A、B、C、D ,A 是 B、C、D 结点的父结点(也称为“双亲结点”),而 B、C、D 都是 A 结点的子结点(也称“孩子结点”)。对于 B、C、D 来说,它们都有相同的父结点,所以它们互为兄弟结点。
  • 树根结点(简称“根结点”):每一个非空树都有且只有一个被称为根的结点。图中,结点A就是整棵树的根结点。树根的判断依据为:如果一个结点没有父结点,那么这个结点就是整棵树的根结点。
  • 叶子结点:如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。图 中,结点 K、L、F、G、M、I、J 都是这棵树的叶子结点。
  • 子树:除了根节点外,每个子节点可以看作多个不相交的子树。
  • 节点的度(degree):该节点子树的个数称为该节点的度。如A的度为3。
  • 树的度:所有节点中,度的最大值称为树的度。如图中树的度为3。
  • 非叶子节点:度不为零的节点。
  • 内部节点与外部节点:只有到达而没有出发的为外部节点,即叶子节点,有出发的为内部节点。如图中F,G,I,J,K,L,M为外部节点,其余节点都为内部节点。
  • 高度(height):当前节点到最远叶子节点的路径长,所有叶节点的高度为零。高度是从下往上数的,如图中D节点,有D-H-M(3),D-I(2),D-J(2)三条路径,取子树中最远的那个叶子结点作为1往上数到D节点,即D节点的高度为3。参考博客:高度与深度的区别
  • 深度(depth):对于任意节点n,n的深度为从根到n的唯一路径长。深度是从上往下数的,例如图中节点E的深度为3,D节点为2(有些认为根深度为0,有些认为根深度为1,这里取1)。注意:对树而言,其高度和深度是相等的;对树中的某一节点而言,其高度和深度可能不等。
  • 节点的层数(level):从根开始定义,根为第一层,根的子节点为第二层。以此类推。
  • 堂兄弟节点:父节点在同一层的节点互为堂兄弟。图中的EFGHIJ互为堂兄弟节点。
  • 节点的祖先(ancestor):从根到该节点所经分支上的所有节点。
  • 子孙(descendant):以某节点为根的子树中任一节点都称为该节点的子孙。
  • 森林:由m(m >= 0)棵互不相交的树的集合称为森林。

二叉树

概念:每个节点最多含有两个子树的树称为二叉树(一般在试题中见到的树是二叉树,但并不意味着所有的树都是二叉树)。 高度为h的二叉树至多有2^h - 1个节点,此时为满二叉树。

满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。即除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。 满二叉树中叶子节点必然比内部节点的个数多1个。

完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边(依次从左到右分布),这就是完全二叉树。
在这里插入图片描述

二叉查找树

二叉查找树:Binary Search Tree (BST),又称二叉排序树、二叉搜索树,二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:

  • 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
  • 左、右子树也分别为二叉排序树;
  • 没有键值相等的节点。
    在这里插入图片描述

二叉查找树的特点:对二叉查找树进行中序遍历,即可得到有序的数列
二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡,极端情况下会退化为链表,因此出现AVL、红黑树。

平衡二叉树:Balanced Binary Tree(BBT),又被称为AVL树。它或者是一棵空树,或者是具有下列性质的二叉树:

  • 任意节点的子树的高度差都小于等于1;
  • 任何一个节点的左子树与右子树都是平衡二叉树;
  • 二叉树节点的平衡因子定义为该节点的左子树的深度减去右子树的深度。则平衡二叉树的所有节点的平衡因子只可能是-1,0,1。
  • 平衡二叉树是二叉排序树,具有相应的特点(左子树任一节点小于父节点,右子树任一节点大于父节点);
    在这里插入图片描述
    AVL树很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

红黑树:红黑树是一种自平衡的二叉查找树,是一种高效的查找树,具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。因此,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的
红黑树属于二叉查找树范畴,具备相应的特点(左子树任一节点小于父节点,右子树任一节点大于父节点),但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近似于平衡的。
树中每个结点包含5个属性:color、key、left、right和p。如果一个结点没有子节点或父节点,则该结点相应的指针属性值为NIL,我们可以把这些NIL视为指向二叉搜索树的叶节点(外部结点)的指针,而把带关键字的结点视为树的内部结点。
一棵红黑树是满足下面性质(实现自平衡)的二叉搜索树:
1)每个结点或是红色的,或是黑色的;
2)根结点是黑色的;
3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑色的;
4)如果一个结点是红色的,则它的两个子结点都是黑色的(每个叶子节点到根的所有路径上不能有两个连续的红色节点);
5)对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点(简称黑高)。
在这里插入图片描述
红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。如性质4和性质5就保证了任意节点到其每个叶子节点路径最长不会超过最短路径的2倍,
参考博客【必看】
参考博客【美团技术点评团队】

B树

目前大部分数据库系统及文件系统都采用 B-树或其变种 B+树 作为索引结构。参考:B+Tree原理及mysql的索引分析【必看】
B树:又称B-树,是一种平衡多路查找树,它在文件系统中很有用。一棵m阶B-树(图为4阶B-树),具有下列性质:
(1)树中每个节点至多有m棵子树;
(2)若根节点不是叶子节点,则至少有2棵子树;
(3)除根节点之外的所有非终端节点至少有棵子树;
(4)每个节点中的信息结构为(A0,K1,A1,K2…Kn,An),其中n表示关键字个数,Ki为关键字,Ai为指针;
(5)所有的叶子节点都出现在同一层次上,且不带任何信息,也是为了保持算法的一致性
在这里插入图片描述
B+树:B+数是B-树的一种变形,图为3阶B+树,它与B-树的差别在于:
(1)有n棵子树的节点含有n个关键字;
(2)所有的叶子节点包含了全部关键字的信息,及指向这些关键字记录的指针,且叶子节点本身按关键字大小自小到大顺序链接;
(3)所有非终端节点可以看成是索引部分,节点中仅含有其子树(根节点)中最大(或最小)关键字,所有B+树更像一个索引顺序表;
(4)对B+树进行查找运算,一是从最小关键字起进行顺序查找,二是从根节点开始,进行随机查找。
在这里插入图片描述

前缀树

Trie,又称字典树,单词查找树,是一种以树形结构保存大量字符串。以便于字符串的统计和查找,经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。具有以下特点:
(1)根节点为空;
(2)除根节点外,每个节点包含一个字符;
(3)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
(4)每个字符串在建立字典树的过程中都要加上一个区分的结束符,避免某个短字符串正好是某个长字符串的前缀而淹没。
在这里插入图片描述

在这里插入图片描述
208. 实现前缀树

二叉树的遍历方式

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

  • 深度优先遍历:先往深走,遇到叶子节点再往回走。 借助栈

    • 前序遍历(递归法,迭代法) 中左右
    • 中序遍历(递归法,迭代法) 左中右
    • 后序遍历(递归法,迭代法) 左右中
      在这里插入图片描述
  • 广度优先遍历:一层一层的去遍历。 借助队列

    • 层次遍历(迭代法) 第102题

这里涉及到两种算法思想:DFS(Deep First Search)深度优先搜索 和 BFS(Breath First Search)广度优先搜索。BFS和DFS的简单解释

参考博客

解题技巧

1、引入一个队列/栈,是把递归程序改写成迭代程序的常用方法。

  • 前序(中左右)、中序(左中右)、后序(左右中)遍历迭代版一般借助栈实现
  • 层序遍历迭代版一般借助队列来实现

2、树的题必须掌握四种遍历方式,前序(中左右)、中序(左中右)、后序(左右中)、层序,之后的题基本都是建立在这基础上进行的。

3、一般来说,使用前序遍历(中左右)是从上到下去查所以求的深度【不常用】;而使用后序遍历(左右中)是从下到上去查所以求的高度【递归法常用】,而把每个节点当做根节点,其通过后序遍历得到的高度也就是当前节点的深度;

4、树的最大深度等同于树的最大高度,如果要求使用迭代法,最好就使用层序遍历,104题。

5、一般情况下,只使用前序遍历得到的结果并不能反向唯一确定一棵二叉树,需要前序遍历+中序遍历才可以唯一确定二叉树,第105题;如果非要使用前序遍历,可以把空节点也加入遍历结果中,第297题。

题目练习

144. 二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
在这里插入图片描述

代码实现

递归法

class Solution {
    //递归版   时间复杂度为O(n) 空间复杂度为O(n)
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        dfs(root, result);
        return result;
    }

    public void dfs(TreeNode root, List<Integer> result){
        //递归终止条件
        if(root == null){
            return;
        }
        //单层递归逻辑  前序遍历 中左右
        result.add(root.val);
        dfs(root.left, result);
        dfs(root.right, result);
    }
}

迭代法

class Solution {
    //迭代版  时间复杂度为O(N) 空间复杂度为O(N)
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if(root == null){
            return result;
        }
        //借助栈实现   前序遍历:中左右   入栈顺序;中右左
        Deque<TreeNode> stk = new LinkedList<>();
        stk.push(root);
        
        while(!stk.isEmpty()){
            //访问并处理中
            TreeNode node = stk.pop();
            result.add(node.val);
            //右先入栈,后出栈,实现后处理右
            if(node.right != null){
                stk.push(node.right);
            }
            //左后入栈,先出栈,实现先处理右
            if(node.left != null){
                stk.push(node.left);
            }
        }
        return result;
    }
}

145. 二叉树的后序遍历

题目链接

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。
在这里插入图片描述

题目分析

二叉树的后序遍历:左右中

代码实现

递归法

class Solution {
    //递归版   时间复杂度为O(n) 空间复杂度为O(n)  
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        dfs(root, result);

        return result;
    }

    public void dfs(TreeNode root, List<Integer> result){
        //递归终止条件
        if(root == null){
            return;
        }

        //单层递归业务   后序:左右中
        dfs(root.left, result);
        dfs(root.right, result);
        result.add(root.val);
    }
}

迭代法

class Solution {
    //迭代版  
    //先压左子树入栈(直到为空,实现先左)
    //然后弹出栈中节点(此时为最左的叶子节点),右子树为空(实现再右),再处理当前节点(实现后中)
    //然后往上,弹出父节点(已实现先左),右节点不为空先处理右(实现再右),右节点处理后再处理当前节点
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if(root == null){
            return result;
        }

        //借助栈实现  后序:左右中   
        Deque<TreeNode> stk = new LinkedList<>();
        TreeNode prev = null;
        while(root != null || !stk.isEmpty()){
            //先左
            while (root != null) {
                stk.push(root);
                root = root.left;
            }
            //然后右   
            root = stk.pop();
            //右子树为空或上一处理的节点为当前节点的右子树(保证右在中前)时 再处理当前节点
            if (root.right == null || root.right == prev) {
                result.add(root.val);
                //记录当前节点
                prev = root;
                //恢复root为空,避免再次入栈,陷入死循环
                root = null;
            } else {   //不为空,则先处理右子树
                stk.push(root);
                root = root.right;
            }

        }
        return result;
    }
}

//后序遍历:左-右-中  入栈顺序:中-左-右 出栈顺序:中-右-左,最后翻转
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null){
            return result;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        //右后入栈先出栈,因此顺序满足中右左
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();  //中
            result.add(node.val);
            if (node.left != null){
                stack.push(node.left);   //左
            }
            if (node.right != null){
                stack.push(node.right);  //右
            }
        }
        //翻转后:左右中
        Collections.reverse(result);
        return result;
    }
}

104. 二叉树的最大深度

题目链接

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

题目分析

二叉树的最大深度就是根的高度。
递归法:可以使用前序遍历,也可以使用后序遍历;
迭代法:使用BFS更方便,有多少层就是对应根的深度

代码实现

递归法

class Solution {
    //DFS  递归法实现后序遍历 时间复杂度O(n) 空间复杂度O(n)
    public int maxDepth(TreeNode root) {
        //递归终止条件
        if(root == null) return 0;

        //单层递归 左右子树中最大深度+当前节点=当前节点深度
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }
}

迭代法

class Solution {
    //BFS 层序遍历 迭代法
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int depth = 0;
        int len = 0;
        //遍历每一层 
        while(!queue.isEmpty()){
            len = queue.size();
            depth++;
            for(int i = 0; i < len; i++){
                //下一层节点入队
                TreeNode node = queue.poll();
                if(node.left != null) queue.offer(node.left);
                if(node.right != null) queue.offer(node.right);
            }
        }  
        return depth;
    }
}

98. 验证二叉搜索树

题目跳转

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
在这里插入图片描述

题目分析

法一:递归
根据BST的定义,可以知道根节点x的取值范围为(-∞,+∞),其左子树y取值范围为(-∞,x),右子树取值范围为(x,+∞),左子树y的左子树的取值范围为(-∞,y),,,因此我们可以据此判断各节点是否在取值范围内从而判断是否为合法的二叉搜索树。
在这里插入图片描述
设计一个递归函数helper,对以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r)的范围内(注意是开区间),如果 root 节点的值 val 不在 (l,r)的范围内说明不满足条件直接返回,否则继续递归检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。

根据二叉搜索树的性质,在递归调用左子树时,上界为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。

同理递归调用右子树时,下界为 root.val,即调用 helper(root.right, root.val, upper)。

函数递归调用的入口为 helper(root, -inf, +inf), inf 表示无穷大。

时间复杂度 : O(n), n 为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次。
空间复杂度 : O(n),其中 n 为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 n ,递归最深达到 n 层,故最坏情况下空间复杂度为O(n) 。

注意:节点的取值范围可能等于Integer的最大最小值,因此要使用Long

法二:中序遍历
中序遍历是二叉树的一种遍历方式,它先遍历左子树,再遍历根节点,最后遍历右子树。而我们二叉搜索树保证了左子树的节点的值均小于根节点的值,根节点的值均小于右子树的值,因此中序遍历以后得到的序列一定是升序序列。

时间复杂度 : O(n),其中 n 为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
空间复杂度 : O(n),其中 n 为二叉树的节点个数。栈最多存储 n 个节点,因此需要额外的O(n) 的空间。

代码实现
/**
 * 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 {
    public boolean isValidBST(TreeNode root) {
    	//注意初始取值范围用Long
        return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    //递归判断左右子树是否在取值范围(lower,upper)内
    public boolean dfs(TreeNode root, long lower, long upper){
        //递归终止条件 节点为空返回true,
        // 因为在下方递归时有&&,一边为正还需判断另一边,而只要一边为负就直接为负
        if(root == null) return true;
         
        //递归核心业务 判断节点是否在取值范围内
        if(root.val <= lower || root.val >= upper) return false;
        

        //子树更新为“根节点”,判断其左右子树是否满足 
        return dfs(root.left, lower, root.val) && dfs(root.right, root.val, upper);
    }
}

代码流程如下: 设root = [5,1,4,null,null,3,6] 遍历顺序是 1,5,3,4,6
1、根节点5进入方法,不为空,访问左子树1;节点1进入方法,不为空,访问左子树,左子树进入方法,为空,返回true,到达第2层递归(当前节点为1),取非为FALSE;进入访问当前节点语句,此时当前节点为1 ,大于 为Long.MIN_VALUE的pre;进入pre=root.val,pre更新为1;开始访问当前节点1的右子树,右子树为空,返回true,回到第1层(当前节点为5);
2、取非为FALSE;进入访问当前节点语句,此时当前节点为5 ,大于pre=1;进入pre=root.val,pre更新为5;开始访问当前节点5的右子树,右子树为4;节点4进入方法(第2层),不为空,访问左子树,左子树进入方法,为3;节点3进入方法(第3层),不为空,访问左子树,左子树进入方法,为空,返回true,到达第3层递归(当前节点为3),取非为FALSE;进入访问当前节点语句,此时当前节点为3 ,小于pre=5,返回FALSE,即不是合法的二叉搜索树,因为根节点的右子树中出现小于根节点的节点

法二:

class Solution {
    long pre = Long.MIN_VALUE;
    public boolean isValidBST(TreeNode root) {
        if (root == null) {
            return true;
        }
        // 访问左子树
        if (!isValidBST(root.left)) {
            return false;
        }
        // 访问当前节点:如果当前节点小于等于中序遍历的前一个节点,说明不满足BST,返回 false;否则继续遍历。
        if (root.val <= pre) {
            return false;
        }
        pre = root.val;
        // 访问右子树
        return isValidBST(root.right);
    }
}

94. 二叉树的中序遍历 ***

题目链接

给定一个二叉树的根节点 root ,返回它的 中序 遍历。
在这里插入图片描述

题目分析

中序遍历:先遍历左子树,再遍历根节点,最后遍历右子树。

有递归和迭代两种实现方式。实质上递归函数可以用迭代的方式实现,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而迭代的时候需要显式地将这个栈模拟出来,栈其实就是递归的一种实现结构,其他都相同。

代码实现
class Solution {
    //递归法  时间复杂度为O(n) 空间复杂度为O(h)=O(n) h为数的高度,最坏时为O(n)
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        inorder(root, res);
        return res;
    }

    //递归实现中序遍历
    public void inorder(TreeNode root, List<Integer> res){
        //递归终止条件
        if(root == null) return;

        inorder(root.left, res);
        res.add(root.val);
        inorder(root.right, res);
    }
}

迭代法借助栈实现中序遍历,举例说明:
在这里插入图片描述

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        //借助栈通过迭代模拟实现递归
        Deque<TreeNode> stk = new LinkedList<TreeNode>();
        
        //当根节点不为空或者栈不为空时
        while(root != null || !stk.isEmpty()){
            //当根节点不为空时,将其左子树压入栈中,更新"根节点",继续压入
            while(root != null){
                stk.push(root);
                root = root.left;
            }
            //弹出栈顶元素 
            // 叶子节点左为空,因此不再压入,弹出叶子节点,从而实现先访问左子树,再访问当前节点
            root = stk.pop();
            res.add(root.val);
            //然后访问右子树 对当前节点的右子树的左子树同理压入栈
            //叶子节点右为空,因此弹出栈顶元素,实现对叶子节点作为其左子树的节点访问
            //来到其右子树,压入栈,如果也为叶子节点,则同理弹出,栈中不为空则继续弹出栈顶元素,以此类推。 
            root = root.right;

        }
        return res;
    }
}

101. 对称二叉树

题目链接

给定一个二叉树,检查它是否是镜像对称的。
在这里插入图片描述

你可以运用递归和迭代两种方法解决这个问题吗?

题目分析

在这里插入图片描述
递归法:
实现这样一个递归函数,通过「同步移动」两个指针的方法来遍历这棵树,p 指针和 q 指针一开始都指向这棵树的根,随后 p 右移时,q 左移,p 左移时,q 右移。每次检查当前 p 和 q 节点的值是否相等,如果相等再继续判断左右子树是否对称。
设树上一共 n 个节点。时间复杂度:这里遍历了整棵树,时间复杂度为O(n);空间复杂度:空间复杂度和递归使用的栈空间有关,递归层数不超过 n,故空间复杂度为O(n)。

迭代法:
引入一个队列/栈,是把递归程序改写成迭代程序的常用方法。

这里引入一个队列,初始化时我们把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像);然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者我们检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。 代码实现参见LeetCode官方,但需注意官方代码中实际上了遍历了两次树,应该

public boolean isSymmetric(TreeNode root) {
   return check(root, root);
}
//修改为
public static boolean isSymmetric(TreeNode root) {
   if(root == null) return true;
   return check(root.left, root.right);
}

或者引入两个栈实现对根节点的左子树的中序遍历(左右中)和右子树的反中序遍历(右中左)同时完成判断。

时间复杂度:O(n)
空间复杂度:需要用队列/栈来维护节点,每个节点最多进队一次,出队一次,队列中最多不会超过 n 个点,故空间复杂度为O(n)

代码实现

递归法:

class Solution {
    public boolean isSymmetric(TreeNode root) {
        //单独讨论根节点为空的特殊情况
        if(root == null) return true;
        //递归法
        return dfs(root.left, root.right);

    }
    //初始时设置两个指针p,q指向根节点,判断符合后继续判断子树
    public boolean dfs(TreeNode p, TreeNode q){
        //都为空满足条件
        if(p == null && q == null) return true;
        //其中一个为空则不满足条件
        if(p == null || q == null) return false;

        //不为空值相等则满足条件 并继续递归判断其子树  p左q右,p右q左
        return p.val == q.val && dfs(p.left, q.right) && dfs(p.right, q.left);
    }
}

迭代法:
借助队列

/**
 * 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 {
    public boolean isSymmetric(TreeNode root) {

        Deque<TreeNode> queue = new LinkedList();
        queue.offer(root.left);
        queue.offer(root.right);

        while(!queue.isEmpty()){
            TreeNode u = queue.poll();
            TreeNode v = queue.poll();

            if(u == null && v == null) continue;

            if(u == null || v == null || u.val != v.val) return false;

            queue.offer(u.left);
            queue.offer(v.right);

            queue.offer(u.right);
            queue.offer(v.left);
        }
        
        return true;
    }
}

借助栈

class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;
        //两个栈分别存储两条边的节点  实现对根节点的左子树的中序遍历和右子树的反中序遍历
        Deque<TreeNode> leftStk = new LinkedList<TreeNode>();
        Deque<TreeNode> rightStk = new LinkedList<TreeNode>();

        //
        TreeNode leftNode = root.left, rightNode = root.right;
        while(leftNode != null || rightNode != null || !leftStk.isEmpty() || !rightStk.isEmpty()){
            //不断压入左条边节点进左栈,右条边节点进右栈,直至有节点为空
            while(leftNode != null && rightNode != null){
                leftStk.push(leftNode);
                leftNode = leftNode.left;
                rightStk.push(rightNode);
                rightNode = rightNode.right;
            }
            //退出循环后还存在有一节点不为空则必然不对称
            if(leftNode != null || rightNode != null) return false;

            //弹出栈顶元素,从下往上对比两条边上的节点
            leftNode = leftStk.pop();
            rightNode = rightStk.pop();
            if(leftNode.val != rightNode.val) return false;

            //节点为空时直接弹出栈中元素,节点不为空时压入当前节点的另一颗子树
            leftNode = leftNode.right;
            rightNode = rightNode.left;
        }
        return true;
    }
}

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

题目链接

给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点
在这里插入图片描述

题目分析

前序:中左右 中序:左中右
通过前序遍历可以确定根节点,在中序遍历中根节点左边就是左子树,右边就是右子树,由此可以通过递归构建整棵树,如下:
在这里插入图片描述
递归时确定左子树和右子树在前序遍历和中序遍历中的边界:

  • 左子树 preLeft + 1, preLeft + lenLeftSubtree, inleft, rootInorder - 1
    • 前序遍历数组中:[前序遍历左边界 + 1,前序遍历左边界+左子树长度] ,其中左子树长度=根节点在中序遍历数组中的位置-inleft+1-1;
    • 中序遍历数组中:[中序遍历左边界 ,根节点位置 - 1]
  • 右子树 preLeft + lenLeftSubtree + 1, preRight, rootInorder + 1, inRight
    • 前序遍历数组中:[前序遍历边界+左子树长度 + 1,前序遍历右边界];
    • 中序遍历数组中:[根节点位置 + 1 ,中序遍历右边界]

时间复杂度的改善点在于如何快速在前序遍历中找到根节点,可以使用哈希表快速定位根节点在中序遍历中的位置。

时间复杂度O(n),空间复杂度O(n)。

代码实现
class Solution {
     //前序:中左右  中序:左中右
    //通过前序遍历可以确定根节点,在中序遍历中根节点左边就是左子树,右边就是右子树,由此可以通过递归构建整棵树
    //每个节点都会遍历一次,时间复杂度的改善点在于如何快速在前序遍历中找到根节点,可以使用哈希表
    //时间复杂度O(n),空间复杂度O(n),证明见官方题解

    //哈希表便于在中序遍历中定位根节点
    Map<Integer, Integer> indexMap = new HashMap<>();

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int n = preorder.length;
        for(int i = 0; i < n; i++){
            indexMap.put(inorder[i], i);
        }

        return myBuild(preorder, inorder, 0, n - 1, 0, n - 1);
    }

    //递归构建子树
    public TreeNode myBuild(int[] preorder, int[] inorder, int preLeft, int preRight, int inleft, int inRight){
        //递归终止条件  
        //当节点的子树为空,此时有 preLeft > preRight 或 inLeft > inRight
        //如节点9的左子树为空,preLeft=2,preRight=1, 回归到上一层,构建右子树,右子树为空,preLeft=2,preRight=1
        if(preLeft > preRight){
            return null;
        }
		// if(inLeft > inRight) return null;

        //子树根节点的值
        int rootVal = preorder[preLeft];
        //中序遍历中定位根节点
        int rootInorder = indexMap.get(rootVal);
        //左子树长度
        int lenLeftSubtree = rootInorder - inleft;
        //构建根节点
        TreeNode root = new TreeNode(rootVal);

        //递归构建左子树 传入左子树在前序遍历中的范围和中序遍历中的范围
        root.left = myBuild(preorder, inorder, preLeft + 1, preLeft + lenLeftSubtree, inleft, rootInorder - 1);
        //递归构建右子树 传入右子树在前序遍历中的范围和中序遍历中的范围
        root.right = myBuild(preorder, inorder, preLeft + lenLeftSubtree + 1, preRight, rootInorder + 1, inRight);

        return root;
    }
}

102. 二叉树的层序遍历

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)

题目链接

在这里插入图片描述

代码实现
class Solution {
    //利用队列先进先出的特性模拟实现树的层级遍历,时间复杂度和空间复杂度为O(n)
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();

        if(root == null){
            return result;
        }
        //首先压入根节点
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        //队列为空时说明已遍历完树中所有节点
        while(!queue.isEmpty()){
            //存储当前层级的元素
            List<Integer> level = new ArrayList<>();
            //获取当前层级的元素个数,通过len可以实现分层
            int len = queue.size();
            //层序遍历,并存下一层的节点入队列
            for(int i = 0; i < len; i++){
                TreeNode node = queue.poll();
                level.add(node.val);
                //如果当前节点子树存在则压入队列
                if(node.left != null){
                    queue.add(node.left);
                }
                if(node.right != null){
                    queue.add(node.right);
                }
            }
            //当前层遍历完毕
            result.add(level);
        }
        return result;
    }
}

236. 二叉树的最近公共祖先

题目链接

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
在这里插入图片描述

题目分析

递归实现

本题的关键是确定递归终止条件:如果当前根节点root为p或q或空,返回root;

不满足终止条件则向下递归查看左右子树;

而根据递归条件,每层递归的返回值只会是为p或q或空;

因此当左右子树返回值都不为空时,当前根节点就为最近的公共祖先。

代码实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    //递归实现 时间复杂度和空间复杂度为O(n)
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //把每一个节点想作根节点,输入函数后其返回值有四种情况:
        //当前根节点的子树中包含q和p,则直接返回他们最近的公共祖先
        //当前根节点的子树中只包含q,则返回节点q
        //当前根节点的子树中只包含p,则返回节点p
        //当前根节点的子树中即不包含p,也不包含q,则返回空
        
        //递归终止条件:如果当前根节点为p或为q或为空,返回root
        if(root == null || root == p || root ==q) return root;

        //若不满足递归终止条件,则向下传递查看其子树,而根据终止条件,返回值left和right只会是空或p或q
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //当左子树的返回值为空时,说明左边即不包含p,也不包含q;
        //那么此时对于其右子树有:可能也都不包含,则右边的返回值right=null;
        //可能只包含p或q,则右边返回值right=p或q;
        //可能同时包含q和p,则right为最近公共祖先,应返回right
        //因此不论何种情况,本层递归的返回值都应取右边的返回值,同理右子树的返回值为空时也成立。
        if(left == null) return right;
        if(right == null) return left;

        //如果左子树右子树返回值都不为空,说明p和q左右两边一边一个,
        //则当前节点就是最近公共祖先,直接返回,程序结束
        return root;
    }
}

543 二叉树的直径

题目链接

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
在这里插入图片描述

题目分析

所谓二叉树的直径就是以根节点为起点的最大路径,也就是所经过的最大节点数-1(如果右子树为空,就可能不穿过根节点),因此我们要求的实际上以根节点为起点的路径所经过的最大节点数。

由此想到每个节点都可以想作根节点,最大节点数就是左子树高度+右子树高度+1(根节点),树的高度就是左右子树的高度最大值+1,例如叶子节点左右子树为空(高度看作0),因此叶子节点的高度为1。

从而可以通过递归求得各节点的高度,进而求得最大节点数,得到二叉树直径。

代码实现
class Solution {
    //递归实现 时间复杂度为O(N) 空间复杂度为O(Height) Height为二叉树的高度
    //通过递归不断求当前节点的左右子树的深度(也可以说是高度,例如4的高度为1,2的高度为2)
    //知道了子树的深度就可以得到以当前节点为起点的路径的最大节点数(leftDepth + rightDepth + 1)
    //直到求出以根节点为起点的路径的最大节点数,减1就是该二叉树直径

    //全局变量记录以节点node为起点的路径经过节点数的最大值
    int ans = 1;
    public int diameterOfBinaryTree(TreeNode root) {
        depth(root);

        //二叉树直径就是经过的最多节点数-1
        return ans - 1;
    }

    //递归求出子树深度
    public int depth(TreeNode root){
        //递归终止条件
        if(root == null) return 0;

        //求得 当前节点的左右子树 的深度
        int leftDepth = depth(root.left);
        int rightDepth = depth(root.right);
        //计算以当前节点为起点的路径所经过的最大节点数并与ans比较取较大值赋给ans
        ans = Math.max(ans, leftDepth + rightDepth + 1);
        //返回 以当前节点为根的子树 的深度
        return Math.max(leftDepth, rightDepth) + 1;
    }
}

124. 二叉树中的最大路径和

题目链接

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。
在这里插入图片描述

题目分析

本题和543题相似,只不过是543题中每个节点的权重值为1,而本题中的权重值为节点的值。

关键在于:

  • 递归函数返回的是从当前节点向下左右两边较大的路径和,因为同一个节点在一条路径序列中至多出现一次;
  • 每个节点可以想作根节点,其路径和=节点值+左子树最大路径和+右子树最大路径和;
  • 递归完成后ans中存储的值就是最大路径和;
代码实现
class Solution {
    //递归实现 时间复杂度为O(N) 空间复杂度为O(N)
    //设置全局变量ans记录最大路径和
    //将每个节点想作根节点,递归求出每个节点的路径和,比ans大则存入ans
    //递归完毕时求得根节点的路径和,此时ans值就是最大路径和
    //需要注意的是最大路径和所在路径至少包含一个节点,且不一定经过根节点(根节点为负)。

    //定义全局变量存储最大路径和
    int ans = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        dfs(root);
        return ans;
    }

    //返回从当前节点向下走,左右两边较大的路径和
    public int dfs(TreeNode root){
        //递归终止条件
        if(root == null) return 0;

        //递归求得当前节点的左右子树的最大路径和
        int left = dfs(root.left);
        int right = dfs(root.right);

        //更新最大路径和  当前节点想作根极点,路径和=root.val+左子树最大路径和+右子树最大路径和
        ans = Math.max(ans, left + root.val + right);

        //因为同一个节点在一条路径序列中至多出现一次,
        //因此若当前节点是其他节点的子树,则应当选择值较大的路径 
        //返回当前节点的最大路径和(当前节点的值+当前节点子树中的最大值),如果小于0,则直接返回0
        return Math.max(0, root.val + Math.max(left, right));
    }
}

173. 二叉搜索树迭代器

题目链接

实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:
BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。
boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false 。
int next()将指针向右移动,然后返回指针处的数字。
注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。

你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。

在这里插入图片描述
在这里插入图片描述

题目分析

根据题目,使用 O(h) 内存。其中 h 是树的高度。可以联想到 94 二叉树中的中序遍历,其有递归法和迭代法。

本题实质也是实现中序遍历,因此也有递归法和迭代法。

递归法:
直接通过递归法对二叉搜索树做一次完全的中序遍历,将中序遍历的结果依次存入数组中,然后利用得到的数组来实现迭代器。如下:

public BSTIterator(TreeNode root) {
    idx = 0;
    arr = new ArrayList<Integer>();
    inorderTraversal(root, arr);
}
public int next() {
    return arr.get(idx++);
}
public boolean hasNext() {
    return idx < arr.size();
}
  • 时间复杂度:初始化需要O(n) 的时间,其中 n 为树中节点的数量,随后每次调用只需要 O(1) 的时间。
  • 空间复杂度:O(n),因为需要保存中序遍历的全部结果

迭代法:
94题中的迭代法用栈来实现二叉树的中序遍历,空间复杂度为O(h),因此本题也可以采用迭代法通过维护栈来实现中序遍历,只是将代码拆分到不同的函数中。相比于递归法不用预先计算出中序遍历的全部结果,只需要实时维护当前栈的情况即可。

  • 时间复杂度:初始化和调用 hasNext() 都只需要 O(1) 的时间。每次调用next() 函数最坏情况下需要 O(n) 的时间;但考虑到 n 次调用next() 函数总共会遍历全部的 n 个节点,总的时间复杂度为O(n),因此单次调用平均下来的均摊复杂度为O(1)。
  • 空间复杂度:O(n),其中 n 是二叉树的节点数量。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
代码实现
class BSTIterator {
    //利用栈,通过迭代的方式对二叉树做中序遍历  
    //均摊时间复杂度为 O(1) 空间复杂度:O(n)  证明见官方题解

    private TreeNode cur;   //访问每个节点
    private Deque<TreeNode> stack;

    //初始化
    public BSTIterator(TreeNode root) {
        cur = root;
        stack = new LinkedList<TreeNode>();
    }
    
    //进行中序遍历 
    public int next() {
        //  先压左子树入栈
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        //首次调用时弹出最左的叶子节点,也就是BST中的最小元素
        cur = stack.pop();
        int result = cur.val;
        cur = cur.right;

        return result;
    }
    
    //当cur指针所访问的节点不为空或栈中元素不为空时,就还存在节点没有遍历
    public boolean hasNext() {
        return cur != null || !stack.isEmpty();
    }
}

297. 二叉树的序列化与反序列化 *****

题目链接

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
在这里插入图片描述
在这里插入图片描述

题目分析

二叉树的序列化本质上是对其值进行编码,更重要的是对其结构进行编码。可以遍历树来完成上述任务。一般有两个策略:广度优先搜索和深度优先搜索。广度优先搜索可以按照层次的顺序从上到下遍历所有的节点;深度优先搜索可以从一个根开始,一直延伸到某个叶,然后回到根,到达另一个分支。根据根节点、左节点和右节点之间的相对顺序,可以进一步将深度优先搜索策略区分为:

  • 先(前)序遍历
  • 中序遍历
  • 后序遍历

这里,我们选择先序遍历的编码方式。

注意:一般情况下,只使用前序遍历得到的结果并不能反向唯一确定一棵二叉树,需要前序遍历+中序遍历才可以唯一确定二叉树。

这里的处理方式是把空节点也加入遍历结果中,如示例的遍历结果为:(1,2,null,null,3,4,null,null,5,null,null)
这样是可以唯一确定一棵二叉树的。

反序列化:需要根据 , 把序列分割开来得到先序遍历的元素列表,然后从左向右遍历这个序列:

  • 如果当前的元素为 None,则当前为空树;
  • 否则先解析这棵树的左子树,再解析它的右子树

时间复杂度:在序列化和反序列化函数中,只访问每个节点一次,因此时间复杂度为O(n),n是树的大小。
空间复杂度:在序列化和反序列化函数中,递归使用栈空间,空间复杂度为 O(n)。

代码实现
public class Codec {
    //采用dfs方法实现,也可以采用bfs(leetcode官方使用层序遍历)
    //时间复杂度和空间复杂度为 O(n)

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        return dfs1(root, "");
    }
    //递归实现序列化  前序遍历:中左右
    public String dfs1(TreeNode root, String str) {
        //如果当前节点为空
        if (root == null) {
            str += "null,";
        } else {  //不为空 取值追加到字符串,逗号隔开
            str += str.valueOf(root.val) + ",";
            //递归遍历左子树右子树
            str = dfs1(root.left, str);
            str = dfs1(root.right, str);
        }

        return str;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        //按,将字符串分割为字符串并存入字符串数组中
        String[] dataArray = data.split(",");
        //将字符串数组转为列表,便于删除已经访问过的元素
        //涉及到删除,用LinkedList效率较高
        List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
        
        return dfs2(dataList);
    }
    //递归实现反序列化
    public TreeNode dfs2(List<String> dataList) {
        //如果节点为空
        if (dataList.get(0).equals("null")) {
            dataList.remove(0);
            return null;
        }
        //如果节点不为空,创建根节点
        TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
        //list移除0位的元素后,后续元素前移
        dataList.remove(0);
        //递归创建子树
        root.left = dfs2(dataList);
        root.right = dfs2(dataList);
    
        return root;
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值