作者:陶然
时间:2017年8月26日
转载请注明,请尊重作者。
本文仅代表我对二叉树的理解与认识,并部分参考网络,如果有任何错误或者疑问,也请Email我提醒我及时修改与更新。之所以研究平衡二叉树,也是因为最近一个作业,要求使用平衡二叉树来完成,但是搜索了网络,很难找到适合的解决方案,所以在这里也顺便讨论一下问题的解决思路和参考代码。
本文大概的思路是先讲解一下平衡二叉树,然后讲解一下平衡二叉树的原理,然后讲解一下如何在C++中实现平衡二叉树,最后根据作业要求,实践操作。
第一部分,二叉平衡树的定义
二叉排序树,也叫AVL Tree,他查找算法的性能取决于二叉树的结构,而二叉树的形状,则取决于他的数据收集。如果数据是有序排列,则二叉树就是线性的,这样的话,它的查找算法效率不是很高。当然,这种情况属于最糟糕的。但是,如果二叉树的结构合理,则它的查找算法则会是最快的。事实上,很明显,一棵树的告诉越小,它的查找速度就会越快。因为这个特性,所以我们永远都希望我们的树的高度,尽量的小。这里的高度,依旧是确保它依旧符合二叉树的特点。二叉平衡树是一种特殊类型的二叉树。在二叉平衡树中,二叉树的结构基本上是平衡的。
二叉平衡树具有如下的特征:
- 根的左子树和右子树的高度差的绝对值不大于1;
根的左子树和右子树都是二叉平衡树。
如果把二叉树上的结点的平衡因子,也就是Balance Factor(BF)定义为这个结点的左子树和右子树的高度差,则二叉平衡树上所有结点的BF的绝对值小于等于1,只能是1、0和-1。
第二部分,二叉排序树的一些基本操作
在讲解基本操作之前,先说几个名词。
我们需要一个结构体来存储结点。我们在这里定义一下这个结构体
struct BinAVLTreeNode{
ElemType data; //数据域
int bf; //结点的平衡因子
BinAVLTreeNode<ElemType> * leftChild; //左孩子指针域
BinAVLTreeNode<ElemType> * rightChild; //右孩子指针域
}
我们通过自定义数据类型(User defined data types)来定义一个ElemType类型:
typedef existing_type new_type_name; 利用typedef来实现。这里 existing_type 是C++ 基本数据类型或其它已经被定义了的数据类型,new_type_name 是我们将要定义的新数据类型的名称。
typedef char * ElemType; //定义一个ElemType类型
现在我们来看一下AVL Tree有哪些操作。所有操作的初始条件都是二叉平衡树已经存在。
BinAVLTreeNode <ElemType> * GetRoot() const;
//操作结果:返回二叉平衡树的根
Bool Empty() const;
//操作结果:如果二叉平衡树为空,返回true,否则返回false
StatusCode GetElem(TreeNode<ElemType> * cur, ElemType &e) const
//初始条件:二叉平衡树已经存在,cur为二叉平衡树的一个结点
//操作结果:如果这个cur结点不存在,函数返回异常提示。否则用e返回结点cur元素值
StatusCode SetElem(TreeNode<ELemType> * cur, const ElemType &e)
//初始条件:二叉平衡树已经存在,cur为二叉平衡树的一个结点
//操作结果:如果这个cur结点不存在,函数返回异常提示。否则将这个结点cur的值设置为e
void Inorder(void (* Visit)(const ElemType &)) const
//操作结果:中序遍历二叉平衡树,对每个结点调用函数(* Visit)
void PreOrder(void (* Visit)(const ElemType &)) const
//操作结果:先序遍历二叉平衡树,对每个结点调用函数(* Visit)
void PostOrder(void (* Visit)(const ElemType &)) const
//操作结果:后序遍历二叉平衡树,对每个结点调用函数(* Visit)
void Levelorder(void (* Visit)(const ElemType &)) const
//操作结果:层次遍历二叉平衡树,对每个结点调用函数(* Visit)
中序,先序,后序,层次遍历,在这里先不强调,在这里也不需要过分的纠结。
int NodeCount() const
//操作结果:返回二叉平衡树的结点个数
int Height() const
//操作结果:返回二叉平衡树的高
BinAVLTreeNode<ElemType> * Search(const KeyType &key) const
//操作结果:查找关键字为key的数据元素
作业中没有要求Search这个功能,所以这里不做过多解释,后面版本可能会增加这部分的解释说明。
bool Insert(const ElemType &e)
//操作结果:插入数据元素e
bool Delete(const ElemType &e)
//操作结果:删除关键字为key的数据元素
作业中没有要求Delete这个功能,所以这里不做过多解释,后面版本可能会增加这部分的解释说明。
BinAVLTreeNode <ElemType> * LeftChild (const BinAVLTreeNode <ElemType> * cur) const
//初始条件:二叉平衡树已经存在,cur为二叉平衡树的一个结点
//操作结果:返回二叉平衡树结点cur的左孩子
BinAVLTreeNode <ElemType> * RightChild (const BinAVLTreeNode <ElemType> * cur) const
//初始条件:二叉平衡树已经存在,cur为二叉平衡树的一个结点
//操作结果:返回二叉平衡树结点cur的右孩子
BinAVLTreeNode <ElemType> * Parent (const BinAVLTreeNode <ElemType> * cur) const
//初始条件:二叉平衡树已经存在,cur为二叉平衡树的一个结点
//操作结果:返回二叉平衡树结点cur的双亲结点
上面的部分操作,在本版本中不做过多解释说明。
对于二叉平衡树,BF与每个结点都有关系,每个结点必须要存在BF的值。
我们定义一下左高,等高,右高:
#define LH 1 //左高
#define EH 0 //等高
#define RH -1 //右高
第三部分,下面说一下二叉平衡树实现中几个重要的函数
首先是插入函数,要在一棵二叉平衡树中插入一个元素,首先需要找到二叉树中结点要插入的位置,因为二叉平衡树是一个二叉排序树,所以想给要插入的元素找到插入点,可以用二叉树的查找算法来查找二叉树。两种情况:第一种,要插入的结点已经存在于这棵二叉树中,也就是查找到一个非空子树的结点时停止,原因是:二叉树中不准许有相同元素出现。根据这个特点,作业中的单词词频计数,可以用这个特点来实现。当要插入的数据已存在于树中,则属于击中,该已存在的(这里指和要插入数据相同的树中该数据的位置)数据的计数器自加1。第二种情况,加入要插入的数据,不存在于二叉平衡树中,则查找到达这个空子树的地方停止,然后把这个数据,插入到树中。但是,注意,这里要注意。因为这里开始,就是和二叉树有区别的地方了。数据插入到树后,这棵树,可能就不是二叉平衡树,因为可能不平衡了,所以我们需要额外的操作,来调整这棵树的结构,使它保持平衡。可以通过回溯插入新元素时所经过的路径来实现。当插入这个数据的时候,这条路径上所有的结点,都会被访问,这样的话,BF可能就被改变了,也可能需要重新建立这棵树的某一部分。回溯插入新数据时所经过的路径,需要一个额外的函数来辅助它,这就是辅助查找算法。
注意,这里的栈不可以使用官方的标准库来实现。所以我在这里先简单写一下实现。代码仅供理解Stack。
//Push & Pop in Linked Stack
//class and typedef implementation
#include <iostream>
using namespace std;
class stack
{
private:
typedef struct node
{
int info;
node* next;
}* nptr;
nptr top,save,ptr;
public:
stack()
{
top=NULL;
save=NULL;
ptr=NULL;
}
nptr create(int n)
{
ptr=new node;
ptr->info=n;
ptr->next=NULL;
return ptr;
}
void push(nptr np)
{
if(np==NULL)
{
cout<<"\nOVERFLOW !!!\nABORTING !!!\n";
exit(1);
}
else
{
if(top==NULL)
top=np;
else
{
save=top;
top=np;
np->next=save;
}
}
}
void pop()
{
if(top==NULL)
{
cout<<"\nUNDERFLOW !!!\nABORTING !!!\n";
exit(1);
}
else
{
ptr=top;
top=top->next;
delete ptr;
}
}
void display(nptr np);
nptr topout()
{
return top;
}
int delout()
{
return top->info;
}
~stack()
{
cout<<"\n\nTHANKYOU FOR USING THIS APPLICATION !!!\n";
}
};
stack s;
void stack::display(nptr np)
{
cout<<"\nThe Stack Now is : ";
cout<<"\n"<<np->info<<"<-";
while(np!=NULL)
{
np=np->next;
cout<<"\n"<<np->info;
}
cout<<"\n!!!";
}
void printhead()
{
clrscr();
cout<<"\n\t\tLinked List";
cout<<"\n\t\t****** ****";
}
void printfoot()
{
clrscr();
cout<<"\n\t\tPRESS 1 TO GO TO MAIN MENU";
cout<<"\n\t\tPRESS 0 TO EXIT APPLICATION\n";
}
void insert()
{
int a;
char c='y';
cout<<"\n\tInsertion into Stack : ";
cout<<"\n\t--------- ---- -----";
while((c=='y')||(c=='Y'))
{
cout<<"\nEnter Info for Node : ";
cin>>a;
s.push(s.create(a));
s.display(s.topout());
cout<<"\nPress 'Y' for more nodes : ";
cin>>c;
}
}
void remove()
{
char c='y';
cout<<"\n\tDeletion from Stack : ";
cout<<"\n\t-------- ---- -----";
while((c=='y')||(c=='Y'))
{
cout<<"\nPop ?(Y/N) : ";
cin>>c;
if((c=='Y')||(c=='y'))
{
cout<<"\nThe Element to be Deleted is : "<<s.delout();
s.pop();
}
s.display(s.topout());
}
}
void main()
{
char ch;
clrscr();
menu:
printhead();
cout<<"\n1.Insertion into Stack";
cout<<"\n2.Deletion from Stack";
cout<<"\n3.Exit";
cout<<"\nEnter Choice : ";
cin>>ch;
switch(ch)
{
case 1:
insert();
break;
case 2:
remove();
break;
case 3:
goto quit;
default:
goto menu;
}
printfoot();
quit:
getch();
}
下面来写一下查找辅助算法:
BinaryAVLTreeNode <ElemType> * SearchHelp(
const KeyType &key, BinAVLTreeNode <ElemType> * &f,
LinkStack<BinAVLTreeNode <ElemType> *> &s){
//返回指向key的元素的指针,用f返回它的双亲,栈s来存储查找路径
BinAVLTreeNode <ElemType> * p = GetRoot(); //指向当前结点
f=NULL; //指向p的双亲
while (p != NULL && p->data != key){
//开始查找为key的结点
if (key < p->data){ //key小,则去左子树继续查找
f = p;
s.Push(p);
p = p->leftChild;
}else{ //key大,则去右子树继续查找
f = p;
s.Push(p);
p = p->rightChild;
}
}
return p;
}
现在讨论一下插入后,树的失去了平衡,我们就需要一些操作来使树保持平衡。因为这里只能文字介绍。操作分为四种。下面分别介绍一下。
第一种操作:左左旋转
void BinaryAVLTree<ElemType, KeyType> LeftRotate(BinAVLTreeNode<ElemType> * &subRoot){
//对以subRoot为根的二叉树做左旋处理,处理后,subRoot指向新的树根结点,这个根节点,也就是旋转处理前的右子树的根节点
BinAVLTreeNode<ElemType> * ptrRChild;
ptrRChild = subRoot->rightChild; //ptrRChild指向subRoot右孩子
subRoot->rightChild = ptrRChild->leftChild;
ptrRChild->leftChild = subRoot; //subRoot链接为ptrRChild的左孩子
subRoot = ptrRChild; //subRoot指向新的结点
}
第二种操作:右右旋转
void BinaryAVLTree<ElemType, KeyType> RightChild(BinAVLTreeNode<ElemType> * &subRoot){
//对以subRoot为根的二叉树做左旋处理,处理后,subRoot指向新的树根结点,这个根节点,也就是旋转处理前的右子树的根节点
BinAVLTreeNode<ElemType> * ptrLChild;
ptrLChild = subRoot->leftChild; //ptrLChild指向subRoot左孩子
subRoot->leftChild = ptrLChild->rightChild;//ptrLChild的右子树链接为subRoot的左子树
ptrLChild->rightChild = subRoot; //subRoot链接为ptrLChild的右子树
subRoot = ptrLChild; //subRoot指向新的结点
}
两种单旋转实现了,现在写一下InsertLeftBalance和InsertRightBalance函数。InsertLeftBalance函数处理插入结点在旋转结点的子左树上,InsertRightBalance函数处理插入结点在旋转结点的右子树上,以指向要旋转结点的指针作为参数传递给函数。它们使用上面的两个函数来做旋转平衡处理,同时调整由于重构而变化的结点BF。
void BinaryAVLTree<ElemType, KeyType> InsertLeftBalance(BinAVLTreeNode<ElemType> * &subRoot){
//该操作,以对subRoot为根的二叉树插入时左平衡处理,插入结点在subRoot左子树上,处理后subRoot指向新的树根结点
BinAVLTreeNode<ElemType> * ptrLChild, * ptrLRChild;
ptrLChild = subRoot->leftChild;
switch(ptrLChild->bf){
case LH:
subRoot->bf = ptrLChild->bf = EH;
RightRotate(subRoot);
break;
case RH:
ptrLRChild = ptrLChild->rightChild;
switch(ptrLRChild->bf){
case LH:
subRoot->bf = RH;
ptrLChild->bf = EH;
break;
case EH:
subRoot->bf = ptrLChild->bf = EH;
break;
case RH:
subRoot->bf = EH;
ptrLChild->bf = LH;
break;
}
ptrLRChild->bf = 0;
LeftRotate(subRoot->leftChild);
RightRotate(subRoot);
}
}
void BinaryAVLTree<ElemType, KeyType> InsertRightBalance(BinAVLTreeNode<ElemType> * &subRoot){
//该操作,以对subRoot为根的二叉树插入时左平衡处理,插入结点在subRoot左子树上,处理后subRoot指向新的树根结点
BinAVLTreeNode<ElemType> * ptrRChild, * ptrRLChild;
ptrRChild = subRoot->rightChild;
switch(ptrRChild->bf){
case RH:
subRoot->bf = ptrRChild->bf = EH;
LeftRotate(subRoot);
break;
case LH:
ptrRLChild = ptrRChild->leftChild;
switch(ptrRLChild->bf){
case RH:
subRoot->bf = LH;
ptrRChild->bf = EH;
break;
case EH:
subRoot->bf = ptrRChild->bf = EH;
break;
case LH:
subRoot->bf = EH;
ptrRChild->bf = RH;
break;
}
ptrRLChild->bf = 0;
RightRotate(subRoot->rightChild);
LeftRotate(subRoot);
}
}
插入新元素的步骤:
- 用等待插入的数据元素创建一个结点
- 查找树,找到新结点应在的位置(树中位置)
- 将新结点插入树中
- 从插入结点回溯至根结点的路径,该路径是为了查找新结点的位置(树中位置)而建立的
用下面的函数来实现第四步,函数用了一个布尔型参数isTaller向父结点表示子树的高度,是否有增长。
void BinaryAVLTree<ElemType, KeyType> InsertBalance(const ElemType &e, LinkStack<BinAVLTreeNode<ElemType> * >&s){
bool isTaller = true;
while(!s.Empty() && isTaller){
BinAVLTreeNode<ElemType> * ptrCurNode, * ptrParent;
s.Pop(ptrCurNode);
if(s.Empty()){
ptrParent = NULL;
}else{
s.Top(ptrParent);
}
if(e<ptrCurNode->data){
switch(ptrCurNode->bf){
case LH:
if(ptrParent == NULL){
InsertLeftBalance(ptrCurNode);
root = ptrCurNode;
}else if(ptrParent->leftChild == ptrCurNode){
InsertLeftBalance(ptrParent->leftChild);
}else{
InsertLeftBalance(ptrParent->rightChild);
}
isTaller=false;
break;
case EH:
ptrCurNode->bf = LH;
break;
case RH:
ptrCurNode->bf = EH;
isTaller = false;
break;
}
}else{
switch(ptrCurNode->bf){
case RH:
if(ptrParent == NULL){
InsertRightBalance(ptrCurNode);
root = ptrCurNode;
}else if(ptrParent->leftChild = ptrCurNode){
InsertRightBalance(ptrParent->leftChild);
}else{
InsertRightBalance(ptrParent->rightChild);
}
isTaller = false;
break;
case EH:
ptrCurNode->bf = RH;
break;
case LH:
ptrCurNode->bf = EH;
isTaller = false;
break;
}
}
}
}
下面正式写一下Insert函数来实现插入数据。
bool BinaryAVLTree<ElemType, KeyType> Insert(const ElemType &e){
BinAVLTreeNode<ElemType> * f;
LinkStack<BinAVLTreeNode<ElemType> * > s;
if(SearchHelp(e, f, s) == NULL){
BinAVLTreeNode<ElemType> * p;
p = new BinAVLTreeNode<ElemType>(e);
p->bf = 0;
if(Empty()){
root = p;
}else if(e < f->data){
f->leftChild = p;
}else{
f->rightChild = p;
}
InsertBalance(e, s);
return true;
}else{
return false; //查找成功,插入失败
}
}
到这步,整个插入算法就实现了。步骤注解在下个版本会详细说明。
栈实现代码,Pop( ), Push( )暂时不写了,这个在另外一篇C++ 栈原理及编程实现中会详细说明。