1. 二叉树的定义:
二叉树是由n(n>=0)个节点(数据元素)组成的有限集合,递归定义如下:
当n = 0 时,则为空二叉树
当n > 0 时,二叉树由3部分组成:根节点、左子树和右子树,子树本身也是二叉树
除根节点以外,其余节点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树
*一般情况下我们会将森林转为二叉树,因为二叉树结构比较简单,因此方便处理问题
2、二叉树的术语
(1)父母、孩子与兄弟节点
在一棵二叉树中,一个结点的子树的根结点称为孩子结点,相对地,该结点也是该孩子结点的父母结点,拥有同一个父母结点的多个结点是兄弟节点,祖先节点是指其父母结点以及父母的父母的结点直到根结点,这些结点都是其祖先节点
(2) 节点层次和二叉树的高度
结点的层次反映结点处于二叉树的层次位置,根节点层次为1,其他结点的层次为父母结点的层次+1
高度是二叉树中结点的最大层次数
(3)节点的度
度为结点的子树棵树,度为0的结点称为叶子,除了叶子结点之外的其他结点被称为分支结点
(4)边、路径、直径
设x是y的父母结点,则两个结点的连线为边,按此逻辑多个结点的连线既为路径
二叉树的直径是从根到叶子结点的最长路径,直径的路径长度 = 二叉树的高度-1
3.二叉树的性质
二叉树的性质阐述了二叉树的逻辑特点,通过这些特点我们可以推断出二叉树中结点的父子关系,以及二叉树的层次,高度等属性,这是
设计二叉树的重要参数
(1)性质1:若根节点的层次为1,则二叉树第i层最多有2^(i-1)(i>=1)个节点
证明(数学归纳法):
假设第i-1层的结点数为2^(i-2)个结点,由于二叉树中每个结点的度最多为2,则第i层的结点数为2^(i-1),命题成立
(2)性质2:在高度为h的二叉树中,最多有2^h-1个结点
证明:等比数列求和,首项为1,公比为2,项数为h
(3)性质3:设一棵二叉树的叶子结点数为n0,二度结点为n2,则n0 = n2+1
证明:设二叉树结点数为n,1度的结点数为n1,则有*n = n1+n0+n2
由于根没有结点,所以 *n = n0*0+n2*2+n1*1
结合二式可得原式
【满二叉树】:一棵高度为h的满二叉树是具有2^h-1(h>=0)个结点的二叉树。满二叉树每层的结点都达到了最大值
【完全二叉树】:一棵具有n个结点高度为h的二叉树,如果它的每个结点都与高度为h的满二叉树中序号为0-n-1的结点一一对应,则被称为满二叉树。换句话说,完全二叉树是满二叉不满的情况,满二叉是完全二叉,而完全二叉是不满的。
(4)性质4:一棵具有n个结点的完全二叉树,其高度h = (log2n)+1
证明:对一棵结点为n,高度为h的完全二叉树,(2^h-1)-1<n<=(2^h)-1
移项可知,h-1<log2(n+1)<=h,log2(n+1)<=h<log2(n+1)+1--->h必然为log2n取整+1
(5)性质5:一棵具有n个结点的完全二叉树,对序号i(0<=i<n)的结点,有:
*若i=0,则i为根结点,无父母结点;若i>0,则i的父母结点为(i-1)/2
*若2*i+1<n,则i的左孩子结点序号为2*i+1,否则无左孩子
*若2*i+2<n,则i的右孩子结点序号为2*i+2,否则无右孩子
4.二叉树的顺序存储结构:
(1)一般来说,二叉树主要采用链式存储结构,顺序存储结构一般适用于完全二叉树和满二叉树,因为如果用顺序存储来存储普通不规则树可能会造成大量的空间浪费,因此非完全二叉树一般用链式存储
(2)将一颗完全二叉树的所有结点按结点序号进行顺序存储,根据二叉树的性质5,由结点i可知其父母结点、左孩子、右孩子结点的序号,由于具有n个结点的完全二叉树只有一种形态,一颗完全二叉树与其结点序号是一对一的映射,二叉树的性质5,将完全二叉树的结点序号所表达的线性关系映射到树结构的层次关系,唯一确定一颗完全二叉树因此,完全二叉树能够采用属性内需存储结构存储,依靠数据元素相邻的位置关系反映数据间的逻辑结构由于顺序存储结构并没有存储元素间的关系,所以非完全二叉树是不能用顺序结构来存储的。
(3)这里介绍一种常用的顺序存储完全二叉树的方式,定义L,R两个数组,我们这里假设根结点为0其左孩子为1,右孩子为2那么我们可以通过给L,R两个数组赋值,L[0]=1,R[0]=2,这样就定义了0结点的左右两个孩子的下标,但是当该结点如果没有子结点那么就赋值为-1,并且要指明根结点。
5.二叉树的链式存储结构:
(1)二叉链表:
二叉树的二叉链表存储结构,除了数据域,采用两个地址分别指向左、右孩子的结点
并采用root指向二叉树的根结点,采用二叉链表存储二叉树,每个结点只存储了到其孩
子结点的单向关系,没有到其父母结点的关系,如果需要仅用O(1)的时间复杂度查父结 点,就要使用三叉链表了
(2)三叉链表:
*二叉树的三叉链表存储结构是在二叉链表结点的基础上,增加一个地址域parent指向其父节点,这样存储了父母结点与孩子结点的双向关系
*也可以采用一个结点数组存储二叉树的所有结点,称为静态二叉树/三叉链表,每个结点存储其(父母)左、右孩子结点的下标,通过下标来表示结点间的关系,-1代表没有该结点
6.二叉树的二叉链表实现
1、二叉链表结点类
public class BinaryNode<T>{
public T data;//数据域,存储数据元素的数据
public BinaryNode<T> left,right;//地址域,分别指向左右结点
public BinaryNode(T data, BinaryNode<T> left,BinaryNode<T> right) {//构造结点
}
public BinaryNode(T data) {//构造元素为data的叶子结点
};
public boolean isLeaf() {//判断是否为叶子结点
return false;
};
}
2、二叉树类
public class BinaryTree <T>{//二叉树类,T指定
public BinaryTree<T> root; //定义根结点,二叉链表结点结构
public BinaryTree() {//定义空二叉树
this.root = null;
}
public boolean isEmpty() {//判断是否为空二叉树
return this.root==null;
}
}
3、二叉树插入结点
(1)插入结点X作为根结点,原根结点作为x的左孩子
(2)插入一个结点(X或Y)作为指定结点p的左或右孩子,原结点的左或右孩子将作为插入结点的左或右孩子,既该变结点的left或right域
public void insert(T x) {//插入x元素作为根结点,x != null,且原根结点作为其左孩子
if(x != null) {
this.root = new BinaryNode<T>(x,this.root,null);
}
}
public BinaryNode<T> insert(BinaryNode<T>p,boolean left,T x){
//插入一个x结点作为p的左/右孩子结点,left指定左/右孩子,取值true(左)/false(右)
//p原结点的左右孩子成为x的左右孩子;插入成功返回x结点,失败返回null
if(x == null || p== null) {
return null;
}
if(left) {
return p.left = new BinaryNode<T>(x,p.left,null);
}
return p.right = new BinaryNode<T>(x,null,p.right);
}
4、二叉树删除子树
在二叉树中删除一个结点,不仅要修改其父母结点的left和right,还要约定如何调整子树结构的规则,既删除一个结点,原先以该结点为
根的子树则变成由原子树和右子树组成的森林,这里无法约定其规则,故直接删除结点以及其对应的子树
//删除p结点的左/右子树
public void remove(BinaryNode<T>p,boolean left){
if(p!=null) {
if(left) {
p.left = null;
}else {
p.right = null;
}
}
}
public void clear() {//清空树
this.root = null;
}
5、遍历二叉树
遍历二叉树是指,按照一定规则和次序访问二叉树中的所有结点,并且每个结点仅被访问一次。二叉树的遍历规则有孩子优先次序和兄弟优先次序。
(1)孩子优先次序的遍历规则:
由于先遍历左子树或先遍历右子树在算法设计上没有本质区别,因此,约定遍历子树的次序为,先左子树,后右子树
二叉树孩子优先遍历有三种:
①先根遍历:访问根结点,遍历左子树,遍历右子树
②中跟次序:遍历左子树,访问根结点,遍历右子树
③后根次序:遍历左子树,遍历右子树,访问根结点
三者差距也就是访问结点的时机不同
(2)先根次序遍历二叉树:
由于先根次序遍历规则是递归,因此采用递归算法实现的递归方法必须带有参数,通过不同的实际参数区别递归调用执行中的多个方法
public void preorder() {//先根次序遍历二叉树
preorder(this.root);//以root为根开始遍历树
System.out.println();
}
public void preorder(BinaryNode<T> p) {//方法重载,以p结点为根开始遍历
if(p!=null) {//递归出口,当p==null遍历到叶,回溯,根左右原则
System.out.print(p.data.toString()+")");//访问该结点数据
preorder(p.left);//先遍历左子树
preorder(p.right);
}
}
一颗二叉树由多颗子树组成,一个结点也是一颗子树的根。二叉树基于遍历的递归方法,必须以某个结点p为参数,表示以p为根的子树
以下为preorder算法说明:
①结点变量p从root根结点开始执行,表示遍历一颗二叉树
②当p指向某个结点时,按先根次序访问结点元素后,再分别遍历其左子树,右子树。由于遍历子树的规则相同,只是子树的根结点不同,因此递归调用当前preorder()方法,参数分别是结点p的左孩子和右孩子
(3)中跟次序遍历二叉树:
public void inorder() {//中根遍历 规则:左 中 右
inorder(this.root);//以根结点为起始遍历
System.out.println();
}
public void inorder(BinaryNode<T> p) {//递归出口,当p==null遍历到叶,回溯,根左右原则
if(p!=null) {
inorder(p.left);//先遍历左子树,递归往下
System.out.print(p.data.toString()+")");//访问根结点数据
inorder(p.right);//后遍历右子树
}
}
(4)后跟次序遍历二叉树:
public void postorder() {
postorder(this.root);
System.out.println();
}
public void postorder(BinaryNode<T> p) {//递归出口,当p==null遍历到叶,回溯,根左右原则
if(p!=null) {
postorder(p.left);//先遍历左子树,递归往下
postorder(p.right);//后遍历右子树
System.out.print(p.data.toString()+")");//访问根结点数据
}
}
6.构造二叉树
二叉树必须明确两种关系:
①结点与其父母结点及孩子结点之间的层次关系
②兄弟结点间的左右次序关系
(1)由二叉树的一种遍历不能唯一确定一颗二叉树
先根/后根遍历只反映了父母与孩子之间的关系,没有体现出兄弟结点之间的关系
先根遍历该树都是AB
(2)由先根和中根两种遍历可以唯一确定一颗二叉树
先根/后根能确定孩子和父母的关系,中根遍历能确定兄弟间关系因此两结合可以
确定一颗二叉树
(3)标明空子树的先根遍历序列可以唯一确定一颗二叉树
先根/后根都只反映了父母与孩子的关系,那么怎样才能在这个基础上加上兄弟间
的关系呢?我们可以在先根遍历序列中添加空子树("^"),通过空子树位置反映兄
弟结点之间的关系,这样就可以只通过先根/后根遍历确定一颗唯一的二叉树。
设prelist表示一颗二叉树标明空子树的先根遍历序列,构造二叉树的递归算法:
① prelist[0]一定是二叉树的根,prelist[1]是二叉树的第一颗左子树的根
②若prelist[i]是空子树,则返回上一个结点;否则开辟新结点,该结点的左孩
子是prelist[i+1],但父母与孩子结点没有建立联系
③返回到当前结点时,下个元素prelist[i+1]是prelist[i]结点的左/右结点,也
就是回溯时建立父母与孩子的联系,若一个结点的左右子树都建立完成了
则返回上一层结点
④重复执行②~③直到返回根结点,则二叉树建立完成,并使root指向根结点
//以从i开始的标明空子树的先根序列,创建一颗以prelist[i]为根的子树,返回子树的根结点,并建立该结点与上一层结点的父子关系
//递归算法先创建根节点,在创建左子树,右子树
public int i = 0;//定义一个全局变量记录结点在序列中的序号
public BinaryNode<T> create(T[] prelist){
BinaryNode<T> p = null;//初始化根结点
if(i<prelist.length) {//i不能超过序列中最后一个结点的下标
T elem = prelist[i++];//拿到prelist[i]后i自增,跳到下个序号结点
if(elem != null) {//如果是空子树就不递归,反之递归往下搜索
p = new BinaryNode<T>(elem);//创建新的结点作为该层的根结点
p.left = create(prelist);//递归找该结点的左子树
p.right = create(prelist);//递归找该结点的右子树
}
}
return p;//返回该层的根结点,作为上一层结点的左/右子树
}
构造二叉树的递归过程是,p按层次变化,从根开始逐层深入,从父母结点调用到孩子结点返回,i为全局变量,拿到一个结点后自增,因此不能作为局部变量,相当于prelist[i]。
另外我们可以通过标明空子树先根/后根序列来唯一确定一颗二叉树,但不适用于中根遍历,因为标明空子树只是确定了兄弟间的关系,而没有找到父母与孩子间的关系,因此标明空子树的中根遍历并不能唯一确定一颗二叉树
7.二叉树的层次遍历
按层次次序遍历二叉树,其实就是兄弟优先次序遍历,遍历次序是按兄弟关系从左向右遍历,两个兄弟结点的访问次序是从左向右,连续访问。算法描述:设置一个空队列,顺序循环队列或链式队列都可,第一步根节点入队,第二步当队列不为空的时候,使得p指向一个出队结点,访问结点p,将p结点的左、右孩子入队。
public void levelorder() {
if(this.root == null) {
return;
}
//创建空队列,队列里面放结点
Queue<BinaryNode<T>> q = new LinkedTransferQueue<BinaryNode<T>>();
q.add(this.root);
while(!q.isEmpty()) {
BinaryNode<T> p = q.poll();//出队后指向出队元素并遍历该元素
System.out.print(p.data+" ");
if(p.left != null) {//如果左子树不为空则遍历左子树
q.add(p.left);
}
if(p.right != null) {
q.add(p.right);
}
}
System.out.println();
}