数据结构(22)-平衡二叉树

定义

平衡二叉树(Self-Balancing Binary Search Tree 或者 Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个结点的左子树和右子树的高度至多相差1。

平衡二叉树,也称AVL树,由俄罗斯科学家G.M.Adelson-VelskiiE.M.Landis共同发明的一种解决平衡二叉树的算法。

我们将二叉树上结点的左子树深度减去右子树深度所得到的值称为平衡因子BF(Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1,0,1。判断一棵树是不是平衡二叉树,首先需要判断该树是不是二叉排序树,即左子树比双亲结点小,右子树比双亲结点大;然后还需要判断结点的平衡因子是否合规。

距离插入结点最近的,且平衡因子绝对值大于1的结点为根的子树,称之为最小不平衡树。

平衡二叉树实现原理

平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是破坏了,则找出最小不平衡子树,在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,使之成为新的平衡子树。

假设我们有一个数组a[10] = \{3,2,1,4,5,6,7,10,9,8\}需要构建二叉排序树,根据二叉排序树的特性,我们会构建成下图左边样子,但是这样高度达到8的树,查找是比较麻烦的,而右图的样子相对来说更符合我们的预期。

avl树01.png

下面我们就来看看如何构建成右图的样子。3和2,可以正常的构建,到1的时候,此时根结点3的的平衡因子(BF)就变成了2,此时整棵树就变成了最小不平衡子树。由于BF值为证,因此我们需要将整棵树右旋,让结点2变成根结点,3变成2的右孩子。然后加入4,树还是平衡的,然后再加入结点5,此时结点3的BF值变为了-2,则需要左旋,将结点4变为结点2的右子树,结点3变为结点4的左子树,二叉树又达到了新的平衡。接着加入结点6,此时根结点2的BF值变成了-2,需要将树左旋,此时结点4变成了根结点,结点2变成了结点4的左子树,而结点3也需要进行处理,根据二叉排序树的特点结点3就变成了结点2的右孩子。接着加入结点7,此时结点5的BF值变成了-2,左旋,结点6变成了结点4的右子树,结点5变成了结点6的左子树。加入结点10,平衡。接着加入结点9,此时,结点4、6、7的BF值都变成了-2,我们只需要左旋最小不平衡子树7、9、10即可,左旋之后可以发现,树变成了非排序树,这显然是不行的,原因在于结点7的BF值是-2,结点10的BF值是1,符号并不统一,所以我们需要先对结点9和结点10进行右旋,然后再对结点7、9、10左旋,此时结点9变成了结点6的右子树,结点7变成了结点9的左子树,结点10变成了结点9的右子树,平衡。接着加入结点8,这和结点9的情况相同,结点6的BF值变成了-2,结点9的BF值变成了-1,此时需要先对结点9右旋,然后再对结点6左旋,此时结点7就变成了根结点4的右子树,结点6变成了结点7的左子树,结点9变成了结点7的右子树,结点8变成了结点9的左子树,达到最终的平衡。如上图右边所示。

可以看出构建平衡二叉树的过程,就是在不断的调整过程,BF大于1就右旋,BF小于-1就左旋。最小不平衡子树的BF与它的子树的BF符号相反时,就需要对结点先进行一次旋转使得符号相同,然后再进行旋转才能达到平衡。

平衡二叉树的实现

首先,我们需要对二叉排序树的结点结构增加一个平衡因子:

typedef int TStatus;
typedef int ElementType;

typedef struct BinaryNode {
    ElementType data;
    // 存储平衡因子
    int bf;
    struct BinaryNode *leftChild, *rightChild;
} BinaryNode, *BinaryTree;

其次,左旋右旋的时候,我们需要修改结点子树的指向:

// 对以bTree为根结点进行右旋处理
void rightRotate(BinaryTree *bTree) {
    BinaryTree leftChild = (*bTree)->leftChild;
    // 旋转之后leftChild的右子树就变成了bTree
    // 而leftChild原来的右子树因为要满足二叉排序树的特性则需要变成bTree的左子树
    (*bTree)->leftChild = leftChild->rightChild;
    leftChild->rightChild = (*bTree);
    
    // 此时新的根结点就变成了leftChild
    *bTree = leftChild;
}

// 对以bTree为根结点进行左旋处理
void leftRotate(BinaryTree *bTree) {
    BinaryTree rightChild = (*bTree)->rightChild;
    
    // 旋转之后rightChild的左子树就变成了bTree
    // rightChild之前的左子树比rightChild小,但是比bTree大,则需要变成bTree的右子树
    (*bTree)->rightChild = rightChild->leftChild;
    rightChild->leftChild = (*bTree);
    // 此时新的根结点就变成rightChild
    *bTree = rightChild;
}

最后,我们还需要对每个结点的平衡因子做处理,而且如果出现平衡因子符号不一致的情况,需要做双旋操作:

#define LH +1 // 左边比右边高
#define EH 0  // 左右等高
#define RH -1 // 右边比左边高

// 对以bTree为根结点的二叉树做右平衡处理
void rightBalance(BinaryTree *bTree) {
    BinaryTree rightChild = (*bTree)->rightChild;
    BinaryTree rightL;
    switch (rightChild->bf) {
        case RH:
            // 新结点插入到bTree的右子树的右子树 需要左旋
            (*bTree)->bf = rightChild->bf = EH;
            leftRotate(bTree);
            break;
        case LH:
            // 新结点插入到bTree的左子树的右子树 需要双旋 因为bf的值为+1 则先右旋再左旋
            rightL = rightChild->leftChild;
            switch (rightL->bf) {
                case RH:
                    (*bTree)->bf = LH;
                    rightChild->bf = EH;
                    break;
                case EH:
                    (*bTree)->bf = rightChild->bf = EH;
                    break;
                case LH:
                    (*bTree)->bf = EH;
                    rightChild->bf = RH;
                    break;
                default:
                    break;
            }
            rightL->bf = EH;
            rightRotate(&(*bTree)->rightChild);
            leftRotate(bTree);
            break;
        default:
            break;
    }
}

// 对以bTree为根结点的二叉树做左平衡处理
void leftBalance(BinaryTree *bTree) {
    BinaryTree leftChild = (*bTree)->leftChild;
    BinaryTree leftR;
    switch (leftChild->bf) {
        case LH:
            // 新结点插入到bTree的左子树的左子树 需要右旋
            (*bTree)->bf = leftChild->bf = EH;
            rightRotate(bTree);
            break;
        case RH:
            // 新结点插入到bTree的右子树的左子树 需要双旋 因为bf的值为-1 则先左旋再右旋
            // 先修改平衡因子 然后再旋转
            leftR = leftChild->rightChild;
            switch (leftR->bf) {
                case LH:
                    (*bTree)->bf = RH;
                    leftChild->bf = EH;
                    break;
                case EH:
                    (*bTree)->bf = leftChild->bf = EH;
                case RH:
                    (*bTree)->bf = EH;
                    leftChild->bf = LH;
                default:
                    break;
            }
            leftR->bf = EH;
            leftRotate(&(*bTree)->leftChild);
            rightRotate(bTree);
            break;
        default:
            break;
    }
}

现在我们就可以直接插入结点来构建平衡二叉树了:

/// @param e 插入的结点值
/// @param isTall 插入之后树是否长高,即高度是否增加
TStatus insertAVLTree(BinaryTree *bTree, int e, bool *isTall) {
    if (!(*bTree)) {
        // 新插入的结点
        *bTree = (BinaryTree)malloc(sizeof(BinaryNode));
        (*bTree)->data = e;
        (*bTree)->leftChild = (*bTree)->rightChild = NULL;
        (*bTree)->bf = EH;
        *isTall = true;
    } else {
        if ((*bTree)->data == e) {
            // 已经存在data相同的结点
            isTall = false;
            return T_ERROR;
        }
        if (e < (*bTree)->data) {
            // 小于 插入到左子树中
            if (insertAVLTree(&(*bTree)->leftChild, e, isTall) == T_ERROR) {
                // 插入失败 返回
                return T_ERROR;
            }
            if (*isTall) {
                // 插入左子树 且左子树长高
                switch ((*bTree)->bf) {
                    case LH:
                        // 原来左子树比右子树高,则需要做左平衡处理
                        leftBalance(bTree);
                        *isTall = false;
                        break;
                    case EH:
                        // 原来左右子树等高 插入后左子树增高
                        (*bTree)->bf = LH;
                        *isTall = true;
                        break;
                    case RH:
                        // 原来右子树比左子树高,插入后 等高
                        (*bTree)->bf = EH;
                        *isTall = false;
                        break;
                    default:
                        break;
                }
            }
        } else {
            // 大于等于 插入右子树
            if (insertAVLTree(&(*bTree)->rightChild, e, isTall) == T_ERROR) {
                // 插入失败 返回
                return T_ERROR;
            }
            
            if (*isTall) {
                switch ((*bTree)->bf) {
                    case LH:
                        (*bTree)->bf = EH;
                        *isTall = false;
                        break;
                    case EH:
                        (*bTree)->bf = RH;
                        *isTall = true;
                        break;
                    case RH:
                        rightBalance(bTree);
                        *isTall = false;
                        break;
                    default:
                        break;
                }
            }
        }
    }
    
    return T_OK;
}

平衡二叉树增加了由于带有二叉排序树的特性,而且因为自身的平衡性,降低了树的高度,这样大大的提高了对树的查找、插入、删除等动作的便利性,这三种操作的时间复杂度都为O(longn)

参考文献

  • 大话数据结构
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354