AVL 平衡二叉搜索树原理及编程实现 (C++)版本 第二版

来源:互联网 发布:数据挖掘软件容易使用 编辑:程序博客网 时间:2024/05/22 16:13

作者:陶然

时间:2017年8月26日

转载请注明,请尊重作者。

本文仅代表我对二叉树的理解与认识,并部分参考网络,如果有任何错误或者疑问,也请Email我提醒我及时修改与更新。之所以研究平衡二叉树,也是因为最近一个作业,要求使用平衡二叉树来完成,但是搜索了网络,很难找到适合的解决方案,所以在这里也顺便讨论一下问题的解决思路和参考代码。本文大概的思路是先讲解一下平衡二叉树,然后讲解一下平衡二叉树的原理,然后讲解一下如何在C++中实现平衡二叉树,最后根据作业要求,实践操作。

第一部分,二叉平衡树的定义

二叉排序树,也叫AVL Tree,他查找算法的性能取决于二叉树的结构,而二叉树的形状,则取决于他的数据收集。如果数据是有序排列,则二叉树就是线性的,这样的话,它的查找算法效率不是很高。当然,这种情况属于最糟糕的。但是,如果二叉树的结构合理,则它的查找算法则会是最快的。事实上,很明显,一棵树的告诉越小,它的查找速度就会越快。因为这个特性,所以我们永远都希望我们的树的高度,尽量的小。这里的高度,依旧是确保它依旧符合二叉树的特点。二叉平衡树是一种特殊类型的二叉树。在二叉平衡树中,二叉树的结构基本上是平衡的。

二叉平衡树具有如下的特征:

  1. 根的左子树和右子树的高度差的绝对值不大于1;
  2. 根的左子树和右子树都是二叉平衡树。

    如果把二叉树上的结点的平衡因子,也就是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);    }}

插入新元素的步骤:

  1. 用等待插入的数据元素创建一个结点
  2. 查找树,找到新结点应在的位置(树中位置)
  3. 将新结点插入树中
  4. 从插入结点回溯至根结点的路径,该路径是为了查找新结点的位置(树中位置)而建立的

用下面的函数来实现第四步,函数用了一个布尔型参数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++ 栈原理及编程实现中会详细说明。

阅读全文
0 0