二叉树
概念
一棵二叉树是结点的一个有限集合,该集合要么为空,要么是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
举个例子:
从图中我们可以看出:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
注意: 对于任意的二叉树都是由以下几种情况复合而成的:
两种特殊的二叉树
满二叉树
一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为K,且结点总数是2k-1 ,则它就是满二叉树。
举个例子:
从图中我们可以看到,每一层的结点都达到了最大值,因此这类树就叫做满二叉树,因此我们可以算出,当二叉树的有K层时,结点的个数为2k-1个。(等比数列求和公式)
完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
举个例子:
简单来说,就是从上至下,从左至右按顺序依次放入结点的二叉树,就叫做完全二叉树。
二叉树的性质
- 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2i-1 (i>0)个结点。(当第i层满时,结点数最多)
- 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 2k-1(k>=0)。(当二叉树为满二叉树时,取最大值)
- 对任何一棵二叉树, 如果其叶结点个数为 n 0 _0 0, 度为2的非叶结点个数为 n 2 _2 2,则有n 0 _0 0=n 2 _2 2+1。
- 具有n个结点的完全二叉树的深度k为log 2 _2 2(n+1)上取整。
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
1.假设孩子结点下标为i,那么双亲结点的下标是:(i-1) / 2。
2.假设父亲结点下标为i,左孩子:2i + 1,右孩子:2i + 2。
证明性质3
对任何一棵二叉树, 如果其叶结点个数为 n
0
_0
0, 度为2的非叶结点个数为 n
2
_2
2,则有n
0
_0
0=n
2
_2
2+1。
假设一棵树有N个结点,那么 n
0
_0
0(度为0的结点)、n
1
_1
1(度为1的结点)和n
2
_2
2(度为2的结点)之和为N。又因为有N个结点,那么这棵树有N-1条边(树的基本性质),那么n
0
_0
0能够向下引出0条边,n
1
_1
1能够向下引出1条边,n
2
_2
2能够向下引出2条边。由此得到两个方程:
N-1 = n 1 _1 1 + 2n 2 _2 2
N = n 0 _0 0 + n 1 _1 1 + n 2 _2 2
化简方程组可得:n 0 _0 0=n 2 _2 2+1。
证明性质4
具有n个结点的完全二叉树的深度k为log 2 _2 2(n+1)上取整。
由题意得:2k-1-1 < n ≤ 2k-1(当二叉树为满二叉树时取等号)。
化简可得:log 2 _2 2(n+1) ≤ k < log 2 _2 2(n+1) + 1
当二叉树为满二叉树时,取等号。
(ps:画个简单的二叉树更便于理解!)
证明性质5
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
1.假设孩子结点下标为i,那么双亲结点的下标是:(i-1) / 2。
2.假设父亲结点下标为i,左孩子:2i + 1,右孩子:2i + 2。
如图所示:
二叉树的存储
二叉树的存储结构分为:顺序存储和类似于链表的链式存储。
这篇博客中我们讲介绍链式存储。二叉树的链式存储是通过一个一个的节点引用起来的,常见的表示方式有二叉和三叉表示方式,具体如下:
孩子表示法:
// 孩子表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
孩子双亲表示法
// 孩子双亲表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
Node parent; // 当前节点的根节点
}
二叉树的基本操作
创建简单的二叉树
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于对二叉树结构掌握还不够深入,下面只是创建一棵简单的二叉树。等真正了解到二叉树,再学习二叉树真正的创建方式。
public class BTNode {
public char val;
public BTNode left;//左孩子的引用
public BTNode right;//右孩子的引用
public BTNode(char val) {
this.val = val;
}
}
既然是用链表存储实现二叉树,那么结点就必须创建,因此创建一个结点类,结点有三个成员变量val,left,right,构造方法是赋值给val相应的值。
我们以下面的二叉树为例,创建对应的二叉树。
public class BinaryTree {
public BTNode root;//二叉树的根结点
public BTNode createTree() {
BTNode A = new BTNode('A');
BTNode B = new BTNode('B');
BTNode C = new BTNode('C');
BTNode D = new BTNode('D');
BTNode E = new BTNode('E');
BTNode F = new BTNode('F');
BTNode G = new BTNode('G');
BTNode H = new BTNode('H');
root = A;
A.left = B;
A.right = C;
B.left = D;
B.right = E;
C.left = F;
C.right = G;
E.right = H;
return root;
}
}
首先二叉树类有一个成员变量是根节点,有一个createTree()方法,方法中创建了9个结点,分别将这9个结点的对应关系按照图中的对应关系表示就可以完成了二叉树的创建了。这就是一个非常简单的二叉树。
注意: 上述代码并不是创建二叉树的方式,只是简单的示范。
二叉树的遍历
学习二叉树结构,最简单的方式就是遍历。所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如:打印节点内容、节点内容加1)。 遍历是二叉树上最重要的操作之一,是二叉树上进行其它运算之基础。
在遍历二叉树时,如果没有进行某种约定,每个人都按照自己的方式遍历,得出的结果就比较混乱,如果按照某种规则进行约定,则每个人对于同一棵树的遍历结果肯定是相同的。如果N代表根节点,L代表根节点的左子树,R代表根节点的右子树,则根据遍历根节点的先后次序有以下遍历方式:
- NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点—>根的左子树—>根的右子树。
- LNR:中序遍历(Inorder Traversal)——根的左子树—>根节点—>根的右子树。
- LRN:后序遍历(Postorder Traversal)——根的左子树—>根的右子树—>根节点。
前序遍历
如图:
如果我们按照先序遍历来遍历这个二叉树,首先进入根结点A,打印A,到A左子树B,B同样也是A左子树的根结点,打印B,到B的左子树D,D同样也是B左子树的根结点,打印D,D左子树为null,到D右子树,D右子树为null,返回到B,然后到B的右子树E,E同样也是B的右子树的根结点,打印E,到E的左子树,E左子树为null,到E的右子树H,H同样也是E的右子树的根结点,打印H,返回到A,然后到的右子树C,C同样也是A右子树的根结点,打印C,到C的左子树F,F同样也是C的左子树的根节点,打印F,F左右子树为null,返回到C,然后到C的右子树G,G同样也是C右子树的根结点,打印G,G左右子树为null,返回到A,完成遍历。因此前序遍历完打印结果为:A B D E H C F G。从过程来看我们可以发现遍历的过程一直在重复做:根->左->右遍历的事情,只是根结点发生了变化。因此我们很容易想到用递归的方法实现前序遍历。
代码实现
//前序遍历
void preOrder(BTNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
}
从过程来看很是繁琐,但是从代码看非常的简单,是的,就是这么简单的前序遍历代码,简单的分析,从根节点出发遍历,进入preOrder()方法,如果根为null,则返回,否则打印root的值,然后遍历根的左子树,进入左子树也是如此,直到左子树遍历完成,到达右子树,右子树遍历完成,整个前序遍历方法完成。
结果:
中序遍历
如图:
中序遍历根前序遍历区别就在于中序遍历是按照左子树,根,右子树的遍历方式遍历的,过程不再复述,因此遍历的结果为:D B E H A F C G。
代码实现
//中序遍历
void inOrder(BTNode root) {
if (root == null) {
return;
}
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
结果:
后序遍历
如图:
同样的,后序遍历是按照:左子树,右子树,根的顺序遍历的,因此遍历结果为:D H E B F G C A
代码实现
//后序遍历
void postOrder(BTNode root){
if (root == null){
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.print(root.val+" ");
}
结果:
层序遍历
如图:
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。因此图中的遍历的结果为:A、B、C、D、E、F、G、H。
关于二叉树的基础知识就到这里,也希望这篇博客能够给你相应的帮助,如果有疑问和建议欢迎私信和评论,谢谢各位!