【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++】unordered_map(set)
C++中的`unordered`容器(如`std::unordered_set`、`std::unordered_map`)基于哈希表实现,提供高效的查找、插入和删除操作。哈希表通过哈希函数将元素映射到特定的“桶”中,每个桶可存储一个或多个元素,以处理哈希冲突。主要组成部分包括哈希表、哈希函数、冲突处理机制、负载因子和再散列,以及迭代器。哈希函数用于计算元素的哈希值,冲突通过开链法解决,负载因子控制哈希表的扩展。迭代器支持遍历容器中的元素。`unordered_map`和`unordered_set`的插入、查找和删除操作在理想情况下时间复杂度为O(1),但在冲突较多时可能退化为O(n)。
12 5
|
4天前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树,由Georgy Adelson-Velsky和Evgenii Landis提出。它通过确保任意节点的两子树高度差不超过1来维持平衡,支持高效插入、删除和查找操作,时间复杂度为O(log n)。AVL树通过四种旋转操作(左旋、右旋、左-右旋、右-左旋)来恢复树的平衡状态,适用于需要频繁进行数据操作的场景。
16 2
|
1月前
|
存储 JavaScript 前端开发
Set、Map、WeakSet 和 WeakMap 的区别
在 JavaScript 中,Set 和 Map 用于存储唯一值和键值对,支持多种操作方法,如添加、删除和检查元素。WeakSet 和 WeakMap 则存储弱引用的对象,有助于防止内存泄漏,适合特定场景使用。
|
2月前
|
存储 Java API
【数据结构】map&set详解
本文详细介绍了Java集合框架中的Set系列和Map系列集合。Set系列包括HashSet(哈希表实现,无序且元素唯一)、LinkedHashSet(保持插入顺序的HashSet)、TreeSet(红黑树实现,自动排序)。Map系列为双列集合,键值一一对应,键不可重复,值可重复。文章还介绍了HashMap、LinkedHashMap、TreeMap的具体实现与应用场景,并提供了面试题示例,如随机链表复制、宝石与石头、前K个高频单词等问题的解决方案。
40 6
【数据结构】map&set详解
|
1月前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
38 1
|
2月前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
38 5
|
2月前
|
存储 JavaScript 前端开发
js的map和set |21
js的map和set |21
|
2月前
|
存储 前端开发 API
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
该文章详细介绍了ES6中Set和Map数据结构的特性和使用方法,并探讨了它们在前端开发中的具体应用,包括如何利用这些数据结构来解决常见的编程问题。
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
|
2月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树:它以苏联科学家Georgy Adelson-Velsky和Evgenii Landis的名字命名。
27 2
|
1天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
14 2