AVL 平衡二叉搜索树原理及编程实现 (C++)版本 第二版
来源:互联网 发布:数据挖掘软件容易使用 编辑:程序博客网 时间:2024/05/22 16:13
作者:陶然
时间: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,否则返回falseStatusCode GetElem(TreeNode<ElemType> * cur, ElemType &e) const//初始条件:二叉平衡树已经存在,cur为二叉平衡树的一个结点//操作结果:如果这个cur结点不存在,函数返回异常提示。否则用e返回结点cur元素值StatusCode SetElem(TreeNode<ELemType> * cur, const ElemType &e)//初始条件:二叉平衡树已经存在,cur为二叉平衡树的一个结点//操作结果:如果这个cur结点不存在,函数返回异常提示。否则将这个结点cur的值设置为evoid 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)//操作结果:插入数据元素ebool 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++ 栈原理及编程实现中会详细说明。
- AVL 平衡二叉搜索树原理及编程实现 (C++)版本 第二版
- 【算法学习】AVL平衡二叉搜索树原理及各项操作编程实现(C++)
- 【算法学习】AVL平衡二叉搜索树原理及各项操作编程实现(C++)
- AVL平衡二叉查找树实现(C语言版本)
- AVL树 平衡二叉树 简介及实现原理
- AVL树(平衡二叉树)的C语言实现及原理
- 平衡二叉搜索树(AVL树)的原理及实现源代码(有图文详解和C++、Java实现代码)
- 【修改】C实现平衡二叉树---AVL
- C语言实现AVL-平衡二叉树
- 数据结构树相关第二天-AVL二叉平衡搜索树
- AVL平衡二叉搜索树
- AVL平衡搜索二叉树
- AVL平衡二叉搜索树
- 数据结构——平衡二叉搜索树的原理及编程实现
- AVL平衡树(二叉搜索树 c++实现)
- AVL实现平衡二叉树
- 平衡二叉搜索树(AVL树)
- AVL树(平衡二叉搜索树)
- android ExpandableListView三级菜单的使用
- 字符串问题---最小包含子串的长度
- Beginning Spring学习笔记——第3章(三)文件上传、异常处理和个性化
- 单例模式
- CORS——跨域请求那些事儿
- AVL 平衡二叉搜索树原理及编程实现 (C++)版本 第二版
- Android Saving Data
- 关于内存泄漏---auto_ptr
- loadrunner11 回放脚本Action.c(94): 错误 -27979: 找不到请求的表单 [MsgId: MERR-27979]
- LR回放https协议脚本失败:[GENERAL_MSG_CAT_SSL_ERROR]connect to host "XXX" failed:[10054] Connection reset by
- android MAGNETIC_FIELD
- Hadoop 2.0 data write operation acknowledgement
- sqlcipher加密已有数据库及其时机
- 北大方正暗偷明抢,是校办企业的反面教员