map、set(底层结构)——C++

这篇博客介绍了C++中map和set底层实现的平衡树原理,主要聚焦于AVL树和红黑树。AVL树是一种高度平衡的二叉搜索树,插入操作可能导致旋转以保持平衡,而红黑树允许路径最大长度不超过最短路径的两倍,插入旋转次数较少。两者都是高效的平衡二叉搜索树,但红黑树在频繁的增删操作中表现优于AVL树。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

4. 底层结构

map/multimap/set/multiset这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

 

4.1 AVL 树

高度平衡二叉搜索树,是以人名命名的

4.1.1 AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)(非必须)
    平衡因子 = 右子树高度-左子树高度

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)

平衡因子需要三叉链
但是旋转的时候也会变复杂

 

4.1.2AVL树节点的定义

t

emplate<class K, class V>
struct AVLTreeNode
{
   
   
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;

pair<K, V> _kv;
int _bf;  // balance factor

AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{
   
   }
};

 

4.1.3 AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

更新平衡因子的规则
1.新增在右,parent->bf++;新增在左,parent->bf–
 
2.更新后,parent->bf == 1 or -1,说明parent插入前的平衡因子是0,说明左右子树高度相等,插入后有一边高,parent高度变了,需要继续往上更新
 
3.更新后,parent->bf == 0,说明parent插入前的平衡因子是1或-1,说明左右子树一边高一边低,插入后两边一样高,插入填上了矮的那边,parent所在子树高度不变,不需要继续往上更新
 
4.更新后,parent->bf == 2 or -2,说明parent插入前的平衡因子是1或-1,已经到达平衡临界值,插入后变成2或-2,打破平衡,parent所在子树需要旋转处理
 
5.更新后,parent->>2 or <-2的值,不可能,如果存在,说明插入前的就不是AVL数,需要去检查之前操作的问题

//插入
cur = new Node(kv);
//链接
if (parent->_kv.first < kv.first)
{
   
   
//插入的值大就插入右边
parent->_right = cur;
}
else
{
   
   
//插入的值小就插入左边
parent->_left = cur;
}
//三叉链还要考虑parent
cur->_parent = parent;


// 控制平衡
// 1、更新平衡因子
while (parent)
{
   
   
if (cur == parent->_right)
{
   
   
parent->_bf++;
}
else
{
   
   
parent->_bf--;
}

if (parent->_bf == 0)
{
   
   
break;
}
else if (abs(parent->_bf) == 1)
{
   
   
                                //往上更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (abs(parent->_bf) == 2)
{
   
   
// 说明parent所在子树已经不平衡了,需要旋转处理
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);
}

break;
}
else
{
   
   
assert(false);
}
}

return true;
}

 
 

4.1.4 AVL树的旋转

原则:

  1. 旋转成平衡树
  2. 保持搜索树的规则
 else if (abs(parent->_bf) == 2)
{
   
   
// 说明parent所在子树已经不平衡了,需要旋转处理
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);
}

break;
}

 

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种

 

1. 新节点插入较高左子树的左侧—左左:右单旋

a、b、c是h >= 0的平衡树
左边高,往右边旋转
只有parent和subL的平衡因子受影响——全变为0

void RotateR(Node* parent)
{
   
   
Node* subL = parent->_left;
Node* subLR = subL->_right;

parent->_left = subLR;
if (subLR)
{
   
   
subLR->_parent = parent;
}

Node* ppNode = parent->_parent;

subL->_right = parent;
parent->_parent = subL;

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

subL->_parent = ppNode;
}

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

 

  1. 新节点插入较高右子树的右侧—右右:左单旋

parent是整棵树的根
parent是子树的根
要动六个指针
只有parent和subR的平衡因子受影响——全变为0

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

parent->_right = subRL;
if (subRL)
subRL->_parent = parent;

Node* ppNode = parent->_parent;

subR->_left = parent;
parent->_parent = subR;

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

subR->_parent = ppNode;
}

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

 

单旋(1、2):a、b、c是高度为h的AVL子树,h>=0; 旋转的价值和意义:

  • 平衡 降高度(高度恢复到插入之前的样子)
  • 新节点插入较高左子树的右侧—左右:先左单旋再右单旋

 

3. 新节点插入较高左子树的右侧—左右:先左单旋再右单旋
void RotateLR(Node* parent)
{
   
   
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;

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

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

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

 

4. 新节点插入较高右子树的左侧—右左:先右单旋再左单旋

参考右左双旋。

void RotateRL(
### C++ 中 `set` 和 `map` 的基本用法 #### 创建和初始化 对于 `set` 容器,可以像这样创建并初始化: ```cpp #include <iostream> #include <set> int main() { std::set<int> s = {8, 3, 1, 10, 6, 4, 7, 14, 13}; // 初始化一个集合s } ``` 上述代码展示了如何通过列表初始化来定义一个整数类型的 `set` 集合[^2]。 而对于 `map` 容器,则可以通过如下方式声明以及填充键值对数据结构: ```cpp #include <iostream> #include <map> using namespace std; int main(){ map<string, int> m; m["apple"] = 5; // 插入元素到映射表中 m.insert(make_pair("banana", 10)); // 另一种插入方法 } ``` 这里介绍了两种不同的插入新条目的手段——直接赋值与调用成员函数 `insert()` 方法[^1]. #### 基本操作 ##### 向容器内添加元素 当处理 `set` 类型的数据集时,使用 `insert()` 函数可轻松加入新的项目;而针对 `map`,同样适用该机制用于更新或新增记录: ```cpp // 对于 set std::set<int>::iterator pos = s.find(3); if (pos != s.end()){ s.erase(pos); // 删除找到的第一个匹配项 } // 对于 map m["orange"] = 15; // 添加/修改键 "orange" 所对应的数值 ``` 以上片段说明了怎样定位特定目标并通过迭代器执行移除动作,同时也展现了设置关联数组里某一项的新值得过程. ##### 迭代访问所有元素 遍历整个 `set` 或者 `map` 是一件非常常见的事情,在C++中有多种途径完成这项工作。最简单的方式就是借助范围for循环语法糖: ```cpp // 输出所有的 key-value pairs from the map for(const auto& pair : m){ cout << pair.first << ": " << pair.second << endl; } // Print all elements of the set for(auto elem : s){ cout << elem << ' '; } ``` 这些例子清晰地表达了如何优雅地打印出每一个存储单元的内容[^3]. #### 特殊性质 值得注意的是,由于 `set` 和 `map` 底层均基于红黑树实现,所以它们都具备自动维持内部顺序的能力,并且支持高效的查找、插入及删除操作,时间复杂度均为 O(log n)[^4]. 此外,标准库还提供了多版本变体如 `multiset` 和 `multimap` 来允许存在重复关键字的情况发生[^5].
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hey pear!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值