【数据结构】二叉树

目录

一,前言

二,重要概念

1,树的基本概念

2,二叉树的基本概念

1,二叉树的概念

2,两种特殊的二叉树

3,二叉树的性质

4,二叉树的存储

三,基本操作

1,类定义

2,基本操作

[<1> 递归前/中/后序遍历](#<1> 递归前/中/后序遍历)

[<2> 递归建树](#<2> 递归建树)

[<3> 层序遍历](#<3> 层序遍历)

四,结尾


一,前言

在本篇文章之前,我们所学习的顺序表、链表、栈、队列等等均属于线性结构,而本文所讲述的二叉树则是属于树型结构。

树是一种非线性的数据结构,是由n个有限节点组成的一个具有层次关系的集合。把它叫做树是因为它看起来像是一棵倒挂的树,也就是它是根朝上,叶朝下的,如下图所示。

树形结构具有以下基本性质:

1,树形结构中,子树之间不能有交集,否则就不是树形结构

2,除了根节点外,每个节点有且仅有一个父节点

3,一棵有N个节点的树有N-1条边

二,重要概念

1,树的基本概念

度:

  • 一个结点含有子树的个数称为该结点的度; 如上图:A的度为6
  • 一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6

节点:

  • 叶子节点/终端节点:度为0的结点称为叶节点 如上图:B、C、H、I...等节点为叶节点
  • 双亲节点/父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
  • 孩子节点/子节点:一个节点子树的节点称为该节点的子节点; 如上图:B是A的孩子结点
  • 根节点:一棵树中,没有双亲节点的结点;如上图:A

节点的层次:

  • 从根开始定义起,根为第1层,根的子结点为第2层,以此类推

节点间关系:

  • 兄弟节点:具有相同父节点的节点互称为兄弟节点;如上图:B、C是兄弟节点
  • 堂兄弟节点:父节点互为兄弟节点,双亲在同一层且互相不是兄弟的节点互为堂兄弟节点;如上图:H、I互为兄弟节点

树的高度/深度:

  • 树中结点的最大层次; 如上图:树的高度为4

2,二叉树的基本概念

1,二叉树的概念

一棵二叉树是由节点组成的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成,如下图所示:

从上图可以看出:

1,二叉树不存在度大于2的节点

2,二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

并且,对于任意的二叉树都是由以下几种情况复合而成的:

2,两种特殊的二叉树

2.1满二叉树:一棵二叉树,如果每层的节点数都达到了该层所能容纳的最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为k,且节点总数是2^k - 1,则它就是满二叉树。

2.2 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树由满二叉树衍生出来。对于深度为k,有n 个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从0至n-1的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树

3,二叉树的性质

3.1 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个节点

3.2 若规定根节点的深度为1,则深度为k的二叉树最大节点个数为2^k - 1,此时为满二叉树

3.3 具有n个节点的完全二叉树深度k为 log2(n+1)向上取整(以2为底)

3.4 对于具有n个节点的完全二叉树,如果按照从上到下、从左到右的顺序对所有节点从0开始编号,则对于序号i的节点有以下性质(编号从0开始):

  • 若i>0,父节点序号:(i-1)/2;若i==0,i为根节点序号,无双亲节点
  • 若 2i + 1 < n,则左孩子序号为 2i + 1,否则无左孩子
  • 若 2i + 2 < n,则右孩子序号为 2i + 2,否则无右孩子
4,二叉树的存储

二叉树的存储结构分为顺序存储和类似链表的链式存储,这需要根据树的类型进行选型。一般的,若树为完全二叉树则选择使用顺序存储,此时不会造成空间上的浪费;否则使用链式存储。我们此处所接触的都是普通二叉树,故着重讲解链式存储。

二叉树的链式存储是通过一个一个节点引用链接起来的,常见的表示方式有二叉和三叉表示方式,如下所示:

java 复制代码
// 孩子表示法
class Node {
    int val; // 数据域
    Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
    Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
 
// 孩子双亲表示法
class Node {
    int val; // 数据域
    Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
    Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
    Node parent;    // 当前节点的父节点
}

三,基本操作

1,类定义

由于本篇我们所讲解的都是普通二叉树,并且重点为执行逻辑,故固定节点数据类型为String。二叉树采用链式存储结构,节点使用孩子双亲表示法。

javascript 复制代码
public class MyBinaryTree {

    /**
     * 静态成员内部类*/
    static class TreeNode {
        String val;
        TreeNode left;
        TreeNode right;

        TreeNode(String val) {
            this.val = val;
        }
    }
}

因为二叉树普遍是通过递归创建的,但是我们目前还未了解到二叉树的遍历,故此处先手动建树进行讲解,后续讲解完遍历后再通过递归建树。与此同时,本文后续所涉及的所有操作均基于下图中的树进行展开,故我们手动创建该树。

手动建树代码:

javascript 复制代码
    /**
     * 手动建树*/
    public TreeNode createTreeByHands() {
        // 手动创建节点
        TreeNode A = new TreeNode("A");
        TreeNode B = new TreeNode("B");
        TreeNode C = new TreeNode("C");
        TreeNode D = new TreeNode("D");
        TreeNode E = new TreeNode("E");
        TreeNode F = new TreeNode("F");
        TreeNode G = new TreeNode("G");
        TreeNode H = new TreeNode("H");
        // 手动建立连接
        A.left = B;
        A.right = C;
        B.left = D;
        B.right = E;
        E.right = H;
        C.left = F;
        C.right = G;
        // 返回根节点
        return A;
    }

2,基本操作

<1> 递归前/中/后序遍历

学习二叉树结构,最简单的方式就是遍历。所谓遍历就是指沿着某条搜索路线,依次对树中每个节点做且仅作一次访问,而访问节点所做的操作依赖于具体的问题。

对于二叉树的遍历而言,我们根据根节点的访问时机差异,区分了以下三种遍历方式:

  • 前序遍历:访问根节点 ==> 左子树 ==> 右子树
  • 中序遍历:左子树 ==> 访问根节点 ==> 右子树
  • 后序遍历:左子树 ==> 右子树 ==> 访问根节点

由于递归遍历的前中后序遍历逻辑相似,故此处仅讲解前序遍历,代码如下:

java 复制代码
    /**
     * 前序遍历-递归实现*/
    public void preOrderInRecursion(TreeNode root) {
        // 遇见空返回
        if(root == null) {
            return;
        }
        // 打印根节点
        System.out.print(root.val + " ");
        // 递归左右子树
        preOrderInRecursion(root.left);
        preOrderInRecursion(root.right);
    }

递归逻辑如下(仅演示了左子树,右子树逻辑同理):

中序遍历代码:

java 复制代码
    /**
     * 中序遍历-递归实现*/
    public void inOrderInRecursion(TreeNode root) {
        // 遇见空返回
        if(root == null) {
            return;
        }
        // 递归左子树
        inOrderInRecursion(root.left);
        // 打印根节点
        System.out.print(root.val + " ");
        // 递归右子树
        inOrderInRecursion(root.right);
    }

后序遍历代码:

java 复制代码
    /**
     * 后序遍历-递归实现*/
    public void postOrderInRecursion(TreeNode root) {
        // 遇见空返回
        if(root == null) {
            return;
        }
        // 递归左右子树
        postOrderInRecursion(root.left);
        postOrderInRecursion(root.right);
        // 打印根节点
        System.out.print(root.val + " ");
    }
<2> 递归建树

经过<1>中所讲解的递归遍历知识铺垫,我们便可以实现递归建树了。此处为了能够完成一个完整的建树过程,故引入牛客网中的一道综合编程题进行讲解,如下。

题目 二叉树遍历_牛客题霸_牛客网编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。题目来自【牛客题霸】https://www.nowcoder.com/practice/4b91205483694f449f94c179883c1fef?tpId=60&&tqId=29483&rp=1&ru=/activity/oj&qru=/ta/tsing-kaoyan/question-ranking

解析

本题需要我们通过一串前序遍历字符串构建一棵二叉树,再对这棵二叉树进行中序遍历并输出。树的结构、中序遍历等内容在前文中已经讲解,故此处仅需完成建树。

我们需要解决两个问题:

  1. 如何在递归中遍历字符串?
  2. 如何通过递归创建树?

首先第一个问题,我们如何在递归中遍历字符串?简单的做法是在类中添加一个成员变量记录遍历位置,但是如果这样做,在第一次建树后成员变量无法复位,故此法只能建树一次;若不想手动重置成员变量,则我们可以将遍历下标包装成类,每次递归传递这个类的实例,如此即可实现多次建树,如下:

java 复制代码
/**
 * 将下标包装成对象,每次建树传递该对象实例*/
class IndexWrap {
    public int index;
}

其次第二个问题,我们如何通过递归创建树?由题可得,其给定的是一个前序遍历的字符串,故我们建树时的递归路径也必定是前序遍历,我们在此路径的基础上递推时创建节点,回退时建立连接,随后返回根节点即可。如此用文字叙述会显得很晦涩,故重点看后续讲解。

建树逻辑

java 复制代码
    /**
     * 递归建树*/
    public TreeNode createTreeInRecursion(String s) {
        // 创建下标实例并传递
        IndexWrap indexWrap = new IndexWrap();
        indexWrap.index = 0;
        return createTree(s, indexWrap);
    }

    /**
     * 实际实现的方法
     *  使用IndexWrap成员变量index维护遍历下标,如此可以保证多次建树*/
    private TreeNode createTree(String s, IndexWrap indexWrap) {
        // <index下标在每次读取后立即步进>
        String curVal = String.valueOf(s.charAt(indexWrap.index ++));
        // 判空
        if("#".equals(curVal)) {
            return null;
        }
        // 创建节点
        TreeNode root = new TreeNode(curVal);
        // 连接左右子树
        root.left = createTree(s, indexWrap);
        root.right = createTree(s, indexWrap);
        // 返回头节点
        return root;
    }

上述为建树的主要代码,其中createTreeInRecursion方法的作用为创建下标对象并传递,如此便可以达到多次建树的目的;而createTree方法则实际完成建树,其逻辑如下(仅演示左子树,右子树同理):

实现

java 复制代码
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        // 接收输入
        Scanner in = new Scanner(System.in);
        String s =in.nextLine();
        // 建树
        MyBinaryTree tree = new MyBinaryTree();
        MyBinaryTree.TreeNode root = tree.createTreeInRecursion(s);
        // 中序遍历
        tree.inOrderInRecursion(root);
    }
}

class MyBinaryTree {
    /**
     * 静态成员内部类*/
    static class TreeNode {
        String val;
        TreeNode left;
        TreeNode right;

        TreeNode(String val) {
            this.val = val;
        }
    }

        /**
     * 中序遍历-递归实现*/
    public void inOrderInRecursion(TreeNode root) {
        // 遇见空返回
        if(root == null) {
            return;
        }
        // 递归左子树
        inOrderInRecursion(root.left);
        // 打印根节点
        System.out.print(root.val + " ");
        // 递归右子树
        inOrderInRecursion(root.right);
    }

    /**
     * 递归建树*/
    public TreeNode createTreeInRecursion(String s) {
        // 创建下标实例并传递
        IndexWrap indexWrap = new IndexWrap();
        indexWrap.index = 0;
        return createTree(s, indexWrap);
    }

    /**
     * 实际实现的方法
     *  使用IndexWrap成员变量index维护遍历下标,如此可以保证多次建树*/
    private TreeNode createTree(String s, IndexWrap indexWrap) {
        // <index下标在每次读取后立即步进>
        String curVal = String.valueOf(s.charAt(indexWrap.index ++));
        // 判空
        if("#".equals(curVal)) {
            return null;
        }
        // 创建节点
        TreeNode root = new TreeNode(curVal);
        // 连接左右子树
        root.left = createTree(s, indexWrap);
        root.right = createTree(s, indexWrap);
        // 返回头节点
        return root;
    }
}

/**
 * 将下标包装成对象,每次建树传递该对象实例*/
class IndexWrap {
    public int index;
}
<3> 层序遍历

除了前/中/后序遍历外,我们还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为一,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的根节点,然后从左至右访问第二层上的节点,紧接着是第三层的节点,以此类推......从上到下,从左到右逐层访问树中每一个节点的过程就是层序遍历,如下图所示。

在代码实现中,我们若想实现层序遍历,还需要借助队列这一数据结构,其逻辑如下:

1,将根节点入队

2,根据队列状态进行处理:

2.1 队列为空 ==> 已遍历完所有节点

2.2 队列不为空 ==> 出队队头节点打印,并将该节点的非空左右子树节点入队(先左后右)

由于每次出队都会按照先左后右的顺序入队非空左右子树节点,故可以保证所有节点都以从上到下、从左到右的顺序入队;又由于队列具有先进先出的性质,则出队打印的顺序也必定是从上到下、从左到右,即实现了层序遍历,执行逻辑如下图所示。

注:该层序遍历其实也是宽度优先遍历算法(BFS)

java 复制代码
    /**
     * 层序遍历*/
    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);
            }
        }
    }

四,结尾

本文较为系统地讲解了二叉树的基本概念与遍历操作,后续还有一些二叉树相关的OJ题,为了避免内容过于冗杂,故分别发布。

相关推荐
轩轶子2 小时前
Python CLI Todo:一次数据结构错误引发的 Bug
数据结构·python·bug
Omics Pro2 小时前
空间组学!医学代谢显微工具
数据库·人工智能·算法·机器学习·自然语言处理
Coder-magician2 小时前
《代码随想录》刷题打卡day44:图论-part02
算法·深度优先·图论
克里斯蒂亚诺更新2 小时前
理解机器学习的各种算法
人工智能·算法·机器学习
小范的技术工坊2 小时前
大模型学习操作文档(13个核心概念串讲)
人工智能·学习·算法·大模型
一个低调的青年2 小时前
电池健康状态估计(一)
经验分享·笔记·其他·算法·模型
longlongzihan2 小时前
【LeetCode 204. 计数质数】从暴力枚举到埃拉托斯特尼筛法
算法·筛法
alphaTao2 小时前
LeetCode 每日一题 2026/9/21-2026/9/27
python·算法·leetcode
88号技师3 小时前
2026年9月SCI-天气状态蚂蚁优化算法Weather State Ants Optimizer-附Matlab免费代码
开发语言·算法·matlab·优化算法