【Data structure & Algorithm】做题笔记-二叉树

本文详细探讨了二叉树的各种遍历方法,包括前序、中序、后序以及层级遍历,并提供了相关算法题目的解题思路。通过递归思想解析了如何构造、翻转和连接二叉树节点,以及如何根据遍历序列还原二叉树。此外,还介绍了完全二叉树的节点计算优化方法,总结了二叉树遍历的代码框架和特点。
摘要由CSDN通过智能技术生成

方法论

二叉树的遍历方式是递归,写递归函数的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要跳入递归的细节。换句话说,先搞清楚当前 root 节点该做什么,然后根据函数定义递归调用子节点,递归调用会让孩子节点做相同的事情。把题目的要求细化成每个节点(或每几个节点)需要做的事情。

如,计算一棵二叉树共有几个节点:

// 定义:count(root) 返回以 root 为根的树有多少节点
int count(TreeNode root) {
    // base case
    if (root == null) return 0;
    // 自己加上子树的节点数就是整棵树的节点数
    return 1 + count(root.left) + count(root.right);
}

root 本身就是一个节点,加上左右子树的节点数就是以 root 为根的树的节点总数。左右子树的节点数怎么算?其实就是计算根为 root.left 和 root.right 两棵树的节点数,按照定义,递归调用 count 函数即可算出来。

框架:

/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    // 前序遍历位置,在此处访问root.val
    traverse(root.left)
    // 中序遍历位置,在此处访问root.val
    traverse(root.right)
    // 后序遍历位置,在此处访问root.val
}

226-翻转二叉树

题目:输入一个二叉树根节点 root,把整棵树镜像翻转。
Invert a binary tree

思路:只要把二叉树上的每一个节点的左右子节点进行交换,最后的结果就是完全翻转之后的二叉树,如图所示。
在这里插入图片描述

// 将整棵树的节点翻转
TreeNode invertTree(TreeNode root) {
    // 结束递归的条件
    if (root == null) {
        return null;
    }

    /**** 前序遍历位置 ****/
    // root 节点需要交换它的左右子节点
    TreeNode tmp = root.left;
    root.left = root.right;
    root.right = tmp;

    // 让左右子节点继续翻转它们的子节点
    invertTree(root.left);
    invertTree(root.right);

    return root;//注意return的是根节点
}

116-填充每个节点的下一个右侧节点指针

题目
在这里插入图片描述
难点在于如何连接不同子树上的节点,如节点5、6。如果递归函数的参数只有1个(传入1个节点),节点5、6是无法被连接的,因为每次递归,递归函数的作用域(所能访问到的节点)为当前节点、当前节点的左节点、当前节点的右节点,形成一个三角形作用域,节点5、6处于不同作用域中(即处于不同的三角形中),无法同时被访问到,故无法进行连接。

那我们就把递归函数的参数设置为2个(传入2个节点),每次递归,递归函数的作用域(所能访问到的节点)形成了一个梯形,包含6个节点,即参数节点1、参数节点1的左节点、参数节点1的右节点、参数节点2、参数节点2的左节点、参数节点2的右节点。这样节点5、6就可以被同时访问到了,可以进行连接了。

代码如下,示意图如下。

//错误代码,递归函数的参数只有1个
Node connect(Node root) {
    if (root == null || root.left == null) {
        return root;
    }

    root.left.next = root.right;

    connect(root.left);
    connect(root.right);

    return root;
}
//正确代码,递归函数的参数有2个
//主函数
    public Node connect(Node root) {
        if(root == null){
            return null;
        }
        connect2Nodes(root.left,root.right);
        return root;
    }
//辅助函数
    public void connect2Nodes(Node n1,Node n2){
        //n1在左,n2在右(不要反了)
        //结束递归的条件,注意:这是一棵完美二叉树
        if(n1 == null || n2 == null){//由于是完美二叉树,n1、n2实际上会同时为null,或同时不为null
            return;
        }
        /**** 前序遍历位置 ****/
        // 将传入的两个节点连接
        n1.next = n2;
        // 连接相同父节点的两个子节点
        connect2Nodes(n1.left,n1.right);
        connect2Nodes(n2.left,n2.right);
        // 连接跨越父节点的两个子节点
        connect2Nodes(n1.right,n2.left);
    }

在这里插入图片描述

114-将二叉树展开为链表

题目
关键点是要“脑补”出二叉树被拉平(flatten)的过程,过程如下图所示,蓝色框表示当前步骤所关注的二叉树部分,先关注root的左子树,将其拉平,然后关注root的右子树,将其拉平(本来就是平的),然后关注整体,将整体拉平。“拉平”具体又是怎么实现呢?以root的左子树为例,将根节点的左节点插入到根节点与右节点之间;以整体为例,将根节点(flatten后)的左子树,插入到根节点与(flatten后)的右子树之间。所谓的“拉平”就是我们要写的递归方法。
在这里插入图片描述

    public void flatten(TreeNode root) {
        if(root == null){
            return;
        }

        //先拉平左子树
        flatten(root.left);
        //再拉平右子树
        flatten(root.right);

        /**** 后序遍历位置 ****/
        //将拉平后的左子树插入到根节点与右子树之间
        TreeNode rootLeft = root.left;
        TreeNode rootRight = root.right;

        if(rootLeft != null){
            root.left = null;
            root.right = rootLeft;
            //将右子树拼接到原左子树的最右下末端(原左子树已经被flatten过了,一定是\形状)
            TreeNode connectNode = rootLeft;
            while(connectNode.right != null){
                connectNode = connectNode.right;
            }
            //connectNode为原左子树的最右下末端节点
            connectNode.right = rootRight;
        }
        
    }

654-构造最大二叉树

题目
思路:先要找到根节点root,即数组中的最大值,然后将数组中最大值左边的数构建成最大二叉树,作为根节点的左子树,然后将数组中最大值右边的数构建成最大二叉树,作为根节点的右子树。

    public TreeNode constructMaximumBinaryTree(int[] nums) {
        return build(nums,0,nums.length-1);
    }

    public TreeNode build(int[] nums,int start,int end){//start,end表明使用数组哪一步分
        if(end < start){
            return null;
        }

        //找数组中的最大值及其索引,作为根节点
        int max = nums[start];
        int index = start;
        for(int i=start+1;i<=end;i++){
            if(nums[i] > max){
                max = nums[i];
                index = i;
            }
        }
        TreeNode root = new TreeNode(max);

        //将最大值左边部分构建成最大二叉树,作为根节点的左子树
        root.left = build(nums,start,index-1);
        //将最大值右边部分构建成最大二叉树,作为根节点的右子树
        root.right = build(nums,index+1,end);

        return root;
    }

105-用前序/中序遍历结果还原二叉树

题目
首先看前序遍历和后序遍历的递归代码,代码的结构,决定了遍历结果(数组)的特点。

void traverse(TreeNode root) {
    // 前序遍历位置,在此处访问root.val
    traverse(root.left);
    traverse(root.right);
}

void traverse(TreeNode root) {
    traverse(root.left);
    // 中序遍历位置,在此处访问root.val
    traverse(root.right);
}

从代码结构就能看出:
前序遍历的结果(数组),根节点在数组的第一个位置,然后是左子树的节点们,然后是右子树的节点们;
中序遍历的结果(数组),先是左子树的节点们,然后是根节点,最后是右子树的节点们。
事实确实如此,如图所示:
在这里插入图片描述
知道了前序遍历的结果(数组)与中序遍历的结果(数组)各自的特点之后,我们如何构建出二叉树呢?
根据前序遍历数组、中序遍历数组,构建二叉树可以分为以下步骤:
1.先从数组中找出根节点的值,创建根节点
2.获得左子树的中序遍历数组、前序遍历数组,根据这两个数组构建二叉树作为根节点的左子树
3.获得右子树的中序遍历数组、前序遍历数组,根据这两个数组构建二叉树作为根节点的右子树
“根据前序遍历数组、中序遍历数组,构建二叉树”就是我们的递归函数的任务。

注意:当递归函数的参数为数组时,同时使用两个int参数指明当前数组的start index和end index,这样可以避免创建新的数组,减少开销。

第2步和第3步中,获取子树的前序遍历数组和中序遍历数组,最好画个图,方便理解:
我们要知道左子树在数组中的长度(左子树节点个数),才好推算出前序遍历中左子树在数组中的终止位置。
在这里插入图片描述

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return build(preorder,0,preorder.length-1,inorder,0,inorder.length-1);
    }

    public TreeNode build(int[] preorder,int pStart,int pEnd, int[] inorder,int iStart,int iEnd){
        if(pStart > pEnd || iStart > iEnd){
            return null;
        }

        //1.先从数组中找出根节点的值,创建根节点
        //前序遍历数组的第一个值就是根节点的值
        int rootVal = preorder[pStart];
        TreeNode root = new TreeNode(rootVal);

        //获得根节点在中序遍历数组中的位置
        int rootIndex4Inorder = 0;
        for(int i=iStart;i<=iEnd;i++){
            if(inorder[i] == rootVal){
                rootIndex4Inorder = i;
                break;
            }
        }
        //得到左子树的长度
        int leftTreeSize = rootIndex4Inorder - iStart;

        //2.获得左子树的中序遍历数组、前序遍历数组,根据这两个数组构建二叉树作为根节点的左子树
        root.left = build(preorder,pStart+1,pStart+leftTreeSize,inorder,iStart,rootIndex4Inorder-1);

        //3.获得右子树的中序遍历数组、前序遍历数组,根据这两个数组构建二叉树作为根节点的右子树
        root.right = build(preorder,pStart+leftTreeSize+1,pEnd,inorder,rootIndex4Inorder+1,iEnd);

        return root;
    
    }

106-通过后序和中序遍历结果构造二叉树

题目
和上个题类似。先看中序遍历、后序遍历的代码结构:

void traverse(TreeNode root) {
    traverse(root.left);
    traverse(root.right);
    // 后序遍历位置,在此处访问root.val
}

void traverse(TreeNode root) {
    traverse(root.left);
    // 中序遍历位置,在此处访问root.val
    inorder.add(root.val);
    traverse(root.right);
}

由于这样的代码结构,导致了遍历结果的特点:

  • 中序遍历数组:左子树-根节点-右子树
  • 后序遍历数组:左子树-右子树-根节点

明确了两种遍历方式的数组特点之后,考虑如何根据前序遍历数组、中序遍历数组构建二叉树,可以分为以下步骤:
1.先从数组中找出根节点的值,创建根节点
2.获得左子树的中序遍历数组、后序遍历数组,根据这两个数组构建二叉树作为根节点的左子树
3.获得右子树的中序遍历数组、后序遍历数组,根据这两个数组构建二叉树作为根节点的右子树
“根据前序遍历数组、后序遍历数组,构建二叉树”就是我们的递归函数的任务。

第2步和第3步中,获取子树的后序遍历数组和中序遍历数组,最好画个图,方便理解:
我们要知道左子树在数组中的长度(左子树节点个数),才好推算出后序遍历中左子树在数组中的终止位置。
在这里插入图片描述

    public TreeNode buildTree(int[] inorder, int[] postorder) {
        return build(inorder,0,inorder.length-1,postorder,0,postorder.length-1);
    }

    public TreeNode build(int[] inorder,int inStart,int inEnd,int[] postorder,int poStart,int poEnd){
        if(inStart > inEnd || poStart > poEnd){
            return null;
        }
        //1.先从数组中找出根节点的值,创建根节点
        //后序遍历数组的最后一个值是根节点值
        int rootVal = postorder[poEnd];
        TreeNode root = new TreeNode(rootVal);

        //获得根节点在中序遍历数组中的位置
        int rootIndex4Inorder = 0;
        for(int i=inStart;i<=inEnd;i++){
            if(inorder[i] == rootVal){
                rootIndex4Inorder = i;
                break;
            }
        }
        //得到左子树的长度
        int leftTreeSize = rootIndex4Inorder - inStart;

        //2.获得左子树的中序遍历数组、后序遍历数组,根据这两个数组构建二叉树作为根节点的左子树
        root.left = build(inorder,inStart,rootIndex4Inorder-1,postorder,poStart,poStart+leftTreeSize-1);
        
        //3.获得右子树的中序遍历数组、后序遍历数组,根据这两个数组构建二叉树作为根节点的右子树
        root.right = build(inorder,rootIndex4Inorder+1,inEnd,postorder,poStart+leftTreeSize,poEnd-1);

        return root;
    }

652-寻找重复的子树

题目
思考以下问题:
1.怎么描述一棵二叉树的模样?
如果两棵二叉树的前序or中序or后序遍历结果相同,说明这两个二叉树一模一样。我们可以通过拼接字符串的方式把二叉树序列化,我们用非数字的特殊符 # 表示空指针,并且用字符 , 分隔每个二叉树节点值,这属于序列化二叉树的套路了。
2.怎么知道一个二叉树的模样?
知道它的左子树的模样、右子树的模样,再加上根节点,就知道该二叉树的模样了。换言之,知道它的左子树的遍历序列、右子树的遍历序列,再加上该根节点本身,拼接得到的序列就是该二叉树的模样。(对应后序遍历框架)

void traverse(TreeNode root) {
    traverse(root.left);
    traverse(root.right);
    /* 判断以root为根节点的二叉树,是否和其他二叉树一样 */
}

3.怎么知道其他二叉树的模样?
想知道我本人跟其他人一样不一样,总得知道其他人的样子吧。所以要有一个数据结构来记录各二叉树的遍历序列,我们选择使用Map集合,序列作为键,出现次数作为值。给出的方法的返回类型为List< TreeNode >,结合题目要求,是用List集合存放发生重复的二叉树的根节点。

    Map trees = new HashMap<String,Integer>();
    List roots = new LinkedList<TreeNode>();

    public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        describe(root);
        return roots;
    }

    
    public String describe(TreeNode root){
        if(root == null){
            return "#";
        }

        //得到root的左子树的遍历序列
        String leftTree = describe(root.left);
        //得到root的右子树的遍历序列
        String rightTree = describe(root.right);
        /**后序遍历位置:得到以root为根节点的二叉树的后序遍历序列(描述该二叉树的模样)**/
        String description = leftTree + "," + rightTree + "," + root.val;

        /**后序遍历位置:判断以root为根节点的二叉树,是否和其他二叉树一样**/

        //将该序列存储到Map中
        Object freq = trees.get(description);
        if(freq == null){//Map中还没有该序列
            trees.put(description,1);
        }else{//Map中已经存在该序列了
            Integer freq_ = (Integer)freq;
            trees.put(description,freq_+1);
            if(freq_ == 1){//第一次出现重复的时候,将根节点放入List
                roots.add(root);
            }
        }

        return description;
    }

297-二叉树的序列化与反序列化

题目
规定的代码框架:

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        
    }
}

题目说序列化流程我们自己确定,所谓序列化,就是二叉树遍历,有4种方式:前序、中序、后序、层序。
字符串拼接是很浪费空间的,所以使用StringBuilder。

方法1:前序遍历

反序列化的时候会利用前序遍历序列的特点:根-左子树-右子树
在这里插入图片描述

public class Codec {
    //分隔符
    String SEP = ",";
    //空节点
    String NULL = "#";

    /* 主函数,将二叉树序列化为字符串 */
    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        serialize(root,sb);
        return sb.toString();
    }

    /* 辅助函数,将二叉树存入 StringBuilder */
    public void serialize(TreeNode root,StringBuilder sb){
        if(root == null){
            sb.append(NULL).append(SEP);//在序列中加入#以表示为空节点
            return;
        }
        /**前序遍历位置**/
        sb.append(root.val).append(SEP);//在序列中加入本节点的值
        serialize(root.left,sb);
        serialize(root.right,sb);
    }

    /* 主函数,将字符串反序列化为二叉树结构 */
    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        //将String转化为List,方便遍历各个值
        LinkedList<String> vals = new LinkedList<>();
        String[] strArray = data.split(SEP);//以SEP分隔符,获得String数组
        for(String val:strArray){
            vals.add(val);
        }
        return deserialize(vals);
    }

    /* 辅助函数,通过序列构造二叉树 */
    public TreeNode deserialize(LinkedList<String> vals){//从前往后在list中取元素
        if(vals.isEmpty()){
            return null;
        }
		//任务:构造根节点,构造其左右子树并与根节点相连接
        //前序遍历序列的第一个值是根节点
        String first = vals.removeFirst();//removeFirst()是LinkedList的方法,移除并返回此列表的第一个元素
        if(first.equals(NULL)){//如果是#
            return null;
        }
        //生成根节点
        TreeNode root = new TreeNode(Integer.parseInt(first));//String转Integer

        //构建左子树二叉树,连接到root
        root.left = deserialize(vals);
        //构建右子树二叉树,连接到root
        root.right = deserialize(vals);

        return root;

    }
}

方法2:后序遍历

反序列化的时候会利用后序遍历序列的特点:左子树-右子树-根
在这里插入图片描述

public class Codec {
    //分隔符
    String SEP = ",";
    //空节点
    String NULL = "#";

    /* 主函数,将二叉树序列化为字符串 */
    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        serialize(root,sb);
        return sb.toString();
    }

    /* 辅助函数,将二叉树存入 StringBuilder */
    public void serialize(TreeNode root,StringBuilder sb){
        if(root == null){
            sb.append(NULL).append(SEP);//在序列中加入#以表示为空节点
            return;
        }
        serialize(root.left,sb);
        serialize(root.right,sb);
        /**后序遍历位置**/
        sb.append(root.val).append(SEP);//在序列中加入本节点的值
    }

    /* 主函数,将字符串反序列化为二叉树结构 */
    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        //将String转化为List,方便遍历各个值
        LinkedList<String> vals = new LinkedList<>();
        String[] strArray = data.split(SEP);//以SEP分隔符,获得String数组
        for(String val:strArray){
            vals.add(val);
        }
        return deserialize(vals);
    }

    /* 辅助函数,通过序列构造二叉树 */
    public TreeNode deserialize(LinkedList<String> vals){//从后往前在list中取元素
        if(vals.isEmpty()){
            return null;
        }
        //任务:构造根节点,构造其左右子树并与根节点相连接
        //后序遍历序列的最后一个值是根节点
        String first = vals.removeLast();//removeLast()是LinkedList的方法,移除并返回此列表的最后一个元素
        if(first.equals(NULL)){//如果是#
            return null;
        }
        //生成根节点
        TreeNode root = new TreeNode(Integer.parseInt(first));//String转Integer

        //注意:后序遍历序列特点为左子树-右子树-根,我们从后往前取元素,所以要先构建右子树
        //先构建右子树二叉树,连接到root
        root.right = deserialize(vals);
        //再构建左子树二叉树,连接到root
        root.left = deserialize(vals);

        return root;

    }
}

方法3:中序遍历(行不通)

可以用中序遍历进行序列化,但无法进行反序列化,因为找不到根节点。反序列化的时候,我们的任务是:先构造根节点,然后构造左右子树,第一步就是找根节点的值。对于前序遍历序列而言,根节点值就是左边第一个值;对于后序遍历序列而言,根节点值就是右边最后一个值;对于中序遍历序列而言,根节点值在中间,无法确定是哪个值。

方法4:层级遍历

队列Queue是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。
offer:从队列后端插入
poll:从队列前端取出

层级遍历二叉树的代码框架:
标准的二叉树层级遍历框架,从上到下,从左到右打印每一层二叉树节点的值,可以看到,队列 q 中不会存在 null 指针。

void traverse(TreeNode root) {
    if (root == null) return;
    // 初始化队列,将 root 加入队列
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);

    while (!q.isEmpty()) {
        TreeNode cur = q.poll();

        /* 层级遍历代码位置 */
        System.out.println(root.val);
        /*****************/

        if (cur.left != null) {
            q.offer(cur.left);
        }

        if (cur.right != null) {
            q.offer(cur.right);
        }
    }
}

结合自己的需求,把上边框架改一下,就能实现序列化。

反序列化,要看遍历序列的特点,如图所示,根据序列特点来写构建二叉树的流程。所谓“构建”就是“找到根节点,然后找到其左节点,与根节点连接,然后找到其右节点,与根节点连接”,不断重复这一步骤,就能从上到下、从左至右地还原二叉树。

设置一个指针来从左到右遍历数组,还需要一个队列来存放尚未有子节点的根节点。为何需要该队列?为了获得根节点,比如当指针走到下图中第一个#时,它并不知道自己的根节点是谁,需要有额外的数据结构来记录。节点2作为子节点时,节点2比节点3先被连接到节点1(因为指针从左往右遍历数组),节点2就会比节点3先进队列;节点2作为根节点时,节点2比节点3先出队列(因为队列是先进先出的),节点2比节点3先连接自己的子节点(因为指针从左往右遍历数组)。
在这里插入图片描述

public class Codec {
    //分隔符
    String SEP = ",";
    //空节点
    String NULL = "#";

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root == null){
            return "";
        }

        //创建队列,存放层级遍历顺序的节点
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        StringBuilder sb = new StringBuilder();

        while(!queue.isEmpty()){//当队列中存在节点时
            //从队列中取节点
            TreeNode cur = queue.poll();

            /* 层级遍历代码位置 */
            if(cur == null){
                sb.append(NULL).append(SEP);
                continue;//跳到下一次循环
            }
            sb.append(cur.val).append(SEP);

            //往队列里存节点
            queue.offer(cur.left);
            queue.offer(cur.right);
        }
        return sb.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if(data.isEmpty()){
            return null;
        }
        String[] array = data.split(SEP);
        //创建根节点(层序遍历序列的第一个值是根节点值)
        TreeNode root = new TreeNode(Integer.parseInt(array[0]));
        //该队列用来存放尚未连接子节点的根节点
        Queue<TreeNode> rootQ = new LinkedList<>();
        rootQ.offer(root);

        int index = 0;//指示进行到数组中哪个位置
        while(index < array.length-1){//每次循环处理一对左右节点(2个节点),所以要-1
            //队列中取出根节点
            TreeNode parent = rootQ.poll();
            //获取左节点的值
            String leftNode = array[++index];
            if(!leftNode.equals(NULL)){
                //创建左节点,并连接到根节点
                parent.left = new TreeNode(Integer.parseInt(leftNode));
                //将该节点放进队列
                rootQ.offer(parent.left);
            }else{
                parent.left = null;
            }

            //获取右节点的值
            String rightNode = array[++index];
            if(!rightNode.equals(NULL)){
                //创建右节点,并连接到根节点
                parent.right = new TreeNode(Integer.parseInt(rightNode));
                //将该节点放进队列
                rootQ.offer(parent.right);
            }else{
                parent.right = null;
            }

        }
        return root;
    }
}

222-完全二叉树(complete binary tree)的节点个数

常规解法

计算一棵二叉树的节点,这很简单,递归就行:

// 定义:count(root) 返回以 root 为根的树有多少节点
int count(TreeNode root) {
    // base case
    if (root == null) return 0;
    // 自己加上子树的节点数就是整棵树的节点数
    return 1 + count(root.left) + count(root.right);
}

N为节点总数,时间复杂度O(N)。怎么算的?
递归深度(即次数) * 每轮递归的时间复杂度=N * 1=N

最优解

但是题目说的是完全二叉树,如果利用完全二叉树的特性的话,可能会有时间复杂度更低的算法。

这个题目有如下关键点:

  • 1.理解完全二叉树的定义,满二叉树的定义
  • 2.知道满二叉树的节点计算,有现成的公式

英文:
完全二叉树-complete binary tree
满二叉树-perfect binary tree

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

在这里插入图片描述
满二叉树的定义
每层的节点都是满的,很好理解。
一个高度为h的满二叉树,节点数为:
1+2+2^2 +……+ 2^h
这不是个等比数列嘛,用等比数列的求和公式:
在这里插入图片描述
可得节点数N=2^h-1

注意:高度指的是边的个数,而不是节点个数,上图中满二叉树的高度为2而不是3。

所以我们现在知道了普通二叉树节点个数的计算方法为递归,时间复杂度O(N);满二叉树的节点个数的计算方法为直接套公式。显然满二叉树的计算过程简单太多了。我们就把两者结合一下,在递归过程中,如果该轮递归对应的二叉树是满二叉树,就直接套公式了。那么怎么判断一棵二叉树是否是满二叉树呢?如下图所示,左边线和右边线长度相同的话,就是满二叉树。
在这里插入图片描述

    public int countNodes(TreeNode root) {
        TreeNode leftTreeRoot = root;
        TreeNode rightTreeRoot = root;
        //得到左边线的长度(注意:高度是边的个数,不是节点个数)
        int leftTreeHeight = 0;
        while(leftTreeRoot != null){
            leftTreeHeight++;
            leftTreeRoot = leftTreeRoot.left;
        }
        
        //得到右边线的长度
        int rightTreeHeight = 0;
        while(rightTreeRoot != null){
            rightTreeHeight++;
            rightTreeRoot = rightTreeRoot.right;
        }

        if(leftTreeHeight == rightTreeHeight){//如果两边线的长度相同,说明是一棵满二叉树,可以直接使用公式
            return (int)Math.pow(2,leftTreeHeight)-1;
        }

        //以当前root为根节点的树不是满二叉树时,继续递归
        return 1+countNodes(root.left)+countNodes(root.right);
    }

时间复杂度分析:
在这里插入图片描述
一棵完全二叉树的左右子树中,必定有一个满二叉树,满二叉树直接套公式返回值,另外一个子树使用递归。
每轮递归,while循环的次数就是这棵树的高度,计算如下
N=2^h-1
h=log(N+1)
则每轮递归的时间复杂度是O(logN)

算法的递归深度就是树的高度 O(logN),每次递归所花费的时间就是 while 循环,需要 O(logN),所以总体的时间复杂度是 O(logN*logN)

这里理解的不是很透彻…

总结

想好这个递归函数的任务,及其参数,这个递归函数的作用就是:根据XXX参数完成XXX任务。不要跳入细节。

根据遍历序列还原二叉树的条件

如果序列中没有空节点的信息(如:1,2,4,3,),必须有前、中、后序遍历序列中的两种才能还原二叉树;
如果序列中有空节点的信息(如:1,2,#,4,#,#,3,#,#,),使用一种遍历顺序就能还原二叉树。

一些常用方法

Queue队列:
offer-从队列后端插入
poll-从队列前端取出
创建:Queue<> q = new LinkedList<>();

StringBuilder:
append-追加

LinkedList:
removeFirst-移走第一个值并返回其值
removeLast-移走最后一个值并返回其值

Math.pow(底数,幂) :求几次方

二叉树遍历的代码框架、序列特点

前序、中序、后序:

/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    // 前序遍历位置,在此处访问root.val
    traverse(root.left)
    // 中序遍历位置,在此处访问root.val
    traverse(root.right)
    // 后序遍历位置,在此处访问root.val
}

层级:

void traverse(TreeNode root) {
    if (root == null) return;
    // 初始化队列,将 root 加入队列
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);

    while (!q.isEmpty()) {
        TreeNode cur = q.poll();

        /* 层级遍历代码位置 */
        System.out.println(root.val);
        /*****************/

        if (cur.left != null) {
            q.offer(cur.left);
        }

        if (cur.right != null) {
            q.offer(cur.right);
        }
    }
}

前序:根-左子树-右子树
中序:左子树-根-右子树
后序:左子树-右子树-根
层级:一层一层的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值