• 树(三)——自平衡二叉树(AVL)


    简介

    自平衡二叉树(AVL)属于二叉平衡树的一类,此类树主要完成一个从键到值的查找过程,即字典(或映射),它维护树高度的方式与其他数据结构不同。

    自平衡规则:

    1. AVL树的左、右子树都是AVL树
    2. 左、右子树的高度差不超过1

    在数据结构中,最常见的数据间关系的抽象就是集合Collection字典Dictionary

    集合就是线性表(元素允许重复),而字典是一种非多键映射关系(键不允许重复)。

    对集合而言,一个班中的所有学生构成一个集合,可以是有序的(有序集合)也可以是无序的(无序集合),查找的时间复杂度一般是O(n),很低。集合的典型就是C#中的List,STL中的vector。集合主要用于存储数据,很少用于查找。如要用于查找,那么可以选择基于有序表的二分查找,相应时间复杂度是O(logn),而要想从无序表转变为有序表,就是各种排序算法的使命了。

    而对字典而言,如同根据一个学生的学号找到他这次考试的成绩一样,主要是用于查找的。这是从键到值的单值映射,键不能重复,值可以重复。根据键找到值是字典的工作。如果是平衡查找树,就为O(logn),实现一般以红黑树为主(比AVL简单),如Java的TreeMap、STL的map;如果是哈希表,就为O(1),如Java的HashMap。

    源码

    由于网上的算法通常是以递归形式出现的,且结点有父指针,更加自由。但是在这里,没有递归没有父指针,这就意味着,算法的实现难度要更高,算法的维护与调试用了两天时间。

    解决递归问题靠栈,解决无父指针问题靠栈,殊途同归。

    avltree.h(AVL Tree) AVL树模版

    baltree.h(Balance Tree) 平衡树基类

    btree.cpp(Binary Tree and Console) main函数

    树的建立

    在这里,只讲元素的插入,而对于删除操作,由于其复杂性,暂且不能实现,参考网上关于树的旋转等资料,实现了删除操作,但没有进行完整测试。

    结点的数据结构为:

    template<class T>
    struct AVLNode
    {
        T data;
        AVLNode *lchild, *rchild;
        int BF; // 平衡因子
    };

    前面与二叉树一样,BF用于累计高度差,因为查询高度是一个枯燥的递归操作。

    这里用到了数学归纳的知识,一开始,树的BF是满足要求的,那么,我们对树的每个操作若可以保证BF满足条件要OK,由于后面的操作建立在前面的操作之上只要保证了每一步操作的可靠性,那么BF的正确性就是可以保障的。这就要求对于每一个插入和删除操作,要保证类似于数据库那样的ACID特性中的一致性,类比数据库,只要一步操作出错,就算后面的操作是正确的,数据库也没法正常使用了。

    创建结点:

    template<class T, class N>
    N* AVLTree<T, N>::New()
    {
        N* newp = BiTree<T, N>::New();
        newp->BF = 0;
        return newp;
    }

    按数组插入:

    template<class T, class N>
    AVLTree<T, N>::AVLTree(const vector<T>& s)
    {
        if (s.empty()) throw "数组不能为空";
        root = new N;
        root->data = s[0];
        root->BF = 0;
        root->lchild = NULL;
        root->rchild = NULL;
        for (int i = 1; i < s.Length(); i++) Insert(s[i]);
    }

    树的旋转

    1)LL

    template<class T, class N>
    inline TStatus AVLTree<T, N>::RotateLL(N *&p, N *&parent, N *ancestor)
    {
        ////                                      A(parent,root),BF(2)
        //                                      |
        //                  B(p),BF(1)__________|_____AR
        //                  |
        //        BL(X)_____|_____BR
    
        ////                  B(p,root),BF(0)
        //                  |
        //        BL(X)_____|___________________A(parent),BF(0)
        //                                      |
        //                               BR_____|_____AR
    
        bool descent = !parent->rchild;
        this->RotateRight(p, parent, ancestor);
    
        parent->BF = p->BF == 1 ? 0 : 1;
        p->BF--;
        N *swap; swap = p; p = parent; parent = swap;
        return (ancestor && descent) ? parent == ancestor->lchild ? T_DECL : T_DECR : T_OK;
    }

    2)RR

    template<class T, class N>
    inline TStatus AVLTree<T, N>::RotateRR(N *&p, N *&parent, N *ancestor)
    {
        ////                  A(parent,root),BF(-2)
        //                  |
        //           AL_____|___________________|
        //                                      B(p),BF(-1)
        //                               BL_____|_____BR(X)
    
        ////                                      B(p,root),BF(0)
        //                                      |
        //                  A(parent),BF(0)_____|_____BR(X)
        //                  |
        //           AL_____|_____BL
    
        bool descent = !parent->lchild;
        this->RotateLeft(p, parent, ancestor);
    
        parent->BF = p->BF == -1 ? 0 : -1;
        p->BF++;
        N *swap; swap = p; p = parent; parent = swap;
        return (ancestor && descent) ? parent == ancestor->lchild ? T_DECL : T_DECR : T_OK;
    }

    3)LR

    template<class T, class N>
    inline TStatus AVLTree<T, N>::RotateLR(N *&p, N *&parent, N *ancestor)
    {
        //////////////////////////////////////////////////////////////////////////
    
        // >>>> 情况一
    
        ////                                      A(parent,root),BF(2)
        //                                      |
        //                  B(p),BF(-1)_________|_____AR
        //                  |
        //           BL_____|_____C(pc),BF(1)
        //                        |
        //             CL(X)______|______CR
    
        // 后(减少一层)
        //                                C(pc,root),BF(0)
        //                                |
        //                  B(p),BF(0)____|_____A(parent),BF(-1)
        //                  |                   |
        //           BL_____|_____CL(x)  CR_____|_____AR
    
        //////////////////////////////////////////////////////////////////////////
    
        // >>>> 情况二
    
        ////                                      A(parent,root),BF(2)
        //                                      |
        //                  B(p),BF(-1)_________|_____AR
        //                  |
        //           BL_____|_____C(pc),BF(-1)
        //                        |
        //                CL______|______CR(X)
    
        // 后(减少一层)
        //                                C(pc,root),BF(0)
        //                                |
        //                  B(p),BF(1)____|_____A(parent),BF(0)
        //                  |                   |
        //           BL_____|_____CL  CR(x)_____|_____AR
    
        N *pc = p->rchild;
        this->RotateLeft(pc, p, parent);
        this->RotateRight(pc, parent, ancestor);
    
        if (pc->BF == 1)
        {
            p->BF = 0;
            parent->BF = -1;
            pc->BF = 0;
        }
        else if (pc->BF == -1)
        {
            p->BF = 1;
            parent->BF = 0;
            pc->BF = 0;
        }
        else
        {
            parent->BF = 0;
            p->BF = 0;
        }
        parent = pc;
        return ancestor ? parent == ancestor->lchild ? T_DECL : T_DECR : T_OK;
    }

    4)RL

    template<class T, class N>
    inline TStatus AVLTree<T, N>::RotateRL(N *&p, N *&parent, N *ancestor)
    {
        //////////////////////////////////////////////////////////////////////////
    
        // >>>> 情况一
    
        ////                  A(parent,root),BF(-2)
        //                  |
        //           AL_____|___________________B(p),BF(1)
        //                                      |
        //                        C(pc),BF(-1)__|______BR
        //                        |
        //                 CL_____|_____CR(X)
    
        // 后(减少一层)
        //                                 C(pc,root),BF(0)
        //                                 |
        //                  A(parent),BF(1)|_____B(p),BF(0)
        //                  |                    |
        //           AL_____|_____CL   CR(x)_____|_____BR
    
        //////////////////////////////////////////////////////////////////////////
    
        // >>>> 情况二
    
        ////                  A(parent,root),BF(-2)
        //                  |
        //           AL_____|___________________B(p),BF(1)
        //                                      |
        //                        C(pc),BF(1)___|______BR
        //                        |
        //              CL(X)_____|_____CR
    
        // 后(减少一层)
        //                                 C(pc,root),BF(0)
        //                                 |
        //                  A(parent),BF(0)|_____B(p),BF(-1)
        //                  |                    |
        //           AL_____|_____CL(x)   CR_____|_____BR
    
        N *pc = p->lchild;
        this->RotateRight(pc, p, parent);
        this->RotateLeft(pc, parent, ancestor);
    
        if (pc->BF == -1)
        {
            parent->BF = 1;
            p->BF = 0;
            pc->BF = 0;
        }
        else if (pc->BF == 1)
        {
            parent->BF = 0;
            p->BF = -1;
            pc->BF = 0;
        }
        else
        {
            parent->BF = 0;
            p->BF = 0;
        }
        parent = pc;
        return ancestor ? parent == ancestor->lchild ? T_DECL : T_DECR : T_OK;
    }

    5)左旋

    template<class T, class N>
    void BALTree<T, N>::RotateLeft(N *p, N *parent, N *ancestor)
    {
        ////                  A(parent,root)
        //                  |
        //           AL_____|___________________|
        //                                      B(p)
        //                               BL_____|_____BR(X)
    
        ////                                      B(p,root)
        //                                      |
        //                  A(parent)___________|_____BR(X)
        //                  |
        //           AL_____|_____BL
    
        parent->rchild = p->lchild;
        p->lchild = parent;
    
        if (ancestor != NULL)
        {
            if (ancestor->lchild == parent) // 修改p的双亲为ancestor
                ancestor->lchild = p;
            else
                ancestor->rchild = p;
        }
        else
        {
            this->root = p;
        }
    }

    6)右旋

    template<class T, class N>
    void BALTree<T, N>::RotateRight(N *p, N *parent, N *ancestor)
    {
        ////                                      A(parent,root)
        //                                      |
        //                  B(p)________________|_____AR
        //                  |
        //        BL(X)_____|_____BR
    
        ////                  B(p,root)
        //                  |
        //        BL(X)_____|___________________A(parent)
        //                                      |
        //                               BR_____|_____AR
    
        parent->lchild = p->rchild;
        p->rchild = parent;
    
        if (ancestor != NULL)
        {
            if (ancestor->lchild == parent) // 修改p的双亲为ancestor
                ancestor->lchild = p;
            else
                ancestor->rchild = p;
        }
        else
        {
            this->root = p;
        }
    }

    结点的插入

    对于AVL,结点的插入是一项复杂的工作。由于插入操作,树原本的平衡被破坏,需要重新调整一次。

    基本步骤是:

    1. 二分查找,找到相应的空位置,并插入(若已存在则忽略),同时存储查找路径
    2. 从路径逆着向上找到第一个最小不平衡子树的根结点的父结点,接下来就是对这个子树进行调整
    3. 调整子树,有LL、LR、RL、RR四种情况

    1)添加结点

    template<class T, class N>
    bool BALTree<T, N>::Insert(T data)
    {
        N* newp;
    
        if (this->root == NULL)
        {
            newp = this->New();
            newp->data = data;
            this->root = newp;
            this->HandleRoot();
            return true;
        }
    
        stack<N*> path; // 存储插入前的查找路径(便于回溯)
    
        //////////////////////////////////////////////////////////////////////////
        // 插入操作
        N *p = this->root;
        while (true)
        {
            path.push(p);
            if (data < p->data) // 插入值小于根结点,入左子树
            {
                if (p->lchild != NULL)
                {
                    p = p->lchild; // 值小于LL,则递归入L
                }
                else
                {
                    newp = this->New();
                    newp->data = data;
                    path.push(newp);
                    p->lchild = newp;
                    break; // 根结点无左孩子,正好插入
                }
            }
            else if (data > p->data) // 插入值大于根结点,入右子树
            {
                if (p->rchild != NULL)
                {
                    p = p->rchild; // 值大于RR,则递归入R
                }
                else
                {
                    newp = this->New();
                    newp->data = data;
                    path.push(newp);
                    p->rchild = newp;
                    break; // 根结点无右孩子,正好插入
                }
            }
            else // 插入值等于根结点,返回
            {
                return false;
            }
        }
    
        // 插入完毕
    
        //////////////////////////////////////////////////////////////////////////
    
        // 调整插入路径上的结点
        BalanceAfterInsert(path);
        return true;
    }

    2)插入后的平衡

    template<class T, class N>
    void BALTree<T, N>::BalanceAfterInsert(stack<N*>& path)
    {
        N *child = NULL;  // *child作为*p的孩子结点
        N *p = path.top();
        path.pop();
        N *parent = path.top(); // *parent是*p的父结点
        path.pop();
        N *ancestor;
        while (true)
        {
            ancestor = path.empty() ? NULL : path.top();
            TStatus status = CheckPathAfterInsert(child, p, parent, ancestor);
            switch (status)
            {
                case T_OK:
                    return;
                case T_BREAK:
                    BalanceInternalAfterInsert(child, p, parent, ancestor);
                    return;
                case T_CONTINUE:
                    break;
            }
    
            if (path.empty())
                return;
    
            child = p;
            p = parent;
            parent = path.top(); // 由path向上回溯
            path.pop();
        }
    }

    3)检查平衡

    template<class T, class N>
    TStatus AVLTree<T, N>::CheckPathAfterInsert(N *child, N *p, N *parent, N *ancestor)
    {
        if (parent->lchild == p) // p是parent的左孩子,那么parent的BF++
            parent->BF++;  // BF的变化:-1->0->1->2
        else // p是parent的右孩子,那么parent的BF--
            parent->BF--;  // BF的变化:1->0->-1->-2
    
        if (parent->BF == 2 || parent->BF == -2) // *parent是失衡结点(第一个|BF|>=2)
            return T_BREAK; // 找到最小不平衡子树的根结点
    
        if (parent->BF == 0) // 在插入新结点后,*parent的左右子树高度相等(BF为0),说明以*parent为根的子树高度未增加
            return T_OK;       // 所以路径中的其余祖先结点无需调整BF
    
        return T_CONTINUE;
    }

    4)调整平衡

    template<class T, class N>
    TStatus AVLTree<T, N>::BalanceInternalAfterInsert(N *child, N *p, N *parent, N *ancestor)
    {
        //////////////////////////////////////////////////////////////////////////
    
        if (parent->BF == 2 && p->lchild == child)    // LL
        {
            RotateLL(p, parent, ancestor);
        }
    
        //////////////////////////////////////////////////////////////////////////
    
        else if (parent->BF == -2 && p->rchild == child)    // RR
        {
            RotateRR(p, parent, ancestor);
        }
    
        //////////////////////////////////////////////////////////////////////////
    
        else if (parent->BF == 2 && p->rchild == child)    // LR
        {
            RotateLR(p, parent, ancestor);
        }
    
        //////////////////////////////////////////////////////////////////////////
    
        else if (parent->BF == -2 && p->lchild == child)    // RL
        {
            RotateRL(p, parent, ancestor);
        }
    
        //////////////////////////////////////////////////////////////////////////
    
        return T_OK;
    }

    调整过程全部是旋转操作

    结点的删除

    结点的删除比插入要复杂得多。删除某一结点后,可能需要多次调整。

    基本步骤是:

    1. 二分查找,找到要删除的结点,并用最近的结点替换掉它,然后删除它,调整BF
    2. 从路径逆着向上找到第一个最小不平衡子树的根结点的父结点,接下来就是对这个子树进行调整
    3. 调整子树,有LL、LR、RL、RR四种情况,如果调整后子树高度下降(即父结点BF改变),那么跳到步骤2

    1)定位结点

    template<class T, class N>
    bool BALTree<T, N>::Delete(T data)
    {
        if (this->root == NULL)
        {
            return false;
        }
    
        stack<N*> path; // 存储删除前的查找路径(便于回溯)
    
        //////////////////////////////////////////////////////////////////////////
        // 查找操作
        N *p = this->root;
        while (true)
        {
            path.push(p);
            if (data < p->data) // 插入值小于根结点,入左子树
            {
                if (p->lchild != NULL)
                {
                    p = p->lchild; // 值小于LL,则递归入L
                }
                else
                {
                    return false; // 没找到
                }
            }
            else if (data > p->data) // 插入值大于根结点,入右子树
            {
                if (p->rchild != NULL)
                {
                    p = p->rchild; // 值大于RR,则递归入R
                }
                else
                {
                    return false; // 没找到
                }
            }
            else // 找到
            {
                break;
            }
        }
    
        // 定位成功
    
        //////////////////////////////////////////////////////////////////////////
    
        // 调整删除路径上的结点
        BalanceAfterDelete(path);
        return true;
    }

    2)删除结点

    template<class T, class N>
    void BALTree<T, N>::BalanceAfterDelete(stack<N*>& path)
    {
        // 替代结点
        Replace(path);
    
        if (path.empty())
            return;
    
        // 删除结点并平衡
    
        N *p = NULL;
        N *child = NULL;  // *child作为*p的孩子结点
        N *parent = path.top(); // *parent是*p的父结点
        path.pop();
        N *ancestor;
        TStatus status = T_OK;
        while (true)
        {
            ancestor = path.empty() ? NULL : path.top();
            if (status == T_CONTINUE_STATUS)
                status = ancestor ? parent == ancestor->lchild ? T_DECL : T_DECR : T_OK;
            status = CheckPathAfterDelete(child, p, parent, ancestor, status);
            switch (status)
            {
            case T_OK:
                return;
            case T_BREAK:
                status = BalanceInternalAfterDelete(child, p, parent, ancestor);
                if (status == T_OK)
                    return;
                break;
            case T_CONTINUE:
                status = T_OK;
                break;
            case T_CONTINUE_STATUS:
                break;
            }
    
            if (path.empty())
                return;
    
            child = p;
            p = parent;
            parent = path.top(); // 由path向上回溯
            path.pop();
        }
    }

    3)替换结点

    删除结点前,先将结点与其他结点进行替换,使得对于结点的删除转变为对于叶子结点的删除。

    template<class T, class N>
    void AVLTree<T, N>::Replace(stack<N*>& path)
    {
        N *p = path.top();
        path.pop();
        N *parent = path.empty() ? NULL : path.top();
        if (!p->lchild && !p->rchild) // p为叶子结点,直接删除
        {
            if (!parent) // 根结点
            {
                this->root = NULL;
            }
            else if (parent->lchild == p)
            {
                parent->lchild = NULL;
                parent->BF--;
            }
            else
            {
                parent->rchild = NULL;
                parent->BF++;
            }
        }
        else if (p->lchild && p->rchild) // 双子树,转化为单子树
        {
            // 注意:替换时,BF也要替换
            if (p->lchild->rchild) // 替换左子树最右结点
            {
                vector<N*> new_path;
                new_path.push_back(p->lchild);
                N *lr = p->lchild->rchild;
                while (lr)
                {
                    new_path.push_back(lr);
                    lr = lr->rchild;
                }
                path.push(new_path.back());
                lr = path.top();
                new_path.pop_back();
                for (vector<N*>::iterator it = new_path.begin(); it != new_path.end(); it++)
                {
                    path.push(*it);
                }
                lr->BF = p->BF;
                if (!parent) // 根结点
                    this->root = lr;
                else if (parent->lchild == p)
                    parent->lchild = lr;
                else
                    parent->rchild = lr;
                path.top()->rchild = lr->lchild;
                path.top()->BF++;
                lr->lchild = p->lchild;
                lr->rchild = p->rchild;
            }
            else if (p->rchild->lchild) // 替换右子树最左结点
            {
                vector<N*> new_path;
                new_path.push_back(p->rchild);
                N *rl = p->rchild->lchild;
                while (rl)
                {
                    new_path.push_back(rl);
                    rl = rl->lchild;
                }
                path.push(new_path.back());
                rl = path.top();
                new_path.pop_back();
                for (vector<N*>::iterator it = new_path.begin(); it != new_path.end(); it++)
                {
                    path.push(*it);
                }
                rl->BF = p->BF;
                if (!parent) // 根结点
                    this->root = rl;
                else if (parent->lchild == p)
                    parent->lchild = rl;
                else
                    parent->rchild = rl;
                path.top()->lchild = rl->rchild;
                path.top()->BF--;
                rl->lchild = p->lchild;
                rl->rchild = p->rchild;
            }
            else // 替代孩子
            {
                if (!parent) // 根结点
                {
                    if (this->root->BF != -1)
                    {
                        this->root = p->lchild;
                        this->root->rchild = p->rchild;
                        this->root->BF = p->BF - 1;
                    }
                    else
                    {
                        this->root = p->rchild;
                        this->root->lchild = p->lchild;
                        this->root->BF = p->BF + 1;
                    }
                }
                else if (parent->lchild == p)
                {
                    if (parent->BF != -1)
                    {
                        parent->lchild = p->lchild;
                        parent->lchild->rchild = p->rchild;
                        parent->lchild->BF = p->BF - 1;
                    }
                    else
                    {
                        parent->lchild = p->rchild;
                        parent->lchild->lchild = p->lchild;
                        parent->lchild->BF = p->BF + 1;
                    }
                    path.push(parent->lchild);
                }
                else
                {
                    if (parent->BF != -1)
                    {
                        parent->rchild = p->lchild;
                        parent->rchild->rchild = p->rchild;
                        parent->rchild->BF = p->BF - 1;
                    }
                    else
                    {
                        parent->rchild = p->rchild;
                        parent->rchild->lchild = p->lchild;
                        parent->rchild->BF = p->BF + 1;
                    }
                    path.push(parent->rchild);
                }
            }
        }
        else if (p->lchild) // 左单子树
        {
            if (!parent) // 根结点
            {
                this->root = p->lchild;
            }
            else
            {
                if (parent->lchild == p)
                {
                    parent->lchild = p->lchild;
                    parent->BF--;
                }
                else
                {
                    parent->rchild = p->lchild;
                    parent->BF++;
                }
            }
        }
        else // 右单子树
        {
            if (!parent) // 根结点
            {
                this->root = p->rchild;
            }
            else
            {
                if (parent->lchild == p)
                {
                    parent->lchild = p->rchild;
                    parent->BF--;
                }
                else
                {
                    parent->rchild = p->rchild;
                    parent->BF++;
                }
            }
        }
        delete p;
    }

    4)检查平衡

    template<class T, class N>
    TStatus AVLTree<T, N>::CheckPathAfterDelete(N *child, N *p, N *parent, N *ancestor, TStatus status)
    {
        if (status == T_OK)
        {
            if (p && p->BF == 0)
            {
                if (parent->lchild == p) // p是parent的左孩子,p的BF为0,说明p层数减少一,故parent的BF--
                    parent->BF--;
                else // p是parent的右孩子,p的BF为0,说明p层数减少一,故parent的BF++
                    parent->BF++;
            }
        }
        else
        {
            if (status == T_DECL) //平衡后,子树高度减少,那么要向上检查平衡性
            {
                parent->BF--;
                return parent->BF == 0 ? T_CONTINUE_STATUS : T_OK;
            }
            else if (status == T_DECR) //平衡后,子树高度减少,那么要向上检查平衡性
            {
                parent->BF++;
                return parent->BF == 0 ? T_CONTINUE_STATUS : T_OK;
            }
        }
    
        if (parent->BF == 2 || parent->BF == -2) // *parent是失衡结点(第一个|BF|>=2)
            return T_BREAK; // 找到最小不平衡子树的根结点
    
        if (parent->BF != 0) // 在删除结点后,*parent的左右子树高度差绝对值为1(|BF|为1),说明以*parent为根的子树高度未改变
            return T_OK;       // 所以路径中的其余祖先结点无需调整BF
    
        return T_CONTINUE;
    }

    5)调整平衡

    template<class T, class N>
    TStatus AVLTree<T, N>::BalanceInternalAfterDelete(N *child, N *p, N *parent, N *ancestor)
    {
        //////////////////////////////////////////////////////////////////////////
    
        if (parent->BF == 2 && (!p || parent->rchild == p))    // L
        {
            p = parent->lchild;
    
            if (p->BF == -1) // LR
            {
                return RotateLR(p, parent, ancestor);
            }
            else // LL
            {
                return RotateLL(p, parent, ancestor);
            }
        }
    
        //////////////////////////////////////////////////////////////////////////
    
        else if (parent->BF == -2 && (!p || parent->lchild == p))    // R
        {
            p = parent->rchild;
    
            if (p->BF == 1) // RL
            {
                return RotateRL(p, parent, ancestor);
            }
            else // RR
            {
                return RotateRR(p, parent, ancestor);
            }
        }
    
        //////////////////////////////////////////////////////////////////////////
        return T_OK;
    }

    总结

    虽然算法的实现思路很简单,但实际要考虑很多因素,稍微一个小错误都可能导致结果不一致的现象(BF未调整)。然而,在理解AVL上的基础上,理解红黑树就不那么困难了。

    相比较AVL的递归算法而言,可以看出,递归算法是具有简洁美的,这是由二叉树的递归性质决定的。

    由于AVL操作的复杂性,因而不常用AVL,现在主要使用红黑树(RBT),它们的区别是:

    • AVL讲究整体平衡,因而要求严格、操作复杂 ,子树高度差不大于1
    • RBT追求局部平衡,因而实现较AVL简单,最长路径比上最短路径小于2
  • 相关阅读:
    常用数据结构的应用场景
    数组与链表的对比
    [LeetCode 293] Flip Game
    [Leetcode] Palindrome Permutation 回文变换
    九大排序算法再总结
    query函数的可查询数据
    Column常用的参数
    sqlalchemy的常用字段
    sqlalchemy基本的增删改查
    sqlalchemy映射数据库
  • 原文地址:https://www.cnblogs.com/bajdcc/p/4802834.html
Copyright © 2020-2023  润新知