二叉树 (基本概念和应用场景)+ 常见算法题

二叉树的概念与常见知识

定义:

  •  二叉树(Binary Tree)是计算机科学中的一种重要数据结构。
  • 二叉树是一个有限节点的集合,这个集合或者为空(称为空二叉树),或者由一个根节点以及两棵互不相交的、分别称作左子树和右子树的二叉树组成。

特性:

  1. 节点数与边数:在任何非空的二叉树中,节点的数量总比边的数量多1。
  2. :一个节点的度是指其子节点的数量。在二叉树中,节点的度不会超过2。
  3. 深度:节点的深度是指从根节点到该节点的唯一路径上的边的数量
  4. 高度:节点的高度是指从该节点到最远叶子节点的最长路径上的边的数量
  5. 节点:二叉树由节点组成,每个节点包含三个部分:数据域、左子指针和右子指针
    1. 根节点:位于二叉树顶部的节点,没有父节点。
    2. 子节点:一个节点的左子指针和右子指针所指向的节点。
    3. 父节点:如果一个节点含有子节点,那么该节点就是其子节点的父节点。
  1. 叶子节点:没有子节点的节点。
  2. :从根节点开始,根节点为第一层,它的子节点为第二层,以此类推。

类型:

  • 满二叉树:所有层的节点数都达到最大值的二叉树。除了最后一层外,每一层都是完全填充的,并且所有节点都在最左边。
  • 完全二叉树:所有层都完全填充,除了可能的最后一层外,且最后一层所有节点都向左靠拢。
  • 平衡二叉树:左右两个子树的高度差不超过1的二叉树,常见的有AVL树和红黑树。
  • 二叉查找树(Binary Search Tree, BST):对于任意节点,其左子树中的所有节点的值均小于它的值,其右子树中的所有节点的值均大于它的值。

存储:

  • 顺序存储:使用数组存储二叉树,适用于完全二叉树。
  • 链式存储:使用指针(引用)连接节点,适用于各种类型的二叉树。

遍历方法:

  • 前序遍历:访问根节点 -> 遍历左子树 -> 遍历右子树。
  • 中序遍历:遍历左子树 -> 访问根节点 -> 遍历右子树。
  • 后序遍历:遍历左子树 -> 遍历右子树 -> 访问根节点。
  • 层次遍历:按照从上到下、从左到右的顺序访问所有节点。

应用:

  • 在算法设计中用于排序和搜索操作。
  • 实现数据结构如堆、哈夫曼树等。
  • 数据库索引、文件系统和编译器符号表中使用。

二叉树因其简洁性和灵活性,在计算机科学中有着广泛的应用。理解和掌握二叉树的基本概念及其操作是编程和算法设计的基础之一。

二叉树的深度

描述

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度,根节点的深度视为 1 。

数据范围:节点的数量满足0≤n≤100 ,节点上的值满足0≤val≤100

进阶:空间复杂度 O(1) ,时间复杂度 O(n)

假如输入的用例为{1,2,3,4,5,#,6,#,#,7},那么如下图:

示例1

输入:{1,2,3,4,5,#,6,#,#,7}

返回值:4

示例2

输入:{}

返回值:0

解题思路

  1. 使用递归的方法去遍历左右子树的深度。
  2. 比较左右子树的深度,取较大的值,最终加上1(根节点)。

AC代码

public class Solution {
    public int TreeDepth(TreeNode root) {
        // 空节点就没深度
        if(root == null){
            return 0;
        }
        // 递归左右子树 最终比较左右子节点谁更深
        return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1;
    }
}

二叉树的镜像

描述

操作给定的二叉树,将其变换为源二叉树的镜像。

数据范围:二叉树的节点数 0≤n≤1000 , 二叉树每个节点的值 0≤val≤1000

要求: 空间复杂度 O(n) 。本题也有原地操作,即空间复杂度 O(1) 的解法,时间复杂度 O(n)

比如:

源二叉树

镜像二叉树

示例1

输入:{8,6,10,5,7,9,11}

返回值:{8,10,6,11,9,7,5}

说明:如题面所示

示例2

输入:{}

返回值:{}

解题思路

1.递归的思想,递归给定树的左右子树,交换每个子树的左右节点。

AC代码

递归

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pRoot TreeNode类 
     * @return TreeNode类
     */
    public TreeNode Mirror (TreeNode pRoot) {
        // write code here
        if(pRoot == null) return null;

        // 1、递归左右子树
        TreeNode left = Mirror(pRoot.left);
        TreeNode right = Mirror(pRoot.right);
        // 2、交换左右子树的值
        pRoot.right = left;
        pRoot.left = right;
        return pRoot;
    }
}

从上往下打印二叉树

描述

不分行从上往下打印出二叉树的每个节点,同层节点从左至右打印。例如输入{8,6,10,#,#,2,1},如以下图中的示例二叉树,则依次打印8,6,10,2,1(空节点不打印,跳过),请你将打印的结果存放到一个数组里面,返回。

数据范围:

0<=节点总数<=1000

-1000<=节点值<=1000

示例1

输入:{8,6,10,#,#,2,1}

返回值:[8,6,10,2,1]

示例2

输入:{5,4,#,3,#,2,#,1}

返回值:[5,4,3,2,1]

解题思路

  1. 读题之后,首先可以知道这个题就是二叉树的层序遍历,只是换了一种说法,所以获取题目所需结果,就需要正常从上到下从左到右去遍历每个节点。
  2. 这种可以理解为广度遍历,首先想到可以利用数据结构队列来实现,从根节点开始,一个子树一个子树入队,入队的时候判断是否有左右子树,有节点就添加到队列中。
  3. 按照上述步骤去遍历这个队列,不为空就一直遍历,然后保存下每个节点的值,最后成一个序列。

AC代码

队列

public static ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        // 创建一个结果数组
        ArrayList<Integer> res = new ArrayList<>();
        // 判断树是否为空
        if(root == null) return res;
        // 创建一个节点队列便于遍历这棵树
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        // 开始遍历这个树
        while(!q.isEmpty()){
            TreeNode cur = q.poll();
            // 将当前结点的值存入res中
            res.add(cur.val);
            // 判断是否存在左右子树 先判断左子树 保证从左到右的顺序
            if(cur.left != null){
                // 有节点存在就入队
                q.add(cur.left);
            }
            // 再判断右子树
            if(cur.right != null){
                // 有节点存在就入队
                q.add(cur.right);
            }
        }

        return res;
}

public static void main(String[] args) {
        // 模拟题目数据
        TreeNode root = new TreeNode(8);
        root.left = new TreeNode(6);
        root.right = new TreeNode(10);

        root.right.right = new TreeNode(1);
        root.right.left = new TreeNode(2);

        ArrayList<Integer> res = PrintFromTopToBottom(root);
        // 打印结果
        System.out.println(res);
  }

递归

  public static void traverse(TreeNode root,ArrayList<ArrayList<Integer>> res,int depth){
    if(root != null){
        // 当结果数据的size 小于当前传入的depth
        // 新建一个数组 当做新的一层
        if(res.size() < depth){
            ArrayList<Integer> row = new ArrayList<>();
            // 将当前层加入到res中
            res.add(row);
            // 将当前结点的值加入到当前层中
            row.add(root.val);
        }else{
            // 不是新的一层 只用当前值 放入当前层
            // 因为depth是从1开始 而数组的下标是从0开始 所以get(depth - 1)
            ArrayList<Integer> row = res.get(depth - 1);
            // 将当前结点的值加入到当前层中
            row.add(root.val);
        }
    }
    else return;
    // 从左往右开始遍历子节点保证从左到右打印值
    traverse(root.left, res, depth + 1);
    traverse(root.right, res, depth + 1);
  }

    public static ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        // 创建一个结果数组
        ArrayList<Integer> res = new ArrayList<Integer>();
        // 判断树是否为空
        if(root == null) return res;
        ArrayList<ArrayList<Integer>> tmp = new ArrayList<ArrayList<Integer>>();
        // 将给定的树当做第一层开始遍历
        traverse(root, tmp, 1);
        // 遍历 tmp 数组 取出数组中嵌套数组的值 放入res中
        // 外层是控制每一层
        for(int i = 0 ; i < tmp.size();i++){
            // 内层逐个遍历每一层的每一个值
            for(int j  = 0; j < tmp.get(i).size();j++){
                res.add(tmp.get(i).get(j));
            }
        }
        return res;
    }

判断是不是平衡二叉树

描述

输入一棵节点数为 n 二叉树,判断该二叉树是否是平衡二叉树。

在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树

平衡二叉树(Balanced Binary Tree),具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

样例解释:

样例二叉树如图,为一颗平衡二叉树

注:我们约定空树是平衡二叉树。

数据范围:0n≤100,树上节点的val值满足 0≤n≤1000

要求:空间复杂度O(1),时间复杂度O(n)

输入描述:

输入一棵二叉树的根节点

返回值描述:

输出一个布尔类型的值

示例1

输入:{1,2,3,4,5,6,7}

返回值:true

示例2

输入:{}

返回值:true

解题思路

  1. 首先根据题目要求,明确平衡二叉树的要求,空树和左右子树的高度差不超过 1 都是平衡二叉树,反之则不是平衡二叉树。
  2. 题目给定一棵树之后,我们判定他是否是平衡二叉树,就可以分解成一个子问题,他的左右子树是否是平衡二叉树,然后我们就可以想到用递归的思想去解决这个子问题。
  3. 使用递归的思想去解决子问题,计算出根节点的左右子树的最大高度,让他们作差,看结果绝对值是否大于1。
  4. 主要解决问题的方法就是判断一棵树是否是平衡二叉树,就去判断他的左右子树是否是平衡二叉树,把一个大问题分解成一个小问题,去解决小问题,从而解决大的问题。

AC代码

递归

/**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param pRoot TreeNode类
     * @return bool布尔型
     */
    public boolean IsBalanced_Solution (TreeNode pRoot) {
        // write code here
        if(pRoot == null) return true;
        // 不为空 计算左右子树的高度
        int left = deep(pRoot.left);
        int right = deep(pRoot.right);
        // 先做个判断左右子树的高度差是否大于1
        if(left - right > 1 || left - right < -1)
            return false;
        return IsBalanced_Solution(pRoot.left) && IsBalanced_Solution(pRoot.right);
    }

    public int deep(TreeNode pRoot){
        if(pRoot == null) return 0;
        // 计算左右子树的深度
        int left = deep(pRoot.left);
        int right = deep(pRoot.right);
        // 求出当前结点最大深度
        return (left > right) ? left + 1 : right + 1;
    }

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

描述

输入一棵节点数为 n 二叉树,判断该二叉树是否是平衡二叉树。

在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树

平衡二叉树(Balanced Binary Tree),具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

样例解释:

样例二叉树如图,为一颗平衡二叉树

注:我们约定空树是平衡二叉树。

数据范围0n≤100,树上节点的val值满足 0≤n≤1000

要求:空间复杂度O(1),时间复杂度O(n)

输入描述:

输入一棵二叉树的根节点

返回值描述:

输出一个布尔类型的值

示例1

输入:{1,2,3,4,5,6,7}

返回值:true

示例2

输入:{}

返回值:true

解题思路

  1. 因为题目里提到,这是一个二叉搜索树,所以左子树永远小于根节点,反之右子树永远大于根节点。
  2. 所以我们可以根据给定的两个 target 值,去找直到找到他的那个路径。
  3. 比较路径中的每个值,直到第一个两个路径中不相等的值,说明前一个节点的值就是他们的最近公共祖先。

AC代码

 /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @param p int整型
     * @param q int整型
     * @return int整型
     */
    public int lowestCommonAncestor (TreeNode root, int p, int q) {
        // write code here
        // 树为空 不存在公共祖先的说法
        if(root == null) return -1;
        int res = 0;
        // 求出 p q 的路径
        ArrayList<Integer> path_q = getPath(root, q);
        ArrayList<Integer> path_p = getPath(root, p);
        // 分别求出两个路径中公共长度的部分 找到第一个不相等的值
        for(int i = 0; i < path_p.size() && i < path_q.size();i++){
            int x = path_p.get(i);
            int y = path_q.get(i);
            if(x == y){
                // 将res替换为当前两个路径相同的节点值
                res = path_p.get(i);
            }else{
                break;
            }
        }
        return res;
    }

    public ArrayList<Integer> getPath(TreeNode root,int target){
        // 路径数组
        ArrayList<Integer> path = new ArrayList<>();
        // 创建一个工作节点用来存值
        TreeNode node = root;
        while(node.val != target){
            path.add(node.val);
            // 小的左子树反之在右子树
            if(target < node.val){
                node = node.left;
            }else{
                node = node.right;
            }
        }
        // 将target的值添加到路径中
        path.add(node.val);
        return path;
    }

在二叉树中找到两个节点的最近公共祖先

描述

给定一棵二叉树(保证非空)以及这棵树上的两个节点对应的val值 o1 和 o2,请找到 o1 和 o2 的最近公共祖先节点。

数据范围:树上节点数满足 1≤n≤105 , 节点值val满足区间 [0,n)

要求:时间复杂度O(n)

注:本题保证二叉树中每个节点的val值均不相同。

如当输入{3,5,1,6,2,0,8,#,#,7,4},5,1时,二叉树{3,5,1,6,2,0,8,#,#,7,4}如下图所示:

所以节点值为5和节点值为1的节点的最近公共祖先节点的节点值为3,所以对应的输出为3。
 

节点本身可以视为自己的祖先

示例1

输入:{3,5,1,6,2,0,8,#,#,7,4},5,1

返回值:3

示例2

输入:{3,5,1,6,2,0,8,#,#,7,4},2,7

返回值:2

解题思路

  1. 首先要找两个值的公共祖先,我们可以遍历每个节点,找到这两个值的路径,然后比对路径,找到公共最近祖先,可以优化一下,使用层序遍历,找到这两个节点就行。
  2. 我们需要创建一个 parent 的 Map, 里面的每个 key 和 value,存当前结点的值和父节点的值,还需要使用队列来存储每个节点和保证层序遍历。
  3. 然后通过队列入队和出队的操作,直到找到 o1 和 o2 的节点,在寻找o1 o2节点的时候, 顺便在parent(Map) 中添加其中所有节点的 parent,便于后续从下往上找到父节点。
  4. 创建一个 Set 集合去存储 o1 所有的祖先节点(也可以存储 o2 所有的祖先节点)。
  5. 然后根据集合中去寻找有没有是 o2 的祖先节点,找到第一个祖先节点,这便是题目所需的最近公共祖先。

比如

我们要找 6 7 的最近的公共祖先,我们就先找到这两个节点,然后找到 6 的所有公共祖先 (6 5 3),然后去找7的公共祖先,第一个是自己,第二个是2,这两个都不是,然后到 5 发现 6 和 7 共有的祖先,找到 5 是 6 和 7的最近公共祖先

AC代码

import java.util.*;

/*
 * public class TreeNode {
 *   int val = 0;
 *   TreeNode left = null;
 *   TreeNode right = null;
 *   public TreeNode(int val) {
 *     this.val = val;
 *   }
 * }
 */

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param root TreeNode类 
     * @param o1 int整型 
     * @param o2 int整型 
     * @return int整型
     */
    public int lowestCommonAncestor (TreeNode root, int o1, int o2) {
        // write code here
        // map 存储 root 中 每个节点的值和每个节点的父节点的值
        Map<Integer,Integer> parent = new HashMap<>();
        // 使用队列来保证层序遍历
        Queue<TreeNode> nodes = new LinkedList<>();
        // 根节点放入值中
        parent.put(root.val,Integer.MIN_VALUE);
        // 根节点入队
        nodes.offer(root);
        // 找到两个节点
        while(!parent.containsKey(o1) || !parent.containsKey(o2)){
            // 从头出队
            TreeNode curNode = nodes.poll();
            // 将左节点入队且将该值和该值父节点的值放入 parent 中 
            if(curNode.left != null){
                nodes.add(curNode.left);
                parent.put(curNode.left.val, curNode.val);
            }
            // 右节点同样操作
            if(curNode.right != null){
                nodes.add(curNode.right);
                parent.put(curNode.right.val, curNode.val);
            }
        }
        // 存放 o1 的所有祖先节点
        Set<Integer> ancestors = new HashSet<>();
        // 从下往上 找到所有 o1 的祖先节点
        while(parent.containsKey(o1)){
            // 添加祖先节点的值到 Set 中
            ancestors.add(o1);
            // 寻找o1的祖先节点
            o1 = parent.get(o1);
        }
        // 从下往上找 o2 的祖先节点 找到第一个和 o1 祖先节点相同的节点
        while(!ancestors.contains(o2)){
            //  自下而上 更新出 o2 所有的祖先节点
            o2 = parent.get(o2);
        }
        return o2;
    }
}

 其他文章推荐:

链表一(基本概念和应用场景)+ 链表常见算法题-CSDN博客 

链表二 链表常见算法题-CSDN博客

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值