二叉树的存储结构有两种:顺序存储结构和链式存储结构。
顺序存储结构
对于满二叉树和完全二叉树来说,可以将其数据元素逐层存放到一组连续的存储单元中,如图6-3 所示。用一维数组来实现顺序存储结构时,将二叉树中编号为i 的结点存放到数组中的第i 个分量中。如此根据性质6.7,可以得到结点i 的父结点、左右孩子结点分别存放在、2i 以及2i+1 i / 2 分量中。
图6-3 顺序存储结构
这种存储方式对于满二叉树和完全二叉树是非常合适也是高效方便的。因为满二叉树和完全二叉树采用顺序存储结构既不浪费空间,也可以根据公式很快的确定结点之间的关系。
但是对于一般的二叉树而言,必须用“虚结点”将一棵二叉树补成一棵完全二叉树来存储,否则无法确定结点之间的前驱后续关系,但是这样一来就会造成空间的浪费。一种极端的情况是,为了存储k个结点,需要2k-1 个存储单元,图6- 4 说明了这一情况。此时存储空间浪费巨大,这是顺序存储结构的一个缺点。
图6-4 单支二叉树的顺序存储结构
链式存储结构
设计不同的结点结构可构成不同的链式存储结构。在二叉树中每个结点都有两个孩子,则可以设计每个结点至少包括3 个域:数据域、左孩子域和右孩子域。数据域存放数据元素,
左孩子域存放指向左孩子结点的指针,右孩子域存放指向右孩子结点的指针。如图6-5(a)所示。利用此结点结构得到的二叉树存储结构称为二叉链表。容易证明在具有n 个结点的二叉链表中有n+1 个空链域。
图6-5 二叉树的链式存储结构
为了方便找到父结点,可以在上述结点结构中增加一个指针域,指向结点的父结点。如图6-5(b)所示。采用此结点结构得到的二叉树存储结构称为三叉链表。在具有n 个结点的三叉链表中也有n+1 个空链域。
不同的存储结构实现二叉树操作的方法也不同。例如要找某个结点的父结点,在三叉链表中很容易实现;在二叉链表中则需从根结点出发一一查找。在实际应用中,要根据二叉树的主要操作来选择存储结构。
为了方便的找到父结点,我们以三叉链表作为二叉树的存储结构,并且在6.3 节中,二叉树的基本操作的实现也是基于三叉链表来实现的。下面我们首先给出具有四个域的结点结构的定义。
代码6-1 二叉树存储结构结点定义
public class BinTreeNode implements Node {
private Object data; //数据域
private BinTreeNode parent; //父结点
private BinTreeNode lChild; //左孩子
private BinTreeNode rChild; //右孩子
private int height; //以该结点为根的子树的高度
private int size; //该结点子孙数(包括结点本身)
102
public BinTreeNode() { this(null); }
public BinTreeNode(Object e) {
data = e; height = 0; size = 1;
parent = lChild = rChild = null;
}
/******Node 接口方法******/
public Object getData() { return data; }
public void setData(Object obj) { data = obj;}
/******辅助方法,判断当前结点位置情况******/
//判断是否有父亲
public boolean hasParent(){ return parent!=null;}
//判断是否有左孩子
public boolean hasLChild(){ return lChild!=null;}
//判断是否有右孩子
public boolean hasRChild(){ return rChild!=null;}
//判断是否为叶子结点
public boolean isLeaf(){ return !hasLChild()&&!hasRChild();}
//判断是否为某结点的左孩子
public boolean isLChild(){ return (hasParent()&&this==parent.lChild);}
//判断是否为某结点的右孩子
public boolean isRChild(){ return (hasParent()&&this==parent.rChild);}
/******与height 相关的方法******/
//取结点的高度,即以该结点为根的树的高度
public int getHeight() { return height; }
//更新当前结点及其祖先的高度
public void updateHeight(){
int newH = 0;//新高度初始化为0,高度等于左右子树高度加1 中的大者
if (hasLChild()) newH = Math.max(newH,1+getLChild().getHeight());
if (hasRChild()) newH = Math.max(newH,1+getRChild().getHeight());
if (newH==height) return; //高度没有发生变化则直接返回
height = newH; //否则更新高度
if (hasParent()) getParent().updateHeight(); //递归更新祖先的高度
}
/******与size 相关的方法******/
//取以该结点为根的树的结点数
public int getSize() { return size; }
//更新当前结点及其祖先的子孙数
public void updateSize(){
size = 1; //初始化为1,结点本身
if (hasLChild()) size += getLChild().getSize(); //加上左子树规模
if (hasRChild()) size += getRChild().getSize(); //加上右子树规模
if (hasParent()) getParent().updateSize(); //递归更新祖先的规模
}
/******与parent 相关的方法******/
//取父结点
public BinTreeNode getParent() { return parent; }
//断开与父亲的关系
public void sever(){
if (!hasParent()) return;
if (isLChild()) parent.lChild = null;
else parent.rChild = null;
parent.updateHeight(); //更新父结点及其祖先高度
parent.updateSize(); //更新父结点及其祖先规模
parent = null;
}
/******与lChild 相关的方法******/
//取左孩子
public BinTreeNode getLChild() { return lChild; }
//设置当前结点的左孩子,返回原左孩子
public BinTreeNode setLChild(BinTreeNode lc){
BinTreeNode oldLC = this.lChild;
if (hasLChild()) { lChild.sever();} //断开当前左孩子与结点的关系
if (lc!=null){
lc.sever(); //断开lc 与其父结点的关系
this.lChild = lc; //确定父子关系
lc.parent = this;
this.updateHeight(); //更新当前结点及其祖先高度
this.updateSize(); //更新当前结点及其祖先规模
}
return oldLC; //返回原左孩子
}
/******与rChild 相关的方法******/
//取右孩子
public BinTreeNode getRChild() { return rChild; }
//设置当前结点的右孩子,返回原右孩子
public BinTreeNode setRChild(BinTreeNode rc){
BinTreeNode oldRC = this.rChild;
if (hasRChild()) { rChild.sever();} //断开当前右孩子与结点的关系
if (rc!=null){
rc.sever(); //断开lc 与其父结点的关系
this.rChild = rc; //确定父子关系
104
rc.parent = this;
this.updateHeight(); //更新当前结点及其祖先高度
this.updateSize(); //更新当前结点及其祖先规模
}
return oldRC; //返回原右孩子
}
}
代码6-1 说明:代码中判断当前结点位置情况的辅助方法以及简单的get 方法都在常数时间内可以完成,实现也相应非常简单。下面主要讨论updateHeight ()、updateSize ()、sever()、setLChild(lc)、getRChild(rc)的实现与时间复杂度。
⑴ updateHeight ():若当前结点v 的孩子发生变化,就需要使用updateHeight ()方法更新当前结点及其祖先结点的高度。请注意,由于一个结点的高度发生变化,会影响到其祖先结点的高度,在这里我们允许直接对任何结点执行这一操作。因为在二叉树中任何一个结点的高度,都等于其左右子树的高度中大者加1,而左右子树的高度只需要获取该结点左右孩子的高度即可获得,只需要Θ(1)时间。续而从v 出发沿parent 引用逆行向上,依次更新各祖先结点的高度即可。如果在上述过程中,发现某个结点的高度没有发生变化,算法可以直接终止。综上所述,当对一个结点v 调用updateHeight ()方法时,若v 的层数为level(v),则最多只需要更新level(v)+1 个结点的高度,因此算法的时
间复杂度T(n) = Ο(level(v))。
⑵ updateSize ():同样如果结点v 的孩子发生变化,应该更新当前结点以及其祖先的规模。因为在二叉树中任何一个结点的规模,都等于其左右子树的规模之和加上结点自身,而左右子树的规模只需要获取该结点左右孩子的规模即可获得,只需要Θ(1)时间。因此算法的时间复杂度T(n) = Ο(level(v))。
⑶ sever():切断结点v 与父结点p 之间的关系。该算法需要修改v 与p 的指针域,需要常数时间。除此之外由于p 结点的孩子发生了变化,因此需要调用updateHeight ()和updateSize ()来更新父结点p 及其祖先的高度与规模。其时间复杂度T(n) = Ο(level(v))。
⑷ setLChild(lc)、getRChild(rc):两个算法的功能相对,一个是设置结点v 的左孩子,
一个是设置结点v 的右孩子。两个算法的实现是类似的,以setLChild()为例说明。首先,如果v 有左孩子oldLC,则应当调用oldLC. sever()断开v 与其左孩子的关系。其次,调用lc. sever()断开其与父结点的关系。最后,建立v 与lc 之间的父子关系,并调用v. updateSize ()与v.updateHeight ()更新v 及其祖先的规模与高度。
更多精彩内容请关注:http://bbs.superwu.cn
关注超人学院微信二维码:
关注超人学院java免费学习交流群:
原文地址:http://crxy2013.blog.51cto.com/9922445/1657789