目录
[<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>中所讲解的递归遍历知识铺垫,我们便可以实现递归建树了。此处为了能够完成一个完整的建树过程,故引入牛客网中的一道综合编程题进行讲解,如下。
解析
本题需要我们通过一串前序遍历字符串构建一棵二叉树,再对这棵二叉树进行中序遍历并输出。树的结构、中序遍历等内容在前文中已经讲解,故此处仅需完成建树。
我们需要解决两个问题:
- 如何在递归中遍历字符串?
- 如何通过递归创建树?
首先第一个问题,我们如何在递归中遍历字符串?简单的做法是在类中添加一个成员变量记录遍历位置,但是如果这样做,在第一次建树后成员变量无法复位,故此法只能建树一次;若不想手动重置成员变量,则我们可以将遍历下标包装成类,每次递归传递这个类的实例,如此即可实现多次建树,如下:
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题,为了避免内容过于冗杂,故分别发布。

