【C++】set和map的底层AVL树的实现(下)

简介: 【C++】set和map的底层AVL树的实现(下)

下面我们再看h==1的情况:

93ce893840884ae797211326754ccf69.png39a0d19d39f64c9a8e5875cdc132065a.png



h==1有两种情况,要不在60的左树插入,要不在60的右树插入,不同的是如果插入在60的左树那么parent的平衡因子会变成1,其他两个节点的平衡因子还是0.当插入的节点在60的右边,那么只有subL的平衡因子变成-1,其他的平衡因子变成0,下面我们写先左旋再右旋的代码:


void RotateLR(Node* parent)
  {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;
    RotateL(parent->_left);
    RotateR(parent);
    if (bf == 1)
    {
    parent->_bf = 0;
    subL->_bf = -1;
    subLR->_bf = 0;
    }
    else if (bf == -1)
    {
    parent->_bf = 1;
    subL->_bf = 0;
    subLR->_bf = 0;
    }
    else if (bf == 0)
    {
    parent->_bf = 0;
    subL->_bf = 0;
    subLR->_bf = 0;
    }
    else
    {
    assert(false);
    }
  }

首先我们要记住,要满足先左旋再右旋的条件,一定是这棵树或者这颗子树的整体高度是左边高度高,并且左边的第一个节点的右边高度高,因为我们先左旋就是将这个左边第一个节点的右边左旋(因为右边高度高),这个时候整体都变成了纯粹的左边高,如果是纯粹的左边高我们再使用右旋,所以这就是先左旋再右旋的由来。条件一定是:parent的平衡因子为-2(代表整体左边高),cur的平衡因子为1(代表左边的第一个节点右边高度高)。首先我们保存parent的左边节点也就是subL,然后保存这个左边节点的右边节点subLR,并且我们要记录这个subLR的平衡因子,因为我们前面说过只有这个位置插入才会发生旋转,并且插入到这个位置的左树还是右树后面的平衡因子都是不一样的,所以我们先保存这个节点的平衡因子,先左旋parent的左边节点后,再整体右旋,然后判断(用平衡因子判断)新增的节点在subLR的左边还是右边,如果为1说明在右边,我们要将平衡因子更新,这里要对照着图,发现subL的平衡因子变成-1,如果为-1说明在左边parent的平衡因子变成1,如果等于0说明自己就是新增的节点,然后我们的先左旋再右旋就结束了。


下面是先右旋再左旋:


04598c22f97748a2bf51c5ee9982fa5c.png


同样我们先以h==0为例,如果h==0那么60就是新增的节点,这个时候旋转完后subRL,subR和parent的平衡因子都为0.


be642819e415460e999d392736b2fe4b.png


当h==1同样有两种,其实也就是从b插入或者从c插入,在这两个位置都会引发双旋,只不过现在h==1无b,c这两个位置而已,在60的左边插入和60的右边插入结果都是不一样的,下面我们直接看代码:


void RotateRL(Node* parent)
  {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;
    RotateR(parent->_right);
    RotateL(parent);
    if (bf == 1)
    {
    parent->_bf = -1;
    subR->_bf = 0;
    subRL->_bf = 0;
    }
    else if (bf == -1)
    {
    parent->_bf = 0;
    subR->_bf = 1;
    subRL->_bf = 0;
    }
    else if (bf == 0)
    {
    parent->_bf = 0;
    subR->_bf = 0;
    subRL->_bf = 0;
    }
    else
    {
    assert(false);
    }
  }


与先左旋再右旋不一样的地方在于平衡因子的要选择的节点,因为要满足先右旋再左旋的条件,所以这棵树一定是整体右边高度高,右边的第一个节点左边高度高,也就是parent的平衡因子为2(说明整体右边高)并且cur的平衡因子为-1(说明右边第一个节点的左边高度高)。我们保存右边第一个节点和右边第一个节点的左边节点,然后保存subRL的平衡因子,然后先右旋parent的右边节点,然后整体左旋。旋转完毕后继续更新平衡因子即可。所有的else我们都用assert报错,因为有可能一棵树一开始的平衡因子就有问题。当我们旋转完成这棵树一定是高度降低并且平衡了,所以我们直接退出循环返回true即可。


以上就是AVL树的插入,除了erase接口其他接口都与搜索二叉树一样,下面我们将代码测试一下:


namespace AVLTreeK
{
  template<class K>
  struct AVLTreeNode
  {
  AVLTreeNode<K>* _left;
  AVLTreeNode<K>* _right;
  AVLTreeNode<K>* _parent;
  K _kv;
  int _bf;
  AVLTreeNode(const K& kv)
    :_left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _kv(kv)
    , _bf(0)
  {
  }
  };
  template<class K>
  class AVLTree
  {
  typedef AVLTreeNode<K> Node;
  public:
  void Inorder()
  {
    _Inorder(_root);
  }
  void _Inorder(Node* root)
  {
    if (root == nullptr)
    {
    return;
    }
    cout << root->_kv << " ";
    _Inorder(root->_left);
    _Inorder(root->_right);
  }
  bool insert(const K& kv)
  {
    if (_root == nullptr)
    {
    _root = new Node(kv);
    return true;
    }
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
    if (cur->_kv < kv)
    {
      parent = cur;
      cur = cur->_right;
    }
    else if (cur->_kv > kv)
    {
      parent = cur;
      cur = cur->_left;
    }
    else
    {
      return false;
    }
    }
    cur = new Node(kv);
    if (parent->_kv < kv)
    {
    parent->_right = cur;
    }
    else
    {
    parent->_left = cur;
    }
    cur->_parent = parent;
    //开始控制平衡因子
    while (parent)
    {
    if (parent->_left == cur)
    {
      parent->_bf--;
    }
    else if (parent->_right == cur)
    {
      parent->_bf++;
    }
    if (parent->_bf == 0)
    {
      //已经平衡了
      break;
    }
    else if (parent->_bf == 1 || parent->_bf == -1)
    {
      //需要继续向上调节
      parent = parent->_parent;
      cur = cur->_parent;
    }
    else if (parent->_bf == 2 || parent->_bf == -2)
    {
      //需要旋转控制,旋转的目的是降低高度+平衡,所以旋转完毕后退出循环。
      //首先是单纯右边高,需要左单旋
      if (parent->_bf == 2 && cur->_bf == 1)
      {
      RotateL(parent);
      }
      //如果是单纯左边高,需要右单旋
      else if (parent->_bf == -2 && cur->_bf == -1)
      {
      RotateR(parent);
      }
      //如果是整体左边高,并且左边的第一个节点的右子树高,就需要先左单旋,再右单旋
      else if (parent->_bf == -2 && cur->_bf == 1)
      {
      RotateLR(parent);
      }
      //如果整体右边高,但是右边第一个节点的左边高,就需要先右单旋,再左单旋
      else if (parent->_bf == 2 && cur->_bf == -1)
      {
      RotateRL(parent);
      }
      else
      {
      assert(false);
      }
      //当任何一个旋转结束了当前树都会平衡,所以需要直接退出循环
      break;
    }
    else
    {
      assert(false);
    }
    }
    return true;
  }
  private:
  void RotateL(Node* parent)
  {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* ppnode = parent->_parent;
    parent->_right = subRL;
    if (subRL)
    {
    subRL->_parent = parent;
    }
    subR->_left = parent;
    parent->_parent = subR;
    if (ppnode == nullptr)
    {
    _root = subR;
    _root->_parent = nullptr;
    }
    else
    {
    if (ppnode->_left == parent)
    {
      ppnode->_left = subR;
    }
    else
    {
      ppnode->_right = subR;
    }
    subR->_parent = ppnode;
    }
    parent->_bf = subR->_bf = 0;
  }
  void RotateR(Node* parent)
  {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    Node* ppnode = parent->_parent;
    parent->_left = subLR;
    if (subLR)
    {
    subLR->_parent = parent;
    }
    subL->_right = parent;
    parent->_parent = subL;
    if (ppnode == nullptr)
    {
    _root = subL;
    _root->_parent = nullptr;
    }
    else
    {
    if (ppnode->_left == parent)
    {
      ppnode->_left = subL;
    }
    else
    {
      ppnode->_right = subL;
    }
    subL->_parent = ppnode;
    }
    parent->_bf = subL->_bf = 0;
  }
  void RotateLR(Node* parent)
  {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;
    RotateL(parent->_left);
    RotateR(parent);
    if (bf == 1)
    {
    parent->_bf = 0;
    subL->_bf = -1;
    subLR->_bf = 0;
    }
    else if (bf == -1)
    {
    parent->_bf = 1;
    subL->_bf = 0;
    subLR->_bf = 0;
    }
    else if (bf == 0)
    {
    parent->_bf = 0;
    subL->_bf = 0;
    subLR->_bf = 0;
    }
    else
    {
    assert(false);
    }
  }
  void RotateRL(Node* parent)
  {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;
    RotateR(parent->_right);
    RotateL(parent);
    if (bf == 1)
    {
    parent->_bf = -1;
    subR->_bf = 0;
    subRL->_bf = 0;
    }
    else if (bf == -1)
    {
    parent->_bf = 0;
    subR->_bf = 1;
    subRL->_bf = 0;
    }
    else if (bf == 0)
    {
    parent->_bf = 0;
    subR->_bf = 0;
    subRL->_bf = 0;
    }
    else
    {
    assert(false);
    }
  }
  private:
  Node* _root = nullptr;
  };
}

测试的时候我们以K模型测试,下面是测试样例:


void test()
{
  AVLTreeK::AVLTree<int> at;
  int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16,14 };
  int b[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
  for (auto& e : a)
  {
  at.insert(e);
  }
  at.Inorder();
}

我们自己画一个经过旋转后的图与前序遍历结果看是否一样:


ce17c1db16f34883b3469b8a9cef2d87.png9367e2413f6443ad9f0cb6279968bdd6.png


我们可以看到答案是正确的,以上就是我们AVL树的所有内容了。


总结



AVL 树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过 1 ,这样可以保证查询时高效的时间复杂度,即O(logN) 。但是如果要对 AVL 树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的 ( 即不会改变 ) ,可以考虑 AVL 树,但一个结构经常修改,就不太适合。

目录
相关文章
|
4月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
108 2
|
2月前
|
监控 算法 数据处理
基于 C++ 的 KD 树算法在监控局域网屏幕中的理论剖析与工程实践研究
本文探讨了KD树在局域网屏幕监控中的应用,通过C++实现其构建与查询功能,显著提升多维数据处理效率。KD树作为一种二叉空间划分结构,适用于屏幕图像特征匹配、异常画面检测及数据压缩传输优化等场景。相比传统方法,基于KD树的方案检索效率提升2-3个数量级,但高维数据退化和动态更新等问题仍需进一步研究。未来可通过融合其他数据结构、引入深度学习及开发增量式更新算法等方式优化性能。
83 17
|
4月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
218 73
|
1月前
|
存储 JavaScript 前端开发
for...of循环在遍历Set和Map时的注意事项有哪些?
for...of循环在遍历Set和Map时的注意事项有哪些?
46 0
|
1月前
|
存储 C++ 容器
unordered_set、unordered_multiset、unordered_map、unordered_multimap的介绍及使用
unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。在unordered_set中,元素的值同时也是唯一地标识它的key。在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。它的迭代器至少是前向迭代器。前向迭代器的特性。
60 0
|
1月前
|
编译器 C++ 容器
用一棵红黑树同时封装出map和set
再完成上面的代码后,我们的底层代码已经完成了,这时候已经是一个底层STL的红黑树了,已经已符合库里面的要求了,这时候我们是需要给他穿上对应的“衣服”,比如穿上set的“衣服”,那么这个穿上set的“衣服”,那么他就符合库里面set的要求了,同样map一样,这时候我们就需要实现set与map了。因此,上层容器map需要向底层红黑树提供一个仿函数,用于获取T当中的键值Key,这样一来,当底层红黑树当中需要比较两个结点的键值时,就可以通过这个仿函数来获取T当中的键值了。我们就可以使用仿函数了。
28 0
|
1月前
|
存储 编译器 容器
set、map、multiset、multimap的介绍及使用以及区别,注意事项
set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。set默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。
46 0
|
5月前
|
编译器 容器
哈希表模拟封装unordered_map和unordered_set
哈希表模拟封装unordered_map和unordered_set
|
5月前
|
编译器 测试技术 计算机视觉
红黑树模拟封装map和set
红黑树模拟封装map和set
|
7月前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
133 18
你对Collection中Set、List、Map理解?