关联式容器

STL关联式容器分为set和map两大类,以及这两大类的衍生体multiset和multimap,这些容器的底层机制均为rb-tree。rb-tree也是一个独立容器,但并不开放给外界使用。SGI STL还提供了一个不在标准规格之列的关联式容器hash table,以及以此hashtable为底层机制的hash_set,hash_map,hash_miltimap和hash_multiset.

关联式容器,每笔数据都有一个键值(key)和一个实值(value),当元素被插入到关联式容器中时,容器内部结构便依照其键值大小,以某种特定规则将这个元素放置于适当位置。set的键值就是实值,map的键值可以和实值分开,并形成一种映射关系,所以map被称为映射表。关联式容器没有所谓头尾(只有最大值和最小值),所以不提供push_back(),push_front(),end()等操作行为。

关联式容器的内部是一个平衡二叉树,平衡二叉树有很多种类型:AVL-tree, RB-tree,AA-tree(RB-tree的变体),在STL中使用的是RB-tree。

根节点至任何节点之间有唯一路径,路径所经过的边数,称为路径长度。根节点至任何节点的路径长度,即该节点的深度。根节点的深度是0.某节点至其最深子节点(叶节点)的路径长度,称为该节点的高度。任何节点的大小是指其所有子代(包括自己)的节点总数。

二叉搜索树

二叉搜索树的设计原则是:任何节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值。

平衡二叉搜索树

可能因为输入值不够随机,或者某些插入和删除操作,二叉搜索树可能会失去平衡,造成搜索效率低下的情况。
平衡树和不平衡树

树的平衡,没有一个绝对的测量标准。大致意义是:没有任何一个节点过深。不同的平衡条件,造就出不同的效率表现,以及不同的实现复杂度。

AVL tree(Adelson-Velskii-Landis tree)

有额外平衡条件(任何节点的左右子树的高度相差不超过1)的二叉搜索树,确保二叉树的高度为O(logN),保证对数深度平衡状态。

实现

节点的定义

template<class K, class V>
struct AVLTreeNode{
    pair<K,V> _kv;   //存储键值对,键为值的索引用于比较
    int _bf;     //平衡因子
    AVLTreeNode* _left;   //左孩子节点
    AVLTreeNode* _right;   //右孩子节点
    AVLTreeNode* _parent;  //父节点

    AVLTreeNode(const pair<K,V>& val = pair<K,V>()):_kv(val), _bf(0), left(nullptr), _right(nullptr), _parent(nullptr){}          //默认构造函数
};

新节点插入操作

在新节点插入时,和普通二叉搜索树的插入规则一致(小于根节点,置于根节点的左子树;大于根节点,置于根节点的右子树)。

template<class K, class V>
bool insert(const pair<K,V>& kv){
    if(_root == nullptr){  //树为空,则插入进来的节点为根节点
        _root = new AVLTreeNode(kv);
        return true;
    }

    AVLTreeNode* parent = nullptr;
    AVLTreeNode* cur = _root;
    while(cur){         
        if (cur->_kv.first > kv.first){  //小于当前节点
            parent = cur;
            cur = cur->_left;
        }
        else if (cur->_kv.first < kv.first){     //大于当前节点
            parent = cur;
            cur = cur->_right;
        }
        else{         //存在该节点,则退出
            return false;
        }
    }

    cur = new AVLTreeNode(kv);
    if (parent->_kv.first < kv.first){
        parent->_right = cur;
    }
    else{
        parent->_left = cur;
    }
    cur->_parent = praent;

    //判断插入之后是否导致不平衡
}

但是,插入某节点后,AVL tree的平衡可能被破坏,分为两种情况:

  1. 外侧插入
  • 该插入节点E位于A的左节点的左子树

    ll

  • 该插入节点E位于A的右节点的右子树

    rr

  1. 内侧插入
  • 该插入节点F位于A的右节点的左子树

    rl

  • 该插入节点F位于A的左节点的右子树

    lr

情况1需要单旋

  • 节点A作为根节点的树中,左子树比右子树高的多,我们需要把右子树变高,还要保持二叉搜索树的性质(右大于左)。只能把根节点A移下去作为右子树的一部分,根节点的左孩子B移上去作为根节点。这是A节点(大于B)就必须为B的右子树,B原本的右子树(大于B小于A)可以为A的左子树。也称为右旋(对于节点A来说就是向右旋转)。

rrotate

  • 左旋同理,只不过调整了指向。

lrotate

情况2需要双旋

  • 依然是需要把根节点A移下去作为左子树的一部分,但是不能对节点C进行左旋。因为节点C有左孩子,左旋结束后,节点A只能为节点C左孩子的孩子,这样又会导致不平衡。节点F位于A的右节点的左子树,故A的右节点的左子树较高,我们可以直接选取节点D为整棵树的根节点,节点A必须为节点D的左孩子,节点D原本的左孩子为节点A的右孩子;节点C必须为节点D的右孩子,节点D原本的右孩子为节点D的左孩子。

rlrotate

可以分为两个步骤,先把节点C右旋,再把节点A左旋。

  • 与上述类似,只不过调整了指向。

右单旋

void RotateR(AVLTreeNode* parent){   //传入A节点
    AVLTreeNode* subL = parent->_left;   //得到B节点
    AVLTreeNode* subLR = subL->_right;      //得到B的右孩子

    //旋转过程
    parent->_left = subLR;    //B的右孩子变为A的左孩子
    if(subLR){       //若B的右孩子不为空,则必须改变父节点指向
        subLR->_parent = parent;   
    }

    AVLTreeNode* ppNode = parent->_parent;   //得到节点A的父节点
    subL->_right = parent;  //B节点的右孩子指向A
    parent->_parent = subL;  //改变A节点的父节点指向

    if(ppNode){   //节点A只是这个子树的根节点,他还有父节点
        if(parent = ppnode->_left){
            ppnode->_left = subL;
        }else{
            ppnode->_right = subL;
        }
        subL->_parent = ppNode;
    }else{   //节点A为整棵树的根节点,父节点为空
        root = subL;   //设置节点B为整棵树的根节点
        subL->_parent = nullptr;
    }

    //调整平衡因子,因为只有A和B的相对位置发生改变
    subL->_bf = parent->_bf = 0;
}

左单旋

void RotateL(AVLTreeNode* parent){
    AVLTreeNode* subR = parent->_right;
    AVLTreeNode* subRL = subR->_left;
    parent->_right = subRL;

    if (subRL){
        subRL->_parent = parent;
    }

    AVLTreeNode* ppNode = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    if (ppNode){
        if (parent == ppNode->_left){
            ppNode->_left = subR;
        }else{
            ppNode->_right = subR;
        }
        subR->_parent = ppNode;
    }else{
        _root = subR;
        subR->_parent = nullptr;
    }

    subR->_bf = parent->_bf = 0;
}

右左双旋

根据我们的分析,双旋需要进行3个节点(D,C,A)相对位置的变动,我们需要调节这个三个节点的平衡因子。由于第一次旋转时,D和C的状况有多种,就对应了调整后多种不同的平衡因子(右子树高度-左子树高度)情况:

  1. 当B,E,F都不存在,这时D为新插入的节点,D的bf为0,C的为-1,A的为2.调整之后,都为0.
  2. 当为上述图中的情况,F为新插入的节点,D的bf为-1,C的为-1,A的为-1,调整之后,D的bf为0,A为0,C为1
  3. 当F为D的右孩子时,F为新插入的节点,D的bf为1,C的为-1,A的为-1,调整之后,D的bf为0,A为-1,C为0

为什么以D节点为标准呢?因为每次旋转都要安置D的左右孩子,这样就会引起其他两个节点的平衡因子变化。

void RotateRL(AVLTreeNode* parent){   //传入A节点
    AVLTreeNode* subR = parent->_right;  //得到C节点
    AVLTreeNode* subRL = subR->_left;      //得到D节点
    int bf = subR->_bf;      //得到D的bf

    //双旋
    RotateR(subR);    //对C右旋
    RotateL(parent);   //对A左旋

    //调整平衡因子
     if (bf == 0){
        parent->_bf = 0;
        subRL->_bf = 0;
        subR->_bf = 0;
    }
    else if (bf == -1){
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    else if (bf == 1){
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }

}

左右双旋

void RotateLR(AVLTreeNode* parent){
    AVLTreeNode* subL = parent->_left;
    AVLTreeNode* subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(parent->_left);
    RotateR(parent);

    if (bf == 0){
        parent->_bf = 0;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if (bf == -1){
        parent->_bf = 1;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if (bf == 1){
        parent->_bf = 0;
        subL->_bf = -1;
        subLR->_bf = 0;
    }
}

接下来的insert函数

template<class K, class V>
bool insert(const pair<K,V>& kv){
    //...
    //判断插入之后是否导致不平衡,不平衡则旋转
    while(parent){
        if(cur == parent->_left){   //父节点的左子树高度加1,平衡因子减一
            parent->bf--;
        }else{
            parent->bf++;
        }

        if(parent->_bf == 0){  //父节点的平衡因子由原来的-1/1变为0,总体高度不变,只是给高度较小的子树的高度加1.对总体的平衡因子也没有影响
            break;
        }else if(parent->_bf == 1 || parent->_bf == -1){  //原本父节点的左右子树高度是相同的,插入该节点后,改变了以父节点为根节点的子树的高度,可能会影响整棵树,需要向上遍历父节点
            cur = parent;
            parent = parent->_parent;
        }else if(parent->_bf == -2 || parent->_bf == 2){
            //这时的cur不再是插入的节点,而是对应的节点B
            if(parent->_bf == -2 && cur->_bf == -1){
                RotateR(parent);
            }else if(parent->_bf == 2 && cur->_bf == 1){
                RotateL(parent);
            }else if(parent->_bf == 2 && cur->_bf == -1){
                RotateRL(parent);
            }else if(parent->_bf == -2 && cur->_bf == 1){
                RotateLR(parent);
            }
            //该处调整后,所有的都满足条件,可以数学证明
            break;
        }
    }
    return true;
}

删除操作

和普通二叉搜索树的删除流程一样,但是删除之后需要检测平衡因子,若不满足条件,则调整。

删除的节点分为三种情况:

  1. 叶子节点,直接删除
  2. 只有一个孩子节点,删除该节点,该节点的父节点指向其孩子节点
  3. 有两个孩子节点,找到待删除节点的前驱或后继节点(叶子节点),替换值,就转换为删除找到的节点。在这里要进行一步特殊处理,需要判断该节点的哪个子树比较高,若左子树高,则找前驱节点;否则,找后继节点。这样处理后,删除前驱或后继节点后,不会导致以原删除节点为根的树的平衡因子的改变。

delete函数

AVLTreeNode* _remove(AVLTreeNode* node, const K& val){   //传入根节点和待删除节点的索引key
    if(node == nullptr){   
        return nullptr;
    }

    if(node->_kv.first > val){  //该节点位于根节点的左子树
        node->_left = _remove(node->_left, val);  //删除该节点

        node->_bf = Depth(node->_right) - Depth(node->_left);   //调整平衡因子,原文放在倒数第二行
        //判断平衡因子,决定是否旋转(可以对比插入操作)
        if(abs(node->_bf) > 1){
            if(Depth(node->_right->_right) >= Depth(node->_right->_left)){ //右孩子的右子树高
                RotateL(node);
            }else{    //右孩子的左子树高
                RotateRL(node);
            }
        }          
    }else if(node->_kv.first < val){
        node->_right = _remove(node->_right, val);  //删除该节点

        //判断平衡因子,决定是否旋转(可以对比插入操作)
        if(abs(node->_bf) > 1){
            if(Depth(node->_left->_left) >= Depth(node->_left->_right)){ //左孩子的左子树高
                RotateR(node);
            }else{    //左孩子的右子树高
                RotateLR(node);
            }
        }          
    }else{    //定位到删除节点
        if(node->_left != nullptr && node->_right != nullptr){ //有两个孩子
            if(Depth(node->_left) >= Depth(node->_right)){  //判断哪棵树更高
                AVLTreeNode* prev = node->_left;
                while (prev->_right)
                    prev = prev->_right;  //定位到前驱节点
                node->_kv.val = prev->_kv.val;   //原文是first
                node->_left = _remove(node->_left, prev->_kv.first);  //转为删除前驱节点
            }else{
                AVLTreeNode* post = node->_right;
                while (post->_left)
                    post = post->_left;
                node->_kv.val = prev->_kv.val;   
                node->right = _remove(node->_right, post->_kv.first);
            }
        }else{  //只有一个孩子节点或者没有孩子节点
            if(node->_left != nullptr){   //直接删除该节点,返回孩子节点
                AVLTreeNode* left = node->_left;
                delete node;
                return left;
            }else if(node->_right != nullptr){
                AVLTreeNode* right = node->_right;
                delete node;
                return right;
            }else{
                return nullptr;
            }
        }
    }

    //node->_bf = Depth(node->_right) - Depth(node->_left);   
    return node;
}

AVL tree查询非常有效率,但是插入和删除操作时需要进行很多复杂的操作维持树的平衡,所以对于不经常变动的数据可以使用该结构。

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top