1. 树
(ps:写完这篇文章后我才发现,文章中有的地方写成了“节点”,有的地方写成了“结点”,在这里先给大家说明一下,本篇这两个名词都是指二叉树的 “Node”)
1.1 树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。树形结构中,子树之间不能有交集,否则就不是树形结构。
- 每个节点都有一个唯一的标识符(节点键值)。
- 如果节点A是节点B的子节点,则节点B是节点A的父节点,并且它们之间有一条有向边(B -> A)。
- 除了根节点,每个节点都有且只有一个父节点。
1.2 关于树的一些名词
-
结点的度: 一个结点含有子树的个数称为该结点的度; 如上图:
A的度为3,B的度为2。 -
树的度: 一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为
3。 -
叶子结点或终端结点: 度为
0的结点称为叶结点; 如上图:E、F、G、H、D节点为叶结点。 -
双亲结点或父结点: 若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:
A是B的父结点。 -
孩子结点或子结点: 一个结点含有的子树的根结点称为该结点的子结点; 如上图:
B是A的孩子结点。 -
根结点: 一棵树中,没有双亲结点的结点;如上图:
A是根节点。 -
结点的层次: 从根开始定义起,根为第
1层,根的子结点为第2层,以此类推。 -
树的高度或深度: 树中结点的最大层次; 如上图:树的高度为
3。 -
兄弟结点: 具有相同父结点的结点互称为兄弟结点; 如上图:
B、C是兄弟结点。 -
结点的祖先: 从根到该结点所经分支上的所有结点;如上图:
A是所有结点的祖先。 -
子孙: 以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是
A的子孙。
2. 二叉树
2.1 什么是二叉树?
一棵二叉树是结点的一个有限集合,它有以下的性质:
- 每个节点最多有两个子节点;
- 左子节点和右子节点在二叉树中的位置是固定的,左子节点位于其父节点的左侧,右子节点位于其父节点的右侧;
- 二叉树可以为空,即不含任何节点,也可以只有一个结点;
- 二叉树可以是斜的,即所有节点都只有左子节点或右子节点;
- 对于任何一个节点,其左子树和右子树都是二叉树,且它们之间是互不重叠的。
2.2 两种特殊的二叉树
1.满二叉树: 一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。如果一棵二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2.完全二叉树: 完全二叉树是一种特殊的二叉树,其中除了最后一层外,每一层的节点数都达到最大,并且最后一层的节点都靠左排列。也就是说,如果最后一层的节点没有填满,那么这些节点只能出现在最后一层的最右侧。
2.3 二叉树的性质(重要)
二叉树的性质是非常重要的,这为后面编程打下铺垫,二叉树有以下性质:
- 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有个结点。
- 若规定根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是
- 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2, 则有 。
针对完全二叉树:
- 具有
n个结点的完全二叉树的深度k为: 向上取整。 - 对于具有
n个结点的完全二叉树,如果按照从上至下、从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:- 若
i>0,父节点的序号:;i=0时,i为根结点编号,无父结点。 - 若 ,左孩子序号: ,否则无左孩子。
- 若 ,右孩子序号: ,否则无右孩子。
- 若
- 有一棵完全二叉树,当完全二叉树的节点数为奇数时,n1=0;为偶数时,n1=1;(
n1:结点的度为1的个数)
上面的性质一定要熟悉,我们来做做一些例题:
1.某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
解:我们知道 n0 = n2 + 1; n2 = 199、求n0,答案就是 n0 = 199 + 1 = 200;
- 一个具有767个节点的完全二叉树,其叶子节点个数为()
解:由题得:n0 + n1 + n2 = 767;
因为 767 为奇数,所以 n1 = 0;而n0 = n2 + 1,得出n2 = n0 - 1,带入上面的式子:
n0 + 0 + no - 1 = 767-->2 * n0 = 768-->n0 = 384。答案就是 384。
2.4 二叉树的存储
二叉树的存储方式一般有两种方式:顺序存储、链式存储。
1.顺序存储:顺序存储就是用数组来存储数据,一般是用来存储完全二叉树的,因为这样不浪费空间,在后面的堆这个数据结构就是用数组来存储的(后面会更新)。
2.链式存储:链式存储是指用链表的形式来存储二叉树,存储的结点有两种方式:
- 二叉链表:结点中有数据域、左孩子地址、右孩子地址。
-
三叉链表:结点中有数据域、左孩子地址、右孩子地址、双亲结点的地址。
本篇用的是二叉链式存储。
2.5 二叉树的基本操作
由于二叉树的创建需要用到二叉树的遍历等知识,所以这里就手动快速创建一棵简单的二叉树,等后面再来介绍二叉树的创建。
创建如下的二叉树:
public class MyBinaryTree {
private static class Node {
int val;
Node left;//左孩子(左子树)
Node right;//右孩子(右子树)
Node(int val) {
this.val = val;
}
}
public Node root;//头节点
//先手动创建一棵二叉树
public Node create() {
Node root = new Node(1);
root.left = new Node(2);
root.right = new Node(3);
root.left.left = new Node(4);
root.left.right = new Node(5);
root.right.left = new Node(6);
root.right.right = new Node(7);
return root;
}
}
2.5.1 二叉树的遍历
学习二叉树最重要的是学会如何遍历二叉树,二叉树的大部分操作都是需要递归实现的,当然包括二叉树的遍历。二叉树的遍历方式有:前序遍历、中序遍历、后序遍历、层序遍历。 这几种遍历方式是必须要掌握的,因为不管是去刷题、还是后面二叉树的基本操作都是基于遍历操作来完成的。
(注意,这四种遍历方法适用于所有二叉树,不止是完全二叉树,下面之所以举一个完全二叉树的例子是因为完全二叉树观赏性好😁)
(1)前序遍历: 二叉树的遍历是有一个大致模板的,这里先把代码给出,再来理解是怎么遍历二叉树的。
//通过二叉树的前序遍历来打印二叉树
public void preOrder(Node root){
//该结点为null,说明没有左子树或者没有右子树
if(root == null){
return;
}
//先打印(可以改为对该结点的其它操作)
System.out.print(root.val + " ");
//遍历左子树
preOrder(root.left);
//遍历右子树
preOrder(root.right);
}
前序遍历的“前”体现在先对当前结点进行操作,然后再遍历左子树,左子树遍历完后再来遍历右子树,下面是它的演示图,自己结合下面的动图再到草稿纸上画一画这递归是怎么走的。
可以看出,访问根结点--->根的左子树--->根的右子树。
class Main{
public static void main(String[] args) {
MyBinaryTree tree = new MyBinaryTree();
tree.root = tree.create();
tree.preOrder(tree.root);
}
}
结果:
1 2 4 5 3 6 7
(2)中序遍历: 根的左子树--->根节点--->根的右子树。先遍历完左子树,然后操作结点,最后遍历右子树。
public void inOrder(Node root){
//该结点为null,说明遍历完了
if(root == null){
return;
}
//遍历左子树
inOrder(root.left);
//打印(可以改为对该结点的其它操作)
System.out.print(root.val + " ");
//遍历右子树
inOrder(root.right);
}
中序遍历与前序遍历的区别就是操作结点的时机不一样而已。
class Main{
public static void main(String[] args) {
MyBinaryTree tree = new MyBinaryTree();
tree.root = tree.create();
tree.inOrder(tree.root);
}
}
结果:
4 2 5 1 6 3 7
(3)后序遍历: 根的左子树--->根的右子树--->根节点。先遍历完左右子树,最后操作结点。
//通过二叉树的后序遍历来打印二叉树
public void postOrder(Node root){
if(root == null){
return;
}
//遍历左子树
postOrder(root.left);
//遍历右子树
postOrder(root.right);
//打印(可以改为对该结点的其它操作)
System.out.print(root.val + " ");
}
class Main{
public static void main(String[] args) {
MyBinaryTree tree = new MyBinaryTree();
tree.root = tree.create();
tree.postOrder(tree.root);
}
}
结果:
4 5 2 6 7 3 1
(4)层序遍历: 层序遍历比较特殊,它是一层一层来遍历,层序遍历需要借助队列来实现,利用队列的先进先出的性质。
//通过二叉树的层序遍历来打印二叉树
public void levelOrder(Node root){
if(root == null){
return;
}
//借助队列来进行存储下一层的结点
Queue<Node> queue = new LinkedList<>();
queue.offer(root);//先入队
while(!queue.isEmpty()){
int size = queue.size();//当前层的结点个数
//遍历当前层,把每一个结点的左右结点放入队列,
//当这一层遍历完后,下一层已经全部放入队列了。
for (int i = 0; i < size; i++) {
//当前层依次出队
Node tmp = queue.poll();
//打印(可以改为对该结点的其它操作)
System.out.print(tmp.val + " ");
//依次将它们的左右结点放入队列,如果有左右结点就放入,没有就不放入。
//注意,必须是先放左再放右
if(tmp.left != null){
queue.offer(tmp.left);
}
if(tmp.right != null){
queue.offer(tmp.right);
}
}
//每一层换行(可有可无)
System.out.println();
}
}
class Main{
public static void main(String[] args) {
MyBinaryTree tree = new MyBinaryTree();
tree.root = tree.create();
tree.levelOrder(tree.root);
}
}
结果:
1
2 3
4 5 6 7
2.5.2 二叉树的创建
我们已经熟悉二叉树的遍历,接下来就来实现一下二叉树的创建。
二叉树的创建方式有很多种,这里介绍一种。
(1)通过连续输入来创建二叉树, 当输入完成后二叉树就创建成功。这里进行约定,当输入为-1时,表示为null。
//通过输入来创建二叉树
public static Node createBinaryTree(Scanner scanner) {
int val = scanner.nextInt();
if (val == -1) { // 输入-1表示当前节点为空
return null;
}
Node root = new Node(val);
root.left = createBinaryTree(scanner);
root.right = createBinaryTree(scanner);
return root;
}
class Main{
public static void main(String[] args) {
MyBinaryTree tree = new MyBinaryTree();
tree.root = MyBinaryTree.createBinaryTree(new Scanner(System.in));
tree.levelOrder(tree.root);
}
}
输入:1 2 -1 -1 3 -1 -1
结果:
上面程序是先序遍历二叉树的方式来构建二叉树的,但是这样构造出来的二叉树是一颗完全二叉树,对于非完全二叉树,是需要先知道先序遍历序列和中序遍历序列 或者 后序遍历序列和中序遍历序列 才能构建出来。(👉根据前序、中序遍历构造二叉树 and 根据中序、后序遍历构造二叉树 - 掘金 (juejin.cn))
2.5.3 二叉树的其它操作
(1)获取树中结点的个数
//获取树中结点的个数
public int size(Node root){
if(root == null){
return 0;
}
int number = 0;
number++;//当前有一个结点,需要知道它的左子树有几个结点、右子树有几个结点。
int leftNum = size(root.left);//左子树有几个结点
int rightNum = size(root.right);//右子树有几个结点
number += leftNum + rightNum;//总共有多少结点。
return number;
}
递归就是将大问题转化为相同小问题,这里的思路就是:我想获取树中的结点个数,那么就是当前结点+左子树的结点+右子树的结点,左子树的结点怎么求?同样是这个思路,依此类推。
(2)获取叶子结点的个数。
// 获取叶子结点的个数
public int getLeafNodeCount(Node root){
//如果根结点为null
if(root == null){
return 0;
}
//当前结点是叶子结点
if(root.left == null && root.right == null){
return 1;
}
//getLeafNodeCount(root.left)为左子树的叶子结点,getLeafNodeCount(root.right)为右子树结点。
return getLeafNodeCount(root.left) + getLeafNodeCount(root.right);
}
(3)获取第K层节点的个数。
还是大问题化成多个相同小问题,
当k = 1时表示当前层就“根节点”一个。
//获取第k层结点的个数
public int getKLevelNodeCount(Node root,int k){
if(root == null || k < 0){
return 0;
}
//判断是否为第k层
if(k == 1){
return 1;
}
//左子树的k - 1层的结点个数+右子树k - 1层的结点个数
return getKLevelNodeCount(root.left,k - 1) + getKLevelNodeCount(root.right,k - 1);
}
(4)获取二叉树的高度
二叉树的高度 = Max(左子树高度,右子树高度)+ 1,以此类推。
// 获取二叉树的高度
public int getHeight(Node root){
if(root == null){
return 0;
}
int lefHeight = getHeight(root.left);//左子树的高
int rightHeight = getHeight(root.right);//右子树的高
int tmp = Math.max(lefHeight,rightHeight) + 1;
return tmp;
}
(5)检测值为value的元素是否存在。
//检测值为value的元素是否存在
public Node find(Node root,int val){
if(root == null){
return null;
}
//判断根节点是不是待查元素,是就返回。
if(root.val == val){
return root;
}
//判断左子树有没有,没有就到右子树中找。
Node leftFind = find(root.left,val);
if(leftFind != null){
return leftFind;
}
//左子树没有找到,到右子树中找。
Node rightFind = find(root.right,val);
if(rightFind != null){
return rightFind;
}
//左右子树都没找到,并且根结点也不是,说明该二叉树中没有 val 元素。
return null;
}
(6)判断一棵树是不是完全二叉树。
完全二叉树的特征:结点是靠左的,根据这个性质我们可以借助队列。
维护一个队列,从根节点开始入队,出队的时候再依次将出队结点的左右结点入队,以此类推。
当走到最后,如果队列里面全是null,则该二叉树是完全二叉树,否则不是。
//判断一棵树是不是完全二叉树
public boolean comBinaryTree(Node root){
if(root == null){
return true;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
//每出队一个结点就把它的左右孩子结点入队。
Node cur = queue.poll();
if(cur != null){
queue.offer(cur.left);
queue.offer(cur.right);
} else{
break;//出队时第一个 null
}
}
//当出现第一个null时,需要判断队列里是否有非空结点。
while(!queue.isEmpty()){
Node cur = queue.poll();
if(cur != null){
return false;
}
}
//队列里全是 null 就返回 true 。
return true;
}
(如有错误,请在评论区指出,谢谢🌹)