树 与 二叉树

        在数据结构的世界里,线性结构(如数组、链表)虽基础,但面对层级关系的数据(如文件系统、组织架构)时却力不从心。而作为典型的非线性结构,恰好解决了这一痛点,其中二叉树更是因结构简洁、操作高效,成为树结构中的 “明星”—— 从算法面试到实际开发(如数据库索引、表达式解析),处处都有它的身影。

一、什么是树形结构?

        在学二叉树前,我们得先明白 “树” 的本质 —— 它是由n(n≥0)个有限节点组成的层次关系集合,像一棵倒挂的树:根朝上,叶朝下。

1.1 树的 3 个核心特点

  • 有且仅有一个根节点:根是树的 “起点”,没有前驱节点(比如文件系统的根目录/)。
  • 子树互不相交:任意两个子树之间没有公共节点,否则就不是合法的树(比如两个文件夹不能互相包含)。
  • 递归定义:除根外,其余节点被分成M(M>0)个互不相交的子树,每个子树也是一棵独立的树。

1.2 必须掌握的树结构术语

        这些术语是后续学习的 “语言基础”,务必理解清楚(结合下图辅助记忆:根为 A,A 有 B/C/D/E/F/G6 个子节点,D 有 H 子节点,E 有 I 子节点):

术语定义例子
结点的度一个节点拥有的子树个数A 的度为 6(有 6 个子节点)
树的度所有节点度的最大值整棵树的度为 6(A 的度最大)
叶子节点(终端节点)度为 0 的节点(没有子树)B、C、H、I 等
父 / 子节点若节点 A 有子树 B,则 A 是父节点,B 是子节点A 是 B 的父节点,B 是 A 的子节点
节点的层次从根开始计数,根为第 1 层,子节点为第 2 层,以此类推A 在第 1 层,B 在第 2 层,H 在第 3 层
树的高度(深度)树中节点的最大层次上图树的高度为 4(H/I 在第 3 层?不对,若 A1、B2、D3、H4,则高度为 4)
森林m (m≥0) 棵互不相交的树的集合若把 A 的 6 个子树单独拿出来,就是一个森林

1.3 树的表示方式:孩子兄弟表示法

        树的存储比线性表复杂,常见的有双亲表示法、孩子表示法等,其中孩子兄弟表示法最灵活(能轻松将树转为二叉树),核心思路是:每个节点存储 “第一个孩子” 和 “下一个兄弟” 的引用。

        用 Java 简单实现:

class TreeNode {
    int val;          //节点存储的数据
    TreeNode firstChild; //指向第一个孩子
    TreeNode nextBrother; //指向同一父节点的下一个兄弟
}

        比如节点 A 的firstChild是 B,B 的nextBrother是 C,C 的nextBrother是 D,以此类推 —— 这样就串联起了 A 的所有子节点。

1.4 树的实际应用

        最常见的就是文件系统管理:根目录是根节点,文件夹是分支节点(有子节点),文件是叶子节点(无子女),完美契合树的层次结构。

二、二叉树

        二叉树是树的 “特殊版”,但正是这份 “特殊” 让它变得高效 —— 接下来我们从概念、特性到操作,逐一拆解。

2.1 二叉树的定义:度不超过 2 的有序树

        一棵二叉树要么为空,要么由根节点 + 左子树 + 右子树组成,且满足两个关键规则:

  1. 所有节点的度≤2(最多有两个子节点:左孩子、右孩子);
  2. 子树有 “左右之分”,次序不能颠倒(比如左子树是 A、右子树是 B,和反过来是两棵不同的二叉树)。

        二叉树的 4 种基本形态:

  • 空树
  • 只有根节点
  • 根节点 + 左子树
  • 根节点 + 右子树
  • 根节点 + 左子树 + 右子树

2.2 两种特殊的二叉树

        实际应用中,满二叉树和完全二叉树是高频考点,务必区分清楚。

(1)满二叉树:“装满” 的二叉树

        如果二叉树的层数为k,且每层节点数都达到最大值(第i层有2^(i-1)个节点),则称为满二叉树。比如层数k=3的满二叉树,总节点数是2^3 - 1 = 7(第 1 层 1 个,第 2 层 2 个,第 3 层 4 个)。

(2)完全二叉树:“按顺序排满” 的二叉树

        完全二叉树由满二叉树衍生而来,核心定义是:深度为 k、有 n 个节点的二叉树,其节点编号(从上到下、从左到右)与深度为 k 的满二叉树中 1~n 号节点完全对应

        简单理解:完全二叉树是 “先把左半部分排满,再排右半部分”,不允许出现 “左空右不空” 的情况。比如满二叉树是特殊的完全二叉树,但完全二叉树不一定是满二叉树(比如最后一层缺右边几个节点)。

2.3 二叉树的 5 个必背性质(面试高频)

        这些性质是解题的 “公式”,必须牢记并会用!

性质 1:第 i 层最多有 2^(i-1) 个节点
  • 解释:第 1 层(根)最多 1 个(2^0),第 2 层最多 2 个(2^1),第 3 层最多 4 个(2^2),以此类推。
性质 2:深度为 k 的二叉树最多有 2^k - 1 个节点
  • 解释:每层节点数求和(等比数列):1 + 2 + 4 + ... + 2^(k-1) = 2^k - 1(满二叉树的总节点数)。
性质 3:叶子节点数 = 度为 2 的节点数 + 1(n0 = n2 + 1)
  • 推导:设总节点数为n,度为 0(叶子)、1、2 的节点数分别为n0n1n2,则:
    1. 总节点数:n = n0 + n1 + n2
    2. 总边数:每个节点除根外都有一条边指向父节点,所以边数 = n - 1;同时,度为 1 的节点贡献 1 条边,度为 2 的贡献 2 条,边数 = n1 + 2n2
    3. 联立得:n0 = n2 + 1
  • 例子:若有 10 个度为 2 的节点,叶子节点数就是 11。
性质 4:n 个节点的完全二叉树深度为 log₂(n+1)(上取整)
  • 解释:比如 n=5,log₂(5+1)≈2.58,上取整为 3(深度 3);n=7(满二叉树),log₂(7+1)=3,深度 3。
性质 5:完全二叉树的节点编号规律(从 0 开始编号)

对编号为i的节点:

  • 父节点编号:(i-1)/2(i>0 时,比如 i=2,父节点是 0);
  • 左孩子编号:2i + 1(若 2i+1 < n,否则无左孩子);
  • 右孩子编号:2i + 2(若 2i+2 < n,否则无右孩子)。

比如 n=5(编号 0~4):

  • 编号 2 的节点,左孩子是 5(>4,无),右孩子是 6(>4,无);
  • 编号 1 的节点,左孩子是 3,右孩子是 4。

2.4 二叉树的存储方式

        二叉树有两种存储方式,分别适用于不同场景。

(1)顺序存储:用数组存(适合完全二叉树)

        利用完全二叉树的编号规律(性质 5),将节点按编号存入数组:

  • 编号 i 的节点存在数组下标 i 的位置;
  • 父、子节点通过公式计算(无需指针)。

        缺点:非完全二叉树会浪费大量数组空间(比如右斜树,大部分位置为空)。

(2)链式存储:用链表存(通用)

        最常用的是孩子表示法:每个节点存储数据、左孩子引用、右孩子引用,结构灵活,适合所有二叉树。

Java 实现:

//二叉树节点类
class TreeNode {
    int val;          //数据域
    TreeNode left;    //左孩子引用(指向左子树)
    TreeNode right;   //右孩子引用(指向右子树)

    //构造方法
    public TreeNode(int val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

        还有 “孩子双亲表示法”(多一个 parent 引用),后续在平衡树中会用到,这里先聚焦孩子表示法。

2.5 二叉树的基本操作(核心中的核心)

        二叉树的操作大多基于递归(因为二叉树是递归定义的),我们从 “创建二叉树” 开始,逐步学习遍历、节点计数等操作。

2.5.1 临时创建二叉树(快速入门)

        为了快速上手操作,先手动创建一棵简单的二叉树(后续会讲从数组 / 字符串构建二叉树的通用方法):

public class BinaryTree {
    private TreeNode root; //根节点

    //手动创建二叉树(结构:1为根,左2右4;2左3;4左5;5右6)
    public void createTempTree() {
        TreeNode node1 = new TreeNode(1);
        TreeNode node2 = new TreeNode(2);
        TreeNode node3 = new TreeNode(3);
        TreeNode node4 = new TreeNode(4);
        TreeNode node5 = new TreeNode(5);
        TreeNode node6 = new TreeNode(6);

        root = node1;          //根为1
        node1.left = node2;    //1的左孩子是2
        node2.left = node3;    //2的左孩子是3
        node1.right = node4;   //1的右孩子是4
        node4.left = node5;    //4的左孩子是5
        node5.right = node6;   //5的右孩子是6
    }
}

这棵树的结构如下:

2.5.2 二叉树的遍历(重中之重)

        遍历是 “访问树中所有节点,且每个节点仅访问一次”,是后续所有操作的基础。二叉树有 4 种核心遍历方式:前序、中序、后序(递归)和层序(迭代)。

(1)前序遍历(NLR:根 → 左 → 右)

遍历顺序:先访问根节点,再递归遍历左子树,最后递归遍历右子树。

递归实现:

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

对上面的临时树,前序遍历结果:1 2 3 4 5 6

(2)中序遍历(LNR:左 → 根 → 右)

遍历顺序:先递归遍历左子树,再访问根节点,最后递归遍历右子树。

递归实现:

//中序遍历(递归)
public void inOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    inOrder(root.left);               //遍历左子树
    System.out.print(root.val + " "); //访问根节点
    inOrder(root.right);              //遍历右子树
}

对临时树,中序遍历结果:3 2 1 5 6 4

(3)后序遍历(LRN:左 → 右 → 根)

遍历顺序:先递归遍历左子树,再递归遍历右子树,最后访问根节点。

递归实现:

//后序遍历(递归)
public void postOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    postOrder(root.left);             //遍历左子树
    postOrder(root.right);            //遍历右子树
    System.out.print(root.val + " "); //访问根节点
}

对临时树,后序遍历结果:3 2 6 5 4 1

(4)层序遍历(自上而下,自左至右)

遍历顺序:从根节点开始,按层次依次访问每个节点(第 1 层→第 2 层→...→第 k 层),同一层从左到右。

实现思路:用队列(先进先出)辅助,步骤如下:

  1. 根节点入队;
  2. 队列非空时,出队当前节点并访问;
  3. 若当前节点有左孩子,左孩子入队;
  4. 若当前节点有右孩子,右孩子入队;
  5. 重复 2~4,直到队列为空。

代码实现:

import java.util.LinkedList;
import java.util.Queue;

//层序遍历
public 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.print(cur.val + " "); //访问

        //左孩子入队(先左后右,保证同一层顺序)
        if (cur.left != null) {
            queue.offer(cur.left);
        }
        //右孩子入队
        if (cur.right != null) {
            queue.offer(cur.right);
        }
    }
}

对临时树,层序遍历结果:1 2 4 3 5 6

2.5.3 其他常用操作(递归实现)

        这些操作的核心思路都是 “分解为子问题”—— 根节点的操作 + 左子树的操作 + 右子树的操作。

(1)获取树的总节点数
public int getNodeCount(TreeNode root) {
    if (root == null) {
        return 0; //空树节点数为0
    }
    //总节点数 = 1(根) + 左子树节点数 + 右子树节点数
    return 1 + getNodeCount(root.left) + getNodeCount(root.right);
}
(2)获取叶子节点数
public int getLeafCount(TreeNode root) {
    if (root == null) {
        return 0;
    }
    //叶子节点:左右孩子都为空
    if (root.left == null && root.right == null) {
        return 1;
    }
    //叶子数 = 左子树叶子数 + 右子树叶子数
    return getLeafCount(root.left) + getLeafCount(root.right);
}
(3)获取第 k 层的节点数
public int getKLevelCount(TreeNode root, int k) {
    if (root == null || k < 1) {
        return 0; //空树或k无效(层次从1开始)
    }
    if (k == 1) {
        return 1; //第1层只有根节点
    }
    //第k层节点数 = 左子树第k-1层节点数 + 右子树第k-1层节点数
    return getKLevelCount(root.left, k-1) + getKLevelCount(root.right, k-1);
}
(4)获取树的高度(深度)
public int getTreeHeight(TreeNode root) {
    if (root == null) {
        return 0; //空树高度为0
    }
    //树的高度 = 1(当前层) + max(左子树高度, 右子树高度)
    int leftHeight = getTreeHeight(root.left);
    int rightHeight = getTreeHeight(root.right);
    return 1 + Math.max(leftHeight, rightHeight);
}
(5)查找值为 val 的节点
public TreeNode findNode(TreeNode root, int val) {
    if (root == null) {
        return null; //空树,未找到
    }
    if (root.val == val) {
        return root; //找到,返回当前节点
    }
    //先在左子树找,找到则返回
    TreeNode leftResult = findNode(root.left, val);
    if (leftResult != null) {
        return leftResult;
    }
    //左子树没找到,在右子树找
    return findNode(root.right, val);
}
(6)判断是否为完全二叉树

思路:用层序遍历,遇到空节点后,后续不能再出现非空节点(否则不是完全二叉树)。

public boolean isCompleteTree(TreeNode root) {
    if (root == null) {
        return true; //空树是完全二叉树
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    boolean hasNull = false; //标记是否遇到过空节点

    while (!queue.isEmpty()) {
        TreeNode cur = queue.poll();
        if (cur == null) {
            hasNull = true; //遇到空节点,标记
        } else {
            if (hasNull) {
                return false; //空节点后出现非空节点,不是完全二叉树
            }
            //不管孩子是否为空,都入队(后续判断)
            queue.offer(cur.left);
            queue.offer(cur.right);
        }
    }
    return true;
}

三、实战提升:经典二叉树 OJ 题思路

        掌握了基础操作后,我们来看看面试中常见的二叉树题目,如何用所学知识解决。

1. 检查两棵树是否相同(LeetCode 100)

        题目:判断两棵二叉树的结构和节点值是否完全相同。思路:递归比较根节点 + 左子树 + 右子树:

  • 都为空:相同;
  • 一个空一个非空:不同;
  • 根节点值不同:不同;
  • 递归比较左子树和右子树。
public boolean isSameTree(TreeNode p, TreeNode q) {
    if (p == null && q == null) {
        return true;
    }
    if (p == null || q == null) {
        return false;
    }
    if (p.val != q.val) {
        return false;
    }
    //递归比较左和右
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}

2. 翻转二叉树(LeetCode 226)

        题目:将二叉树的左右子树互换(镜像翻转)。思路:递归交换每个节点的左右子树:

  • 空树直接返回;
  • 交换当前节点的左右孩子;
  • 递归翻转左子树和右子树。
public TreeNode invertTree(TreeNode root) {
    if (root == null) {
        return null;
    }
    //交换左右孩子
    TreeNode temp = root.left;
    root.left = root.right;
    root.right = temp;
    //递归翻转左、右子树
    invertTree(root.left);
    invertTree(root.right);
    return root;
}

3. 二叉树的最近公共祖先(LeetCode 236)

        题目:找到二叉树中两个指定节点的最近公共祖先(LCA)。思路:递归查找,利用 “祖先的特性”:

  • 若 root 是 p 或 q,root 就是 LCA;
  • 递归在左子树找 p/q,在右子树找 p/q;
  • 若左子树找到一个,右子树找到一个,root 是 LCA;
  • 若只有左子树找到,返回左子树结果;反之返回右子树结果。
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);
    //左右都找到,root是LCA
    if (left != null && right != null) {
        return root;
    }
    //只在左/右找到,返回对应结果
    return left != null ? left : right;
}

四、总结

        二叉树的核心是递归思维遍历逻辑—— 从树的基础概念到二叉树的特性、操作,再到实战题目,所有知识点都围绕这两点展开。想要真正掌握二叉树,建议:

  1. 牢记 5 个性质,尤其是 n0=n2+1 和完全二叉树的编号规律;
  2. 手动模拟前中后序遍历(画递归栈),理解递归过程;
  3. 多做 OJ 题,比如非递归遍历、根据遍历序列构建二叉树等,强化应用能力。

二叉树是后续学习平衡树(AVL、红黑树)、B 树的基础,打好这个地基,后续复杂的数据结构学习会轻松很多!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值