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

作者:陶然

时间: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,否则返回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);
    }
}

插入新元素的步骤:

  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++ 栈原理及编程实现中会详细说明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值