零基础吃透二叉树:定义、遍历与高频算法 —数据结构柒

你好,我是林森lsjs

我的Github 地址: sqyCoder (Qiyang) · GitHub

以博文记录成长,用心打磨代码与思维

目录

一、必会概念

二、两种特殊二叉树

[2.1 满二叉树](#2.1 满二叉树)

[2.2 完全二叉树](#2.2 完全二叉树)

三、一些计算(面试基本不考)

四、二叉树的存储

五、二叉树的遍历

[5.1 先序遍历](#5.1 先序遍历)

[5.2 中序遍历](#5.2 中序遍历)

[5.3 后序遍历](#5.3 后序遍历)

[5.4 层序遍历](#5.4 层序遍历)

六、二叉树还原

七、二叉树的常见方法

[7.1 获取树中节点的个数](#7.1 获取树中节点的个数)

[7.2 获取叶子节点的个数(遍历思路)](#7.2 获取叶子节点的个数(遍历思路))

[7.3 子问题思路‑求叶子结点个数](#7.3 子问题思路‑求叶子结点个数)

[7.4 获取第K层节点的个数](#7.4 获取第K层节点的个数)

[7.5 获取二叉树的高度(你的原有代码)](#7.5 获取二叉树的高度(你的原有代码))

[7.6 检测值为value的元素是否存在,存在则返回该节点](#7.6 检测值为value的元素是否存在,存在则返回该节点)

[7.7 层序遍历【递归版,你的原有代码】](#7.7 层序遍历【递归版,你的原有代码】)

[7.8 判断一棵树是不是完全二叉树](#7.8 判断一棵树是不是完全二叉树)


一、必会概念

复制代码
            A      ←【根结点】层次1
         /     \
        B       C    ← B、C 互为【兄弟结点】,层次2
       / \        \
      D   E        F   ← D、E 兄弟;D、F / E、F 是【堂兄弟结点】,层次3
         /
        G               ← 层次4
  1. 结点的度:结点拥有子结点数量。本例:A 度 = 2;B 度 = 2;C 度 = 1;D 度 = 0;E 度 = 1;F 度 = 0;G 度 = 0。

  2. 树的度:整棵树所有结点度的最大值。本例最大度是 2,树的度 = 2。

  3. 叶子结点 (终端结点):度为 0 的结点,没有任何子结点。图中 D、F、G 都是叶子。

  4. 双亲(父结点):直接向上相连的前驱结点。B 是 D、E 的父结点;A 是 B、C 的父结点。

  5. 子结点(孩子结点):D、E 是 B 的子结点;二叉树严格区分左孩子、右孩子。

  6. 根结点:A,唯一没有父结点的结点;一棵树有且仅有一个根。

  7. 结点的层次:规定根结点层次 = 1,向下逐层 + 1。A 层次 1;B、C 层次 2;D、E、F 层次 3;G 层次 4。

  8. 叶子结点 (分支结点):度>0,拥有子结点。本例:A、B、C、E。

  9. 兄弟结点:拥有同一个父结点的子结点。B 和 C;D 和 E。

  10. 树的高度 / 深度:树内结点层次的最大值。本树最大层次为 4,树高度 = 4。

  11. 堂兄弟结点:结点层次相同,父结点互为兄弟(父亲不是同一个)。D、E 的父亲是 B,F 父亲是 C,B、C 是兄弟,因此 D 与 F、E 与 F 互为堂兄弟。

  12. 结点的祖先(以 G 举例):从根到达该结点路径上所有上层结点(不含自身):E、B、A。

  13. 子孙(以 B 举例):该结点所有后代(所有子、孙子等):D、E、G。

  14. 森林:若干棵互不相交的树的集合。删掉根 A,剩余 B 子树、C 子树,两棵独立树构成一片森林。

二、两种特殊二叉树

2.1 满二叉树

一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。

也就是说,如果一棵二叉树的层数为K,且结点总数是,则它就是满二叉树。

2.2 完全二叉树

一棵二叉树,除了最后一层之外,其余每一层的节点都必须放满;最后一层的所有节点靠左紧密连续排列,不能出现空隙

合法完全二叉树

java 复制代码
        1
      /   \
     2     3
    / \
   4   5

层数 0:1 个节点(满)层数 1:2 个节点(满)层数 2:节点 4、5 靠左连续,右侧空缺,符合完全二叉树。

不是完全二叉树

复制代码
java 复制代码
        1
      /   \
     2     3
        \
         5

最后一层节点 5 出现在右侧,左边 4 缺失,中间有空隙,不满足。

三、一些计算(面试基本不考)

  1. 根结点所在层次为1。对于非空二叉树,第i层最多有2的(i-1)次方个结点,其中i大于等于1。

  2. 根层次为1时,深度为K的二叉树,其结点总数最多为2的K次方减1。

  3. 任意一棵二叉树中,叶子结点(即度为0的结点)的数量等于度为2的结点数量加1。

  4. 对于有n个结点的完全二叉树,其深度k等于以2为底(n+1)的对数再向上取整。

  5. 完全二叉树若按从上到下、从左到右的顺序编号,且编号从0开始,则:

  • 当i大于0时,结点i的父结点编号为(i-1)除以2后向下取整;

  • 结点i的左孩子编号为2i+1,但只有当2i+1小于n时,该左孩子才实际存在;

  • 结点i的右孩子编号为2i+2,但只有当2i+2小于n时,该右孩子才实际存在。

其中n为二叉树的总结点数。

四、二叉树的存储

复制代码
java 复制代码
class Node {
    int val;
    Node left;
    Node right;
    Node(int val) {
        this.val = val;
        left = null;
        right = null;
    }
}

每个节点保存数值、左孩子引用、右孩子引用;适合任意形态二叉树。

五、二叉树的遍历

5.1 先序遍历

复制代码
java 复制代码
public static void preOrder(TreeNode root) {
    // 递归终止条件:空树直接返回
    if (root == null) {
        return;
    }
    // 先访问当前根节点
    System.out.print(root.val + " ");
    // 递归遍历左子树
    preOrder(root.left);
    // 递归遍历右子树
    preOrder(root.right);
}

5.2 中序遍历

复制代码
java 复制代码
// 中序遍历
public static void inOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    // 先递归左子树
    inOrder(root.left);
    // 左子树走完,再访问根节点
    System.out.print(root.val + " ");
    // 递归遍历右子树
    inOrder(root.right);
}

5.3 后序遍历

复制代码
java 复制代码
// 后序遍历
public static void postOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    // 先递归左子树
    postOrder(root.left);
    // 再递归右子树
    postOrder(root.right);
    // 左右全部遍历完成,最后访问根节点
    System.out.print(root.val + " ");
}

5.4 层序遍历

复制代码
java 复制代码
public static void levelOrder(TreeNode root) {
    // 空树直接结束
    if (root == null) {
        return;
    }
    // 引入队列保存节点
    Queue<TreeNode> queue = new LinkedList<>();
    // 根节点先入队
    queue.offer(root);
    // 队列不为空持续循环
    while (!queue.isEmpty()) {
        // 取出队首节点
        TreeNode cur = queue.poll();
        // 访问当前节点
        System.out.println(cur.val);
        // 左孩子不为空,入队列
        if (cur.left != null) {
            queue.offer(cur.left);
        }
        // 右孩子不为空,入队列
        if (cur.right != null) {
            queue.offer(cur.right);
        }
    }
}

六、二叉树还原

想要还原二叉树,先抓住核心规律:先序第一个是根,后序最后一个是根,中序里根左边是左子树、右边是右子树,所以必须带上中序才能唯一还原。

拿先序 1,2,4,5,3、中序 4,2,5,1,3 来说,先取出先序第一个 1 当作根,去中序找到 1,左边 4、2、5 是左子树,右边 3 是右子树。左子树一共 3 个节点,在先序里根往后数 3 个数字 2,4,5 就是左子树片段,剩下 3 是右子树片段。接着处理左子树片段,按照先序的规则,一段先序序列首位就是当前子树的根,因此首位 2 作为根,在对应中序找到 2,左侧 4、右侧 5 分别作为左右孩子,右子树只有 3,没有后代,树就构建完成。代码依靠区间边界递归,区间起点超过终点就返回 null,用左子树节点数量切割序列。

后序加中序思路完全一样,只是根取片段最后一个元素。而先序搭配后序不能还原,没办法区分节点是左孩子还是右孩子。

七、二叉树的常见方法

7.1 获取树中节点的个数

复制代码
java 复制代码
public int getNodeCount(TreeNode root) {
    //递归终止条件,如果当前节点为null,节点数是0
    if (root == null) {
        return 0;
    }
    // 当前根节点算1个,再加上左子树全部节点,加上右子树全部节点
    return 1 + getNodeCount(root.left) + getNodeCount(root.right);
}

我们来一步步看,当传入一棵树的根节点进来。第一步先判断root是不是null,如果是空树直接返回0。如果节点不为空,1代表当前这个根节点本身,然后交给递归去算左子树一共有多少节点,再算右子树一共有多少节点,三者相加就是整棵树总节点。递归会一直拆到叶子往下的null,返回0,然后一层层向上回溯累加算出总数。

7.2 获取叶子节点的个数(遍历思路)

复制代码
java 复制代码
private int leafCount = 0;
public int getLeafNodeCount(TreeNode root) {
    //每次调用先把计数器清零,防止上一次调用残留数据
    leafCount = 0;
    getLeafCountByTraversal(root);
    return leafCount;
}
private void getLeafCountByTraversal(TreeNode root) {
    if (root == null) {
        return;
    }
    //叶子节点:左右都为空
    if (root.left == null && root.right == null) {
        leafCount++;
    }
    //递归访问左子树
    getLeafCountByTraversal(root.left);
    //递归访问右子树
    getLeafCountByTraversal(root.right);
}

入口方法进来第一件事把成员变量leafCount重置为0,避免上次调用的旧数据干扰结果。调用辅助递归函数开始遍历,遇到null直接return结束这一条分支。如果当前节点左右孩子全是null,这就是叶子节点,计数器就加1。之后继续递归往左走,再递归往右走,把整棵树全部节点遍历一遍。全部遍历结束之后,leafCount里面存的就是叶子总数量,返回给调用者。

7.3 子问题思路‑求叶子结点个数

复制代码
java 复制代码
public int getLeafNodeCountBySub(TreeNode root) {
    //空树,叶子0个
    if (root == null) {
        return 0;
    }
    //当前就是叶子节点,返回1
    if (root.left == null && root.right == null) {
        return 1;
    }
    //当前不是叶子,结果等于左子树叶子数量 + 右子树叶子数量
    return getLeafNodeCountBySub(root.left) + getLeafNodeCountBySub(root.right);
}

这个版本不用成员变量,纯靠递归子问题。如果节点为空返回0。如果当前节点左右都为空,说明自己就是叶子,直接返回1。如果不是叶子,自己不能算叶子,那整棵树叶子就全部来自左子树和右子树,把左右两棵子树算出来的叶子数相加,向上返回。递归不断拆小树,回溯的时候累加得到最终叶子总数。

7.4 获取第K层节点的个数

复制代码
java 复制代码
public int getKLevelNodeCount(TreeNode root, int k) {
    //树为空 或者k是非法负数,直接返回0
    if (root == null || k <= 0) {
        return 0;
    }
    //k等于1,代表当前就是第一层,也就是传入的根节点,节点数为1
    if (k == 1) {
        return 1;
    }
    //求左子树k‑1层节点数量 + 右子树k‑1层节点数量
    return getKLevelNodeCount(root.left, k - 1) + getKLevelNodeCount(root.right, k - 1);
}

首先判断边界,如果树是空,或者k传了小于等于0的非法值,直接返回0。当k==1的时候,代表现在来到目标层,当前这个节点就是目标层节点,返回1。否则,我要找当前树第k层,等价于去找左子树的k‑1层,加上右子树的k‑1层。每往下递归一层,k就减1,直到k变成1,统计节点,回溯相加。

7.5 获取二叉树的高度(你的原有代码)

复制代码
java 复制代码
public int getHeight(TreeNode root) {
    if (root == null) {
        return 0;
    }
    if (root.left == null && root.right == null) {
        return 1;
    }
    return 1 + Math.max(getHeight(root.left), getHeight(root.right));
}

第一步,如果root等于null,空树高度是0。如果当前是叶子节点,没有左右孩子,高度直接返回1。其他情况,1代表当前节点本身这一层,递归算出左子树高度,递归算出右子树高度,取两者更大的那一个,加上当前这一层,就是整棵树高度。递归一路走到最底下叶子,再回溯向上算高度。

7.6 检测值为value的元素是否存在,存在则返回该节点

复制代码
java 复制代码
public TreeNode find(TreeNode root, int val) {
    //空树,找不到,返回null
    if (root == null) {
        return null;
    }
    //当前节点值匹配,直接返回这个节点对象
    if (root.val == val) {
        return root;
    }
    //先去左子树查找
    TreeNode leftNode = find(root.left, val);
    //左子树不为null,说明左边找到了,直接返回结果,不用再跑右边
    if (leftNode != null) {
        return leftNode;
    }
    //左子树没找到,才去右子树查找并返回
    return find(root.right, val);
}

如果树为空直接返回null。先判断当前根节点是不是要找的值,如果匹配直接返回当前节点。不匹配,就先递归搜索左子树,拿到leftNode。如果leftNode不等于null,代表左子树里面已经找到了目标,直接把结果返回,不再执行右子树递归。只有左子树完全搜完没找到,才进入右子树递归查找。两边都找不到最终返回null。

7.7 层序遍历

复制代码
java 复制代码
private List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> levelOrder(TreeNode root) {
    result.clear();
    if (root == null) {
        return result;
    }
    levelOrderHelper(root, 0);
    return result;
}
public void levelOrderHelper(TreeNode root, int level) {
    if (root == null) {
        return;
    }
    if (result.size() == level) {
        result.add(new ArrayList<>());
    }
    List<Integer> curRow = result.get(level);
    curRow.add(root.val);
    levelOrderHelper(root.left, level + 1);
    levelOrderHelper(root.right, level + 1);
}

调用levelOrder入口方法,首先调用result.clear()把成员二维列表清空,防止上一次调用残留旧数据。root为空直接返回空集合。调用辅助函数,根节点层数传0。进入helper,如果root是空直接返回。判断result.size() == level:二维list的size代表已经存了多少层,如果当前层数等于集合大小,说明还没有这一层的容器,new一个ArrayList添加进去。拿到当前层的list,把节点的值add进去。然后递归左孩子,层数+1,再递归右孩子,层数+1。递归走完,二维list里面就是按层分组的遍历结果。

7.8 判断一棵树是不是完全二叉树

复制代码
java 复制代码
public boolean isCompleteTree(TreeNode root) {
    if (root == null) {
        return true;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    boolean meetNull = false;

    while (!queue.isEmpty()) {
        TreeNode cur = queue.poll();
        if (cur != null) {
            if (meetNull) {
                return false;
            }
            queue.offer(cur.left);
            queue.offer(cur.right);
        } else {
            meetNull = true;
        }
    }
    return true;
}

空树直接判定是完全二叉树。创建队列,根节点入队。布尔变量meetNull标记有没有已经遇到过null节点。循环队列不为空,取出队首元素cur。如果cur不是null,先看标记,如果meetNull已经是true,代表前面已经出现过空节点,现在又出现真实节点,说明中间有空缺,直接返回false。把当前节点的左右孩子全部入队,就算孩子是null也直接入队 。如果取出的cur是null,就把meetNull置为true,代表后面不能再有真实节点。队列全部处理完没有触发false,循环结束返回true,代表是完全二叉树。

今天的数据结构讲解就到这了,我们下期再见!

++诸位共勉!++

相关推荐
MacroZheng1 小时前
又一个神级画图Skill开源,再见draw.io!
java·人工智能·后端
船厂电气自动化ai大模型1 小时前
AI大模型与数学·第56课 快速傅里叶变换FFT:DFT高效优化算法,图像、音频、扩散模型工程加速核心工具
数据结构·人工智能·深度学习·算法·机器学习
203号居民1 小时前
【无标题】LeetCode hot 100 —54. 螺旋矩阵
算法
2401_894915531 小时前
GEO 优化源码性能调优:高并发地域请求缓存与索引优化
java·服务器·网络·数据库·缓存
ai产品老杨1 小时前
越界检测算法接入 AI 视频分析平台的流程与误报优化指南
人工智能·算法·音视频
bluesky9612221 小时前
malloc 和 realloc C/C++
c语言·c++·算法
wind100321 小时前
Qt5Widgets.dll 缺失报错|游戏软件无法启动完整修复教程
开发语言·qt·游戏·电脑
我叫张土豆1 小时前
在 OpenClaw Java 对话框输入一句话后,系统是如何运行的?
java·开发语言·人工智能
liangbo71 小时前
JVM规范第 4 章:class 文件格式
java·jvm