一、二叉树的定义
二叉树是指树的度数为2的有序树。它是一种最重要的树结构。二叉树的递归定义为:二叉树或者是一棵空树,或者是一棵由一个根节点和两棵互不相交的分别被称做根的左子树和右子树所组成的非空树,左子树和右子树又同样都是一棵二叉树。
在一棵二叉树中,每个结点的左子树的根结点被称为该结点的左孩子,右子树的根结点被称为该结点的右孩子。
二、二叉树的性质
二叉树具有同普通树类似的性质
1、二叉树上终端结点数等于双分支结点数加1。
2、二叉树上第i层上至多有2^(i-1)个结点(i>=1)。
3、深度为h的二叉树至多有2^h-1个结点。
4、具有n个结点的完全二叉树的深度为log2(n+1)向上取整或log2n向下取整+1.
在一棵二叉树中,若除最后一层外,其余层都是满的,而最后一层上的结点可以任意分布,则称此树为理想平衡二叉树,简称理想平衡树或理想二叉树。显然,理想平衡树包含了满二叉树和完全二叉树。是一棵理想平衡树,但它不是完全二叉树。
三、二叉树的抽象数据类型
二叉树的抽象数据类型同样包括了数据和操作部分。
public interface BinaryTree {
//由mode数组提供遍历二叉树的4种不同的访问次序
final String []mode={"preOrder","inOder","postOrder","levelOrder"};
//根据二叉树的广义表表示在计算机中建立对应的二叉树
boolean createBTree(String gt);
//判断一棵二叉树是否为空,若是则返回true,否则返回false
boolean isEmpty();
//按照字符串s所给定的次序遍历一棵二叉树,每个结点均被访问一次
void traverseBtree(String s);
//从二叉树中查找值为obj的结点,若存在则返回完整值否则返回空值
Object findBTree(Object obj);
//求出一棵二叉树的深度
int depthBTree();
//求出一棵二叉树的结点数
int countBTree();
//按照树的一种表示方法输出一棵二叉树
void printBTree();
//清除二叉树中的所有结点,使之变为一棵空树
void clearBTree();
}
四、二叉树的存储结构
同线性表一样,二叉树也有顺序存储和链接存储两种存储结构。
1、顺序存储结构
顺序存储一棵二叉树时,首先对该树中每个结点进行编号,然后以各结点的编号为下标,把各结点的值对应存储到一维数组中。
在二叉树的顺序存储结构中,各结点之间的关系是通过下标计算出来的,因此访问每一个结点的双亲和左、右孩子(若有的话)都非常方便。例如:对于编号为i的结点(即下标为i的元素),其双亲结点的下标为2/i向下取整。若存在左孩子,则左孩子结点的下标为2i;若存在右孩子,则右孩子结点的下标为2i+1。
二叉树的顺序存储结构对于存储完全二叉树是合适的,它能够充分利用存储空间,但对于一般二叉树,特别是对于那些单支点较多的二叉树来说是很不适合的,因为可能只有少数存储位置被利用,而多数或绝大多数的存储位置空闲着。因此,对于一般二叉树通常采用链接存储结构。
2、链接存储结构
在二叉树的存储中,通常采用的方法是:在每个 结点中设置3个域:值域、左指针域和右指针域,其结点结构为:
left | element | right |
链接存储的另一种方法是:在上面的结点结构中再增加一个parent指针域,用来指向其双亲结点。这种存储结构既便于从树根向下查找孩子结点,也便于向上查找双亲结点,当然也带来存储空间的相应增加。
同单链表相同,二叉树的二叉链表既可由独立分配的结点链接而成,也可由数组中元素结点链接而成。
若采用独立结点,假定类型名用BTreeNode表示,则二叉树中的结点类型可定义为:
public class BTreeNode {
Object element;
BTreeNode left,right;
public BTreeNode(Object obj)
{
element=obj;
left=right=null;
}
public BTreeNode(Object element, BTreeNode left, BTreeNode right)
{
this.element = element;
this.left = left;
this.right = right;
}
}
若采用数组元素结点,则结点类型可定义为:
public class ArrayBTreeNode {
Object element;
int left,right;
public ArrayBTreeNode(Object obj)
{
element=obj;
left=right=0;
}
public ArrayBTreeNode(Object element, int left, int right) {
this.element = element;
this.left = left;
this.right = right;
}
}
在数组元素结点中,left和right域分别存储左孩子、右孩子结点所在数组元素的下标,所以被定义为整型,当没有孩子链接时,相应的指针域的值为0,亦可用-1表示空指针。
在数组中建立二叉树的好处是:建好后可以把整个数组写入到一个文件中保存起来,当需要时再从文件中整体读入到内存数组进行处理。
3、链接存储的二叉树类
假定在二叉树的链接存储中所采用的结点类型为上面定义的BTreeNode,链接存储类的类名用标识符LinkBinaryTree表示,该类要实现二叉树接口类BinaryTree中定义的所有操作,同时还允许定义本类所需要的操作,该类的数据部分就是定义一个二叉树的树根指针,假定用标识符root表示,它为具有结点类型BTreeNode的私有或保护成员。
因为二叉树是一种递归数据结构,所以对它进行的运算绝大多数是通过递归算法实现的,递归算法通常都需要带有参数,每次递归调用时都要修改参数值,使得递归次数逐渐减少,最后达到不用继续递归而直接解决最小化问题,人后再依次返回逐渐消除(结束)递归调用过程,最后消除所有递归,执行完整个递归算法,返回到开始进行的非递归调用的位置继续向下执行。
对于二叉树的递归算法,其参数就是结点类型BTreeNode的指针(引用),开始非递归调用它时,传送给参数的值就是一棵二叉树的树根指针。当在递归函数体中进行递归调用时,传送给参数的实际值是当前指针所引用的树根结点的左孩子指针或右孩子指针,当传送的指针为空值null时,递归结束并返回。
在链接存储二叉树类的定义中,通常把每个递归函数定义为私有的,因为它只需要为对应的接口方法所专门调用。另外,为了记忆和使用方便,对作为接口的方法和相对应的递归方法采用相同的名称,因为他们虽然方法名相同,但参数表不同,所以是不同的方法系统能够辨认出来,不会产生二义性。
二叉树的遍历是二叉树中最重要的运算。二叉树的遍历是指按照一定次序访问树中所有结点,并且每个结点的值仅被访问一次的过程。根据二叉树的递归定义,一棵非空二叉树由根节点、左子树和右子树所组成,因此,遍历一棵非空二叉树的问题可分解为3个子问题:访问根结点、遍历左子树和遍历右子树。分别用D、L和R表示上述3个子问题,则有DLR、LDR、LRD、DRL、RDL、RLD这6种次序的遍历方案。
在遍历方案DLR中,因为访问根节点的操作在遍历左,右子树之前,故称之为前序、先序或先根遍历。类似地,在LDR方案中,访问根结点的操作在遍历左子树之后和遍历右子树之前,故称之为中序或中根遍历;在LRD方案中,访问根结点的操作在遍历左、右子树之后,故称之为后序或后根遍历。
3.1 先序遍历
//先序遍历的递归算法
private void preOrder(BTreeNode rt)
{
if(rt!=null)
{
System.out.print(rt.element+" "); //先访问根结点
preOrder(rt.left); //先序遍历左子树
preOrder(rt.right); //先序遍历右子树
}
}
3.2 中序遍历
//中序遍历的递归算法
private void inOrder(BTreeNode rt)
{
if(rt!=null)
{
inOrder(rt.left); //中序遍历左子树
System.out.print(rt.element+" "); //访问根结点
inOrder(rt.right); //中序遍历右子树
}
}
3.3 后序遍历
//后序遍历的递归算法
private void postOrder(BTreeNode rt)
{
if(rt!=null)
{
postOrder(rt.left); //后序遍历左子树
postOrder(rt.right); //后序遍历右子树
System.out.print(rt.element+" "); //访问根结点
}
}
3.4 层次遍历
层次遍历的算法需要使用一个队列,开始时把整个树的根结点引入队,然后每从队列中删除一个结点引用并输出该结点的值时,都应把它非空的左、右孩子结点的引用(指针)入队,这样当队列空时算法结束。
此算法为一个非递归算法,具体描述如下:
//层次遍历的非递归算法
private void levelOrder(BTreeNode rt)
{
Queue que=new SequenceQueue(); //定义并创建一个空队列
BTreeNode p=null; //定义结点引用p
que.enter(rt); //首先将树根指针入队
while(!que.isEmpty()) //当队列非空时循环执行
{
p=(BTreeNode)que.leave(); //删除队首元素赋给p
System.out.print(p.element+" "); //输出p所指向结点的值
if(p.left!=null)
{
que.enter(p.left); //左孩子结点指针进队
}
if(p.right!=null)
{
que.enter(p.right); //右孩子结点指针进队
}
}
}
此算法需要访问二叉树中的所有结点,访问每个结点时,都要经过进队和出队以及输出结点值的操作,这些操作的时间复杂度均为O(1),所以整个算法的时间复杂度为O(n),n表示二叉树中结点的个数。
3.5 建立二叉树
二叉树的表示方式不同,在计算机中建立二叉树的算法也不同,假定采用广义表表示。二叉树广义表表示的规定如下:
(1)每棵树的根节点作为由子树构成的表的名字而放在表的前面。
(2)每个结点的左子树和右子树用逗号分开,若只有右子树而没有左子树,则逗号不能省略。
根据二叉树的广义表表示建立二叉树链接存储结构的运算过程是:从保存二叉树广义表的字符串gt中输入每个字符,若遇到的是空格字符则不进行任何操作;若遇到的是字母(假定以字母作为结点的值),则表明是结点的值,应为它建立一个新结点,并把该结点(若它不是整个树的根节点的话)作为左孩子(若K=1)或右孩子(若k=2)链接到其双亲结点上;若遇到的是左括号,则表明子表开始,应首先使它前面字母所在结点的指针(即根结点指针)进栈,以便括号内的孩子结点向双亲结点链接只用,然后把k置为1,因为左括号后面紧跟的字母(若有的话)必为根节点的左孩子;若遇到的是右括号,则表明子表结束,应退栈;若遇到的是逗号,则表明以左孩子为根的子树处理完毕,应接着处理以右孩子为根的子树,所以要把k置为2,如此处理每一个字符,直到处理完所有字符为止。
建立二叉树的算法描述如下:
//根据二叉树的广义表表示在计算机中建立对应的二叉树
public boolean createBTree(String str)
{
Stack sck=new SequenceStack(); //定义和创建一个保存结点指针的栈
root=null; //把树根指针置为空,即从空树开始
BTreeNode p=null; //定义p为指向二叉树结点的指针
int k=1; //用k作为处理结点的左子树和右子树的标记
char []a=str.toCharArray(); //将字符串内容转换为字符数组a的内容
for(int i=0;i<a.length;i++) //依次处理二叉树广义表字符串中的每个字符
{
switch(a[i])
{
case ' ': //对空格不作任何处理
break;
case '(': //处理左括号
sck.push(p);
k=1;
break;
case ')':
if(sck.isEmpty())
{
System.out.println("二叉树广义表字符串错,返回假!");
return false;
}
sck.pop();
break;
case ',': //处理逗号
k=2;
break;
default: //扫描到的为字母,其他非法字符
if(a[i]>='a' && a[i]<='z' || (a[i] >='A' && a[i]<='Z'))
{
String aa = String.valueOf(a[i]);
p=new BTreeNode(aa);
if(root==null)
{
root=p; //p结点为树根
}
else
{
System.out.println("二叉树广义表中存在非法字符,返回假!");
return false;
}
}
}
}//for循环结束
if(sck.isEmpty())
{
return true;
}
else
{
return false;
}
}
该算法的时间复杂度为O(n),n表示二叉树广义表中字符的个数,由于平均每两至三个字符之中就有一个元素字符,所以n也可以看做二叉树中元素结点的个数。
//判断一棵二叉树是否为空,若是则返回true,否则返回false
public boolean isEmpty() {
return root==null;
}
3.7 求二叉树深度
若一棵二叉树为空,则它的深度为0,否则它的深度等于左子树和右子树中的最大深度加1,设dep1为左子树的深度,dep2为右子树的深度,则二叉树的深度为:
max( dep1,dep2 ) + 1
其中,max函数表示取参数中的大者。
求二叉树深度的递归算法如下:
//求出一棵二叉树的深度
public int depthBTree(BTreeNode rt) {
if(rt==null)
{
return 0; //对于空树,返回0并结束递归
}
else
{
int dep1=depthBTree(rt.left); //计算左子树深度
int dep2=depthBTree(rt.right); //计算右子树深度
if(dep1>dep2)
{
return dep1+1;
}
else
{
return dep2+2;
}
}
}
3.8 求二叉树中的结点数
若一棵二叉树为空,则结点数为0,否则它等于左子树中的结点数与右子树中的结点数之和再加1,1表示根结点,显然这是一个递归算法。此算法描述如下:
//求出一棵二叉树的结点数
public int countBTree(BTreeNode rt) {
if(rt==null)
{
return 0;
}
else
{
return countBTree(rt.left)+countBTree(rt.right)+1;
}
}
3.9 从二叉树中查找值为x的结点
从二叉树中查找值为x的结点,若存在则返回该结点的完整值,否则返回空值。该算法类似于先序遍历,若树为空则返回null结束递归,若树根结点的值就等于x的值,则返回该结点的值,否则先向左子树查找,若查找成功则返回结点值,否则再向右子树查找,若查找成功则返回结点值,若左右子树均为找到则返回null,表明查找失败。具体算法描述为:
//从二叉树中查找值为obj的结点,若存在则返回完整值否则返回空值
public Object findBTree(BTreeNode rt,Object x) {
if(rt==null)
{
return null; //查找失败返回空值
}
else
{
if(rt.element.equals(x))
{
return rt.element; //树根结点的值等于x则返回该结点的值
}
else
{
Object y;
y=findBTree(rt.left,x); //向左子树查找,若成功则返回结点值
if(y!=null)
{
return y;
}
y=findBTree(rt.right,x); //向右子树查找,若成功则返回节点值
if(y!=null)
{
return y;
}
return null;
}
}
}
3.10 输出二叉树
输出二叉树就是根据二叉树的链接存储结构以某种树的形式打印出来,假定采用广义表的形式打印。我们知道用广义表表示一棵二叉树的规则是:根结点被放在由左、右子树组成的表的前面,而表是用一对圆括号括起来的。
因此,广义表的形式输出一棵二叉树时,应首先输出根结点,然后再依次输出它的左子树和右子树,不过在输出左子树之前要打印出左括号,在输出右子树之后要打印出右括号;另外,依次输出的左右子树要至少有一个不为空,若均为空就没有输出的必要了。
由以上分析可知,输出二叉树的算法可在先序遍历算法的基础上作适当修改后得到,具体描述为:
//按照树的一种表示方法输出一棵二叉树
public void printBTree(BTreeNode rt) {
if(rt!=null) //树为空时结束递归,否则执行如下操作
{
System.out.print(rt.element); //输出根结点的值
if(rt.left!=null ||rt.right!=null)
{
System.out.print('('); //输出左括号
printBTree(rt.left); //输出左子树
if(rt.right!=null)
{
System.out.print(',');
}
printBTree(rt.right); //输出右子树
System.out.print(')'); //输出右括号
}
}
}
3.11 清除二叉树,使之变为一棵空树
//清除二叉树中的所有结点,使之变为一棵空树
public void clearBTree() {
root=null;
}
所有方法的具体实现类如下:
//链接二叉树类的定义
public class LinkBinaryTree implements BinaryTree {
protected BTreeNode root; //定义可继承的二叉树的树根指针(引用)
//无参构造函数的定义
public LinkBinaryTree() {
root=null; //二叉树的初始为空,即把空值null赋给root
}
//带参构造方法的定义
public LinkBinaryTree(BTreeNode root) {
this.root = root; //把一棵二叉树的树根引用赋给root
}
//先序遍历的递归算法
private void preOrder(BTreeNode rt)
{
if(rt!=null)
{
System.out.print(rt.element+" "); //先访问根结点
preOrder(rt.left); //先序遍历左子树
preOrder(rt.right); //先序遍历右子树
}
}
//中序遍历的递归算法
private void inOrder(BTreeNode rt)
{
if(rt!=null)
{
inOrder(rt.left); //中序遍历左子树
System.out.print(rt.element+" "); //访问根结点
inOrder(rt.right); //中序遍历右子树
}
}
//后序遍历的递归算法
private void postOrder(BTreeNode rt)
{
if(rt!=null)
{
postOrder(rt.left); //后序遍历左子树
postOrder(rt.right); //后序遍历右子树
System.out.print(rt.element+" "); //访问根结点
}
}
//层次遍历的非递归算法
private void levelOrder(BTreeNode rt)
{
Queue que=new SequenceQueue(); //定义并创建一个空队列
BTreeNode p=null; //定义结点引用p
que.enter(rt); //首先将树根指针入队
while(!que.isEmpty()) //当队列非空时循环执行
{
p=(BTreeNode)que.leave(); //删除队首元素赋给p
System.out.print(p.element+" "); //输出p所指向结点的值
if(p.left!=null)
{
que.enter(p.left); //左孩子结点指针进队
}
if(p.right!=null)
{
que.enter(p.right); //右孩子结点指针进队
}
}
}
//根据二叉树的广义表表示在计算机中建立对应的二叉树
public boolean createBTree(String str)
{
Stack sck=new SequenceStack(); //定义和创建一个保存结点指针的栈
root=null; //把树根指针置为空,即从空树开始
BTreeNode p=null; //定义p为指向二叉树结点的指针
int k=1; //用k作为处理结点的左子树和右子树的标记
char []a=str.toCharArray(); //将字符串内容转换为字符数组a的内容
for(int i=0;i<a.length;i++) //依次处理二叉树广义表字符串中的每个字符
{
switch(a[i])
{
case ' ': //对空格不作任何处理
break;
case '(': //处理左括号
sck.push(p);
k=1;
break;
case ')':
if(sck.isEmpty())
{
System.out.println("二叉树广义表字符串错,返回假!");
return false;
}
sck.pop();
break;
case ',': //处理逗号
k=2;
break;
default: //扫描到的为字母,其他非法字符
if(a[i]>='a' && a[i]<='z' || (a[i] >='A' && a[i]<='Z'))
{
String aa = String.valueOf(a[i]);
p=new BTreeNode(aa);
if(root==null)
{
root=p; //p结点为树根
}
else
{
System.out.println("二叉树广义表中存在非法字符,返回假!");
return false;
}
}
}
}//for循环结束
if(sck.isEmpty())
{
return true;
}
else
{
return false;
}
}
//判断一棵二叉树是否为空,若是则返回true,否则返回false
public boolean isEmpty() {
return root==null;
}
//按照字符串s所给定的次序遍历一棵二叉树,每个结点均被访问一次
public void traverseBtree(String s) {
}
//从二叉树中查找值为obj的结点,若存在则返回完整值否则返回空值
public Object findBTree(BTreeNode rt,Object x) {
if(rt==null)
{
return null; //查找失败返回空值
}
else
{
if(rt.element.equals(x))
{
return rt.element; //树根结点的值等于x则返回该结点的值
}
else
{
Object y;
y=findBTree(rt.left,x); //向左子树查找,若成功则返回结点值
if(y!=null)
{
return y;
}
y=findBTree(rt.right,x); //向右子树查找,若成功则返回节点值
if(y!=null)
{
return y;
}
return null;
}
}
}
//求出一棵二叉树的深度
public int depthBTree(BTreeNode rt) {
if(rt==null)
{
return 0; //对于空树,返回0并结束递归
}
else
{
int dep1=depthBTree(rt.left); //计算左子树深度
int dep2=depthBTree(rt.right); //计算右子树深度
if(dep1>dep2)
{
return dep1+1;
}
else
{
return dep2+2;
}
}
}
//求出一棵二叉树的结点数
public int countBTree(BTreeNode rt) {
if(rt==null)
{
return 0;
}
else
{
return countBTree(rt.left)+countBTree(rt.right)+1;
}
}
//按照树的一种表示方法输出一棵二叉树
public void printBTree(BTreeNode rt) {
if(rt!=null) //树为空时结束递归,否则执行如下操作
{
System.out.print(rt.element); //输出根结点的值
if(rt.left!=null ||rt.right!=null)
{
System.out.print('('); //输出左括号
printBTree(rt.left); //输出左子树
if(rt.right!=null)
{
System.out.print(',');
}
printBTree(rt.right); //输出右子树
System.out.print(')'); //输出右括号
}
}
}
//清除二叉树中的所有结点,使之变为一棵空树
public void clearBTree() {
root=null;
}
}