🎯 本节目标

  1. 理解关联式容器与序列式容器的区别
  2. 掌握键值对(pair)的概念与使用
  3. 熟悉树形结构的关联式容器:map、set、multimap、multiset
  4. 深入理解底层数据结构:AVL树与红黑树

📖 1. 关联式容器

1.1 什么是关联式容器?

在初阶阶段,我们已经接触过 STL 中的部分容器,比如:vectorlistdequeforward_list(C++11) 等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。

🔍 关联式容器也是用来存储数据的,与序列式容器不同的是,其内部存储的是 <key, value> 结构的键值对,在数据检索时比序列式容器效率更高。

1.2 关联式容器的特点

  • 🔑 键值对存储:每个元素都是一个键值对
  • 高效检索:基于键(key)的查找效率高
  • 📊 自动排序:元素通常按照键的顺序存储
  • 🎯 唯一性:某些容器要求键是唯一的

1.3 关联式容器分类

根据应用场景的不同,STL 总共实现了两种不同结构的关联式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:mapsetmultimapmultiset

容器类型 底层结构 特点
树形结构 平衡二叉搜索树(红黑树) 元素有序,查找稳定
哈希结构 哈希表 查找更快,但元素无序

🔗 2. 键值对(pair)

2.1 键值对的概念

键值对是用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量 keyvalue

  • key 代表键值
  • value 表示与 key 对应的信息

💡 示例:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该单词,在词典中就可以找到与其对应的中文含义。

2.2 SGI-STL 中 pair 的定义

template <class T1, class T2>
struct pair 
{
    typedef T1 first_type;
    typedef T2 second_type;
    
    T1 first;    // 键(key)
    T2 second;   // 值(value)
    
    // 默认构造函数
    pair(): first(T1()), second(T2())
    {}
    
    // 带参构造函数
    pair(const T1& a, const T2& b): first(a), second(b)
    {}
};

2.3 pair 的常用操作

#include <iostream>
#include <utility>  // pair 头文件

int main() {
    // 创建 pair 的几种方式
    std::pair<std::string, int> p1;                // 默认构造
    std::pair<std::string, int> p2("Alice", 25);   // 直接构造
    std::pair<std::string, int> p3 = {"Bob", 30};  // 列表初始化
    auto p4 = std::make_pair("Charlie", 35);       // make_pair 函数
    
    // 访问成员
    std::cout << "Name: " << p2.first << ", Age: " << p2.second << std::endl;
    
    // 比较操作(按 first 比较,相等时比较 second)
    std::pair<int, int> a(1, 2);
    std::pair<int, int> b(1, 3);
    std::cout << "a < b: " << (a < b) << std::endl;  // 输出 1(true)
    
    return 0;
}

🌳 3. 树形结构的关联式容器

3.1 容器概览

STL 提供了四种树形结构的关联式容器:

容器 特点 键是否唯一 底层实现
set 只存储值(value 即 key) ✅ 是 红黑树
map 存储键值对(key-value) ✅ 是 红黑树
multiset 允许重复值的 set ❌ 否 红黑树
multimap 允许重复键的 map ❌ 否 红黑树

3.2 共同特点

  1. 🎯 底层结构:都使用平衡二叉搜索树(红黑树)实现
  2. 📈 有序性:容器中的元素是有序序列
  3. ⏱️ 时间复杂度:查找、插入、删除均为 O(log₂n)
  4. 🔄 迭代器:支持双向迭代器,可正向/反向遍历

3.3 set 容器详解

3.3.1 set 的介绍

set 是按照一定次序存储元素的容器。在 set 中,元素的 value 也标识它(value 就是 key,类型为 T),并且每个 value 必须是唯一的。set 中的元素不能在容器中修改(元素总是 const),但是可以从容器中插入或删除它们。

在内部,set 中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。set 容器通过 key 访问单个元素的速度通常比 unordered_set 容器慢,但它们允许根据顺序对子集进行直接迭代。set 在底层是用二叉搜索树(红黑树)实现的。

注意:

  1. 与 map/multimap 不同,map/multimap 中存储的是真正的键值对 <key, value>,set 中只放 value,但在底层实际存放的是由 <value, value> 构成的键值对。
  2. set 中插入元素时,只需要插入 value 即可,不需要构造键值对。
  3. set 中的元素不可以重复(因此可以使用 set 进行去重)。
  4. 使用 set 的迭代器遍历 set 中的元素,可以得到有序序列。
  5. set 中的元素默认按照小于来比较。
  6. set 中查找某个元素,时间复杂度为: l o g 2 n log_2 n log2n
  7. set 中的元素不允许修改(为什么?)
  8. set 中的底层使用二叉搜索树(红黑树)来实现。
3.3.2 set 的使用

1. set 的模板参数列表

  • T: set 中存放元素的类型,实际在底层存储 <value, value> 的键值对。
  • Compare:set 中元素默认按照小于来比较。
  • Alloc:set 中元素空间的管理方式,使用 STL 提供的空间配置器管理。

2. set 的构造

函数声明 功能介绍
set(const Compare& comp = Compare(), const Allocator& = Allocator()); 构造空的 set
set(InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& = Allocator()); 用 [first, last) 区间中的元素构造 set
set(const set<Key,Compare,Allocator>& x); set 的拷贝构造

3. set 的迭代器

函数声明 功能介绍
iterator begin() 返回 set 中起始位置元素的迭代器
iterator end() 返回 set 中最后一个元素后面的迭代器
const_iterator cbegin() const 返回 set 中起始位置元素的 const 迭代器
const_iterator cend() const 返回 set 中最后一个元素后面的 const 迭代器
reverse_iterator rbegin() 返回 set 第一个元素的反向迭代器,即 end
reverse_iterator rend() 返回 set 最后一个元素下一个位置的反向迭代器,即 rbegin
const_reverse_iterator crbegin() const 返回 set 第一个元素的反向 const 迭代器,即 cend
const_reverse_iterator crend() const 返回 set 最后一个元素下一个位置的反向 const 迭代器,即 crbegin

4. set 的容量

函数声明 功能介绍
bool empty() const 检测 set 是否为空,空返回 true,否则返回 false
size_type size() const 返回 set 中有效元素的个数

5. set 修改操作

函数声明 功能介绍
pair<iterator,bool> insert(const value_type& x) 在 set 中插入元素 x,实际插入的是 <x, x> 构成的键值对,如果插入成功,返回 <该元素在 set 中的位置, true>,如果插入失败,说明 x 在 set 中已经存在,返回 <x 在 set 中的位置, false>
void erase(iterator position) 删除 set 中 position 位置上的元素
size_type erase(const key_type& x) 删除 set 中值为 x 的元素,返回删除的元素的个数
void erase(iterator first, iterator last) 删除 set 中 [first, last) 区间中的元素
void swap(set<Key,Compare,Allocator>& st) 交换 set 中的元素
void clear() 将 set 中的元素清空
iterator find(const key_type& x) const 返回 set 中值为 x 的元素的位置
size_type count(const key_type& x) const 返回 set 中值为 x 的元素的个数

6. set 的使用举例

#include <set>
#include <iostream>

void TestSet() {
    // 用数组 array 中的元素构造 set
    int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    std::set<int> s(array, array + sizeof(array)/sizeof(array[0]));
    
    std::cout << "Set size: " << s.size() << std::endl;  // 输出 10(去重后)
    
    // 正向打印 set 中的元素,从打印结果中可以看出:set 可去重
    std::cout << "Ascending order: ";
    for (auto& e : s) {
        std::cout << e << " ";
    }
    std::cout << std::endl;
    
    // 使用迭代器逆向打印 set 中的元素
    std::cout << "Descending order: ";
    for (auto it = s.rbegin(); it != s.rend(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    
    // set 中值为 3 的元素出现了几次
    std::cout << "Count of 3: " << s.count(3) << std::endl;  // 输出 1
}

int main() {
    TestSet();
    return 0;
}

3.4 map 容器详解

3.4.1 map 的介绍

map 是关联容器,它按照特定的次序(按照 key 来比较)存储由键值 key 和值 value 组合而成的元素。

在 map 中,键值 key 通常用于排序和惟一地标识元素,而值 value 中存储与此键值 key 关联的内容。键值 key 和值 value 的类型可能不同,并且在 map 的内部,key 与 value 通过成员类型 value_type 绑定在一起,为其取别名称为 pair:

typedef pair<const key, T> value_type;

在内部,map 中的元素总是按照键值 key 进行比较排序的。map 中通过键值访问单个元素的速度通常比 unordered_map 容器慢,但 map 允许根据顺序对元素进行直接迭代(即对 map 中的元素进行迭代时,可以得到一个有序的序列)。map 支持下标访问符,即在 [] 中放入 key,就可以找到与 key 对应的 value。map 通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

3.4.2 map 的使用

1. map 的模板参数说明

  • key: 键值对中 key 的类型
  • T:键值对中 value 的类型
  • Compare: 比较器的类型,map 中的元素是按照 key 来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
  • Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器

注意:在使用 map 时,需要包含 <map> 头文件。

2. map 的构造

函数声明 功能介绍
map() 构造一个空的 map

3. map 的迭代器

函数声明 功能简介
begin()end() begin: 首元素的位置,end: 最后一个元素的下一个位置
cbegin()cend() 与 begin 和 end 意义相同,但 cbegin 和 cend 所指向的元素不能修改
rbegin()rend() 反向迭代器,rbegin 在 end 位置,rend 在 begin 位置,其 ++ 和 – 操作与 begin 和 end 操作移动相反
crbegin()crend() 与 rbegin 和 rend 位置相同,操作相同,但 crbegin 和 crend 所指向的元素不能修改

4. map 的容量与元素访问

函数声明 功能简介
bool empty() const 检测 map 中的元素是否为空,是返回 true,否则返回 false
size_type size() const 返回 map 中有效元素的个数
mapped_type& operator[](const key_type& k) 返回 key 对应的 value

问题:当 key 不在 map 中时,通过 operator[] 获取对应 value 时会发生什么问题?

注意:在元素访问时,有一个与 operator[] 类似的操作 at()(该函数不常用)函数,都是通过 key 找到与 key 对应的 value 然后返回其引用,不同的是:当 key 不存在时,operator[] 用默认 value 与 key 构造键值对然后插入,返回该默认 value,at() 函数直接抛异常。

5. map 中元素的修改

函数声明 功能简介
pair<iterator,bool> insert(const value_type& x) 在 map 中插入键值对 x,注意 x 是一个键值对,返回值也是键值对:iterator 代表新插入元素的位置,bool 代表是否插入成功
void erase(iterator position) 删除 position 位置上的元素
size_type erase(const key_type& x) 删除键值为 x 的元素
void erase(iterator first, iterator last) 删除 [first, last) 区间中的元素
void swap(map<Key,T,Compare,Allocator>& mp) 交换两个 map 中的元素
void clear() 将 map 中的元素清空
iterator find(const key_type& x) 在 map 中插入 key 为 x 的元素,找到返回该元素的位置的迭代器,否则返回 end
const_iterator find(const key_type& x) const 在 map 中插入 key 为 x 的元素,找到返回该元素的位置的 const 迭代器,否则返回 cend
size_type count(const key_type& x) const 返回 key 为 x 的键值在 map 中的个数,注意 map 中 key 是唯一的,因此该函数的返回值要么为 0,要么为 1,因此也可以用该函数来检测一个 key 是否在 map 中

6. map 的使用举例

#include <string>
#include <map>
void TestMap()
{
	 map<string, string> m;
	 // 向map中插入元素的方式:
	 // 将键值对<"peach","桃子">插入map中,用pair直接来构造键值对
	 m.insert(pair<string, string>("peach", "桃子"));
	 // 将键值对<"peach","桃子">插入map中,用make_pair函数来构造键值对
	 m.insert(make_pair("banan", "香蕉"));
	 
	 // 借用operator[]向map中插入元素
	 /*
		 operator[]的原理是:
		  用<key, T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中
		  如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器
		  如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器
		  operator[]函数最后将insert返回值键值对中的value返回
	 */
	 // 将<"apple", "">插入map中,插入成功,返回value的引用,将“苹果”赋值给该引用结果,
	 m["apple"] = "苹果";
	 // key不存在时抛异常
	 //m.at("waterme") = "水蜜桃";
	 cout << m.size() << endl;
	 // 用迭代器去遍历map中的元素,可以得到一个按照key排序的序列
	 for (auto& e : m)
		 cout << e.first << "--->" << e.second << endl;
	 cout << endl;
	 // map中的键值对key一定是唯一的,如果key存在将插入失败
	 auto ret = m.insert(make_pair("peach", "桃色"));
	 if (ret.second)
		 cout << "<peach, 桃子>不在map中, 已经插入" << endl;
	 else
		 cout << "键值为peach的元素已经存在:" << ret.first->first << "--->"\
	<< ret.first->second <<" 插入失败"<< endl;
	 // 删除key为"apple"的元素
	 m.erase("apple");
	 if (1 == m.count("apple"))
		 cout << "apple还在" << endl;
	 else
		 cout << "apple被吃了" << endl;
}

3.5 multimap 容器详解

3.5.1 multimap 的介绍

multimap 是关联式容器,它按照特定的排序准则存储键值对,与 map 不同的是,multimap 允许键重复。multimap 中的元素按照键排序,并且支持通过键快速查找。

multimap 的特点:

  1. 🔑 允许重复键:同一个键可以对应多个不同的值
  2. 📊 有序存储:元素按照键的顺序排序
  3. 🔍 高效查找:基于键的查找效率为 O(log n)
  4. 🎯 不支持 operator[]:因为键不唯一,无法通过下标访问
3.5.2 multimap 的使用
#include <map>
#include <iostream>
#include <string>

void TestMultiMap() {
    std::multimap<std::string, int> mm;
    
    // 插入重复键
    mm.insert({"Alice", 25});
    mm.insert({"Bob", 30});
    mm.insert({"Alice", 28});  // 允许重复键
    mm.insert({"Alice", 32});  // 再次插入相同的键
    
    // 遍历 multimap
    std::cout << "All elements in multimap:" << std::endl;
    for (const auto& kv : mm) {
        std::cout << kv.first << ": " << kv.second << std::endl;
    }
    
    // 查找特定键的所有值
    std::cout << "\nAll values for key 'Alice':" << std::endl;
    auto range = mm.equal_range("Alice");
    for (auto it = range.first; it != range.second; ++it) {
        std::cout << it->second << " ";
    }
    std::cout << std::endl;
    
    // 统计特定键的数量
    std::cout << "Number of 'Alice': " << mm.count("Alice") << std::endl;
    
    // 删除特定键的所有元素
    size_t erased = mm.erase("Alice");
    std::cout << "Erased " << erased << " elements with key 'Alice'" << std::endl;
}

int main() {
    TestMultiMap();
    return 0;
}

3.6 multiset 容器详解

3.6.1 multiset 的介绍

multiset 是按照特定顺序存储元素的容器,与 set 不同的是,multiset 允许元素重复。在 multiset 中,元素的值同时也是键,因此元素不能修改,但可以插入和删除。

multiset 的特点:

  1. 🔄 允许重复元素:相同的值可以出现多次
  2. 📈 自动排序:元素按照升序排列
  3. 高效操作:插入、删除、查找均为 O(log n)
  4. 🎯 元素不可修改:元素总是 const
3.6.2 multiset 的使用
#include <set>
#include <iostream>

void TestMultiSet() {
    std::multiset<int> ms;
    
    // 插入重复元素
    ms.insert(5);
    ms.insert(2);
    ms.insert(8);
    ms.insert(5);  // 重复元素
    ms.insert(5);  // 再次重复
    ms.insert(3);
    
    // 遍历 multiset
    std::cout << "All elements in multiset:" << std::endl;
    for (const auto& val : ms) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    
    // 统计元素出现次数
    std::cout << "Number of 5: " << ms.count(5) << std::endl;
    
    // 查找元素范围
    auto range = ms.equal_range(5);
    std::cout << "Range of 5: ";
    for (auto it = range.first; it != range.second; ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    
    // 删除特定值的所有元素
    size_t era    // 删除特定值的所有元素
    size_t erased = ms.erase(5);
    std::cout << "Erased " << erased << " elements with value 5" << std::endl;
    std::cout << "After erasing 5:" << std::endl;g 5:" << std::endl;
    for (const auto& val : ms) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

int main() {
    TestMultiSet();
    return 0}

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树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
在这里插入图片描述
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O ( l o g 2 n ) O(log_2 n) O(log2n),搜索

4.1.2 AVL树节点的定义

🔧 AVL树节点的定义:

template<class T>
struct AVLTreeNode
{
     AVLTreeNode(const T& data)
         : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
         , _data(data), _bf(0)
     {}
     AVLTreeNode<T>* _pLeft;   // 该节点的左孩子
     AVLTreeNode<T>* _pRight;  // 该节点的右孩子
     AVLTreeNode<T>* _pParent; // 该节点的双亲
     T _data;
     int _bf;  
 }
4.1.3 AVL树的插入

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

  1. 📝 按照二叉搜索树的方式插入新节点
  2. 🔄 调整节点的平衡因子
bool Insert(const T& data)
{
    // 1. 先按照二叉搜索树的规则将节点插入到AVL树中
    // ...
    // 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树
    //    的平衡性
    /*
     pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
     的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
      1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
      2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
     此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
      1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整成0,此时满足
        AVL树的性质,插入成功
      2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,此
        时以pParent为根的树的高度增加,需要继续向上更新
      3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理
    */
    while (pParent)
    {
        // 更新双亲的平衡因子
        if (pCur == pParent->_pLeft)
             pParent->_bf--;
        else
             pParent->_bf++;
        // 更新后检测双亲的平衡因子
        if (0 == pParent->_bf)
        {    
            break;
        }
        else if (1 == pParent->_bf || -1 == pParent->_bf)
        {
              // 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲为根的二叉树
              // 的高度增加了一层,因此需要继续向上调整
             pCur = pParent;
             pParent = pCur->_pParent;
        }
        else
        {
             // 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
             // 为根的树进行旋转处理
              if(2 == pParent->_bf)
              {
                  // 右子树高,需要左旋
                  if(pParent->_pRight->_bf == 1)
                  {
                      // 右右情况:左单旋
                      _RotateL(pParent);
                  }
                  else if(pParent->_pRight->_bf == -1)
                  {
                      // 右左情况:先右旋再左旋
                      _RotateRL(pParent);
                  }
              }
              else if(-2 == pParent->_bf)
              {
                  // 左子树高,需要右旋
                  if(pParent->_pLeft->_bf == -1)
                  {
                      // 左左情况:右单旋
                      _RotateR(pParent);
                  }
                  else if(pParent->_pLeft->_bf == 1)
                  {
                      // 左右情况:先左旋再右旋
                      _RotateLR(pParent);
                  }
              }
              
              // 旋转完成后,以pParent为根的子树高度降低,已经平衡
              // 不需要继续向上更新
              break;
        }
    }
    return true;
}
4.1.4 AVL树的旋转

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

  1. 新节点插入较高左子树的左侧 —左左:右单旋
    在这里插入图片描述
/*
  上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加
  了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,
  即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
     如果是根节点,旋转完成后,要更新根节点
     如果是子树,可能是某个节点的左子树,也可能是右子树
  同学们再此处可举一些详细的例子进行画图,考虑各种情况,加深旋转的理解
*/
void _RotateR(PNode pParent)
{
    // pSubL: pParent的左孩子
    // pSubLR: pParent左孩子的右孩子,注意:该
    PNode pSubL = pParent->_pLeft;
    PNode pSubLR = pSubL->_pRight;
    // 旋转完成之后,30的右孩子作为双亲的左孩子
    pParent->_pLeft = pSubLR;
    // 如果30的左孩子的右孩子存在,更新亲双亲
    if(pSubLR)
        pSubLR->_pParent = pParent;
    // 60 作为 30的右孩子
    pSubL->_pRight = pParent;
    // 因为60可能是棵子树,因此在更新其双亲前必须先保存60的双亲
    PNode pPParent = pParent->_pParent;
    // 更新60的双亲
    pParent->_pParent = pSubL;
    // 更新30的双亲
    pSubL->_pParent = pPParent;
    // 如果60是根节点,根新指向根节点的指针
    if(NULL == pPParent)
    {
        _pRoot = pSubL;
        pSubL->_pParent = NULL;
    }
    else
    {
         // 如果60是子树,可能是其双亲的左子树,也可能是右子树
        if(pPParent->_pLeft == pParent)
            pPParent->_pLeft = pSubL;
        else
            pPParent->_pRight = pSubL;
    }
    // 根据调整后的结构更新部分节点的平衡因子
    pParent->_bf = pSubL->_bf = 0;
}
  1. 新节点插入较高右子树的右侧 —右右:左单旋
    在这里插入图片描述
    实现及情况考虑可参考右单旋。

  2. 新节点插入较高左子树的右侧 —左右:先左单旋再右单旋
    在这里插入图片描述
    将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再
    考虑平衡因子的更新。

// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进行调整
void _RotateLR(PNode pParent)
{
    PNode pSubL = pParent->_pLeft;
    PNode pSubLR = pSubL->_pRight;
    // 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
    int bf = pSubLR->_bf;
    // 先对30进行左单旋
    _RotateL(pParent->_pLeft);
    // 再对90进行右单旋
    _RotateR(pParent);
    if(1 == bf)
        pSubL->_bf = -1;
    else if(-1 == bf)
        pParent->_bf = 1;
}
  1. 新节点插入较高右子树的左侧 —右左:先右单旋再左单旋在这里插入图片描述
    🔄 参考右左双旋。

📋 总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑:

  1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
    • 当pSubR的平衡因子为1时,执行左单旋
    • 当pSubR的平衡因子为-1时,执行右左双旋
  2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
    • 当pSubL的平衡因子为-1是,执行右单旋
    • 当pSubL的平衡因子为1时,执行左右双旋
      旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
4.1.5 AVL树的验证

验证方法:
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    • 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    • 节点的平衡因子是否计算正确
int _Height(PNode pRoot);
bool _IsBalanceTree(PNode pRoot)
{
     // 空树也是AVL树
     if (nullptr == pRoot) return true;
     // 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
     int leftHeight = _Height(pRoot->_pLeft);
     int rightHeight = _Height(pRoot->_pRight);
     int diff = rightHeight - leftHeight;
     // 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
     // pRoot平衡因子的绝对值超过1,则一定不是AVL树
     if (diff != pRoot->_bf || (diff > 1 || diff < -1))
         return false;
     // pRoot的左和右如果都是AVL树,则该树一定是AVL树
     return _IsBalanceTree(pRoot->_pLeft) && _IsBalanceTree(pRoot->_pRight);
}
  1. 验证用例
    🔍 请同学们结合上述代码按照以下的数据次序,自己动手画AVL树的创建过程,验证代码是否有漏洞。
    • 常规场景1{16, 3, 7, 11, 9, 26, 18, 14, 15}
    • 特殊场景2{4, 2, 6, 1, 3, 5, 15, 7, 16, 14}
      在这里插入图片描述
4.1.6 AVL树的删除(了解)

🗑️ 删除操作简介:
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
具体实现学生们可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。

4.1.7 AVL树的性能

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

4.2 红黑树

4.2.1 红黑树的概念

🔴⚫ 红黑树的概念:
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
在这里插入图片描述

4.2.2 红黑树的性质

📋 红黑树的性质:

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  5. 每个叶子结点都是黑色的 (此处的叶子结点指的是空结点)
    🤔 **思考:**为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?
4.2.3 红黑树节点的定义

🔧 红黑树节点的定义:

// 节点的颜色
enum Color{RED, BLACK};

// 红黑树节点的定义
template<class ValueType>
struct RBTreeNode
{
     RBTreeNode(const ValueType& data = ValueType(), Color color = RED)
         : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
         , _data(data), _color(color)
     {}
     RBTreeNode<ValueType>* _pLeft;   // 节点的左孩子
     RBTreeNode<ValueType>* _pRight;  // 节点的右孩子
     RBTreeNode<ValueType>* _pParent; // 节点的双亲(红黑树需要旋转,为了实现简单给出该字段)
     ValueType _data;            // 节点的值域
     Color _color;               // 节点的颜色
};

🤔 **思考:**在节点的定义中,为什么要将节点的默认颜色给成红色的?

4.2.4 红黑树结构

🏗️ 红黑树结构:
为了后续实现关联式容器简单,红黑树的实现中增加一个头结点,因为根节点必须为黑色,为了与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,pLeft域指向红黑树中最小的节点, _pRight域指向红黑树中最大的节点,如下:
在这里插入图片描述

🔥红黑树性质:左根右,根叶黑,不红红,黑路同
🌲旋转规则:
黑叔:旋转 + 染色:
1.LL(右单旋,父换爷 +染色)
2.RR(左单旋,父换爷 +染色)
3.LR(左右双旋,子换爷 + 染色)
4.RL(右左双旋,子换爷 +染色)
红叔:叔父爷染色 +爷变为 新节点

4.2.5 红黑树的插入操作

🔴 红黑树的插入操作:
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

  1. 按照二叉搜索的树规则插入新节点
template<class ValueType>
class RBTree
{
    //……
    bool Insert(const ValueType& data)
    {
        PNode& pRoot = GetRoot();
        if (nullptr == pRoot)
        {
            pRoot = new Node(data, BLACK);
            // 根的双亲为头节点
            pRoot->_pParent = _pHead;
            _pHead->_pParent = pRoot;
        }
        else
        {
            // 1. 按照二叉搜索的树方式插入新节点
            PNode pCur = pRoot;
            PNode pParent = nullptr;
            
            // 查找插入位置
            KeyOfValue kov;
            Compare com;
            while (pCur)
            {
                pParent = pCur;
                if (com(kov(data), kov(pCur->_data)))
                    pCur = pCur->_pLeft;
                else if (com(kov(pCur->_data), kov(data)))
                    pCur = pCur->_pRight;
                else
                    return false; // 已存在,插入失败
            }
            
            // 创建新节点(默认红色)
            pCur = new Node(data);
            pCur->_pParent = pParent;
            
            // 连接到父节点
            if (com(kov(data), kov(pParent->_data)))
                pParent->_pLeft = pCur;
            else
                pParent->_pRight = pCur;
            
            // 2. 检测新节点插入后,红黑树的性质是否造到破坏,
            //    若满足直接退出,否则对红黑树进行旋转着色处理
            // 新节点为红色,如果父节点也是红色,需要调整
            while (pParent && RED == pParent->_color)
            {
                PNode grandFather = pParent->_pParent;
                
                // 父节点是祖父节点的左孩子
                if (pParent == grandFather->_pLeft)
                {
                    PNode uncle = grandFather->_pRight;
                    
                    // 情况一:叔叔节点存在且为红色
                    if (uncle && RED == uncle->_color)
                    {
                        // 父节点和叔叔节点变黑,祖父节点变红
                        pParent->_color = BLACK;
                        uncle->_color = BLACK;
                        grandFather->_color = RED;
                        
                        // 继续向上调整
                        pCur = grandFather;
                        pParent = pCur->_pParent;
                    }
                    else
                    {
                        // 情况二:叔叔节点不存在或为黑色,且当前节点是父节点的右孩子
                        if (pCur == pParent->_pRight)
                        {
                            // 左单旋
                            _RotateL(pParent);
                            std::swap(pParent, pCur);
                        }
                        
                        // 情况三:叔叔节点不存在或为黑色,且当前节点是父节点的左孩子
                        pParent->_color = BLACK;
                        grandFather->_color = RED;
                        _RotateR(grandFather);
                    }
                }
                else // 父节点是祖父节点的右孩子
                {
                    PNode uncle = grandFather->_pLeft;
                    
                    // 情况一:叔叔节点存在且为红色
                    if (uncle && RED == uncle->_color)
                    {
                        // 父节点和叔叔节点变黑,祖父节点变红
                        pParent->_color = BLACK;
                        uncle->_color = BLACK;
                        grandFather->_color = RED;
                        
                        // 继续向上调整
                        pCur = grandFather;
                        pParent = pCur->_pParent;
                    }
                    else
                    {
                        // 情况二:叔叔节点不存在或为黑色,且当前节点是父节点的左孩子
                        if (pCur == pParent->_pLeft)
                        {
                            // 右单旋
                            _RotateR(pParent);
                            std::swap(pParent, pCur);
                        }
                        
                        // 情况三:叔叔节点不存在或为黑色,且当前节点是父节点的右孩子
                        pParent->_color = BLACK;
                        grandFather->_color = RED;
                        _RotateL(grandFather);
                    }
                }
            }
        }
        // 根节点的颜色可能被修改,将其改回黑色
        pRoot->_color = BLACK;
        _pHead->_pLeft = LeftMost();
        _pHead->_pRight = RightMost();
        return true;
    }
private:
    PNode& GetRoot(){ return _pHead->_pParent;}
    // 获取红黑树中最小节点,即最左侧节点
    PNode LeftMost()
    {
        PNode pCur = GetRoot();
        if (nullptr == pCur)
            return _pHead;
        while (pCur->_pLeft)
            pCur = pCur->_pLeft;
        return pCur;
    }
    // 获取红黑树中最大节点,即最右侧节点
    PNode RightMost()
    {
        PNode pCur = GetRoot();
        if (nullptr == pCur)
            return _pHead;
        while (pCur->_pRight)
            pCur = pCur->_pRight;
        return pCur;
    }
private:
    PNode _pHead;
};
  1. 检测新节点插入后,红黑树的性质是否造到破坏
    因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:

约定: cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

情况一: cur为红,p为红,g为黑,u存在且为红
在这里插入图片描述

cur和p均为红,违反了性质三,此处能否将p直接改为黑?

**解决方式:**将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。

情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
在这里插入图片描述

p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
p为g的右孩子,cur为p的右孩子,则进行左单旋转
p、g变色 --p变黑,g变红

情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑
在这里插入图片描述

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,
p为g的右孩子,cur为p的左孩子,则针对p做右单旋转
则转换成了情况2
针对每种情况进行相应的处理即可。

bool Insert(const ValueType& data)
{
    // ...
    // 新节点插入后,如果其双亲节点的颜色为空色,则违反性质3:不能有连在一起的红色结点
    while(pParent && RED == pParent->_color)
    {
        // 注意:grandFather一定存在
        // 因为pParent存在,且不是黑色节点,则pParent一定不是根,则其一定有双亲
        PNode grandFather = pParent->_pParent;
        // 先讨论左侧情况
        if(pParent == grandFather->_pLeft)
        {
            PNode unclue = grandFather->_pRight;
            // 情况三:叔叔节点存在,且为红
            if(unclue && RED == unclue->_color)
            {
                pParent->_color = BLACK;
                unclue->_color = BLACK;
                grandFather->_color = RED;
                pCur = grandFather;
                pParent = pCur->_pParent;
            }
            else
            {
                // 情况五:叔叔节点不存在,或者叔叔节点存在且为黑
                if(pCur == pParent->_pRight)
                {
                    _RotateLeft(pParent);
                    swap(pParent, pCur);
                }
                 // 情况五最后转化成情况四
                grandFather->_color = RED;
                pParent->_color = BLACK;
                _RotateRight(grandFather);
            }
        }
        else
        {
            // 右侧情况:父节点是祖父节点的右孩子
            PNode uncle = grandFather->_pLeft;
            
            // 情况一:叔叔节点存在且为红色
            if(uncle && RED == uncle->_color)
            {
                pParent->_color = BLACK;
                uncle->_color = BLACK;
                grandFather->_color = RED;
                pCur = grandFather;
                pParent = pCur->_pParent;
            }
            else
            {
                // 情况二:叔叔节点不存在或为黑色,且当前节点是父节点的左孩子
                if(pCur == pParent->_pLeft)
                {
                    _RotateRight(pParent);
                    swap(pParent, pCur);
                }
                
                // 情况三:叔叔节点不存在或为黑色,且当前节点是父节点的右孩子
                pParent->_color = BLACK;
                grandFather->_color = RED;
                _RotateLeft(grandFather);
            }
        }
    }
    // ...
}

动态效果演示:

  • 以升序(降序)插入构建红黑树
    在这里插入图片描述
    在这里插入图片描述

  • 随机插入构建红黑树
    在这里插入图片描述

4.2.6 红黑树的验证

红黑树的验证:
红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树 (中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质
bool IsValidRBTree()
{
     PNode pRoot = GetRoot();
     // 空树也是红黑树
     if (nullptr == pRoot)
         return true;
     // 检测根节点是否满足情况
     if (BLACK != pRoot->_color)
     {
         cout << "违反红黑树性质二:根节点必须为黑色" << endl;
         return false;
     }
     // 获取任意一条路径中黑色节点的个数
     size_t blackCount = 0;
     PNode pCur = pRoot;
     while (pCur)
     {
         if (BLACK == pCur->_color)
             blackCount++;
         pCur = pCur->_pLeft;
     }
     // 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
     size_t k = 0;
     return _IsValidRBTree(pRoot, k, blackCount);
}

bool _IsValidRBTree(PNode pRoot, size_t k, const size_t blackCount)
{
    //走到null之后,判断k和black是否相等
     if (nullptr == pRoot)
     {
         if (k != blackCount)
         {
             cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
             return false;
         }
         return true;
     }
     // 统计黑色节点的个数
     if (BLACK == pRoot->_color)
         k++;
     // 检测当前节点与其双亲是否都为红色
     PNode pParent = pRoot->_pParent;
     if (pParent && RED == pParent->_color && RED == pRoot->_color)
     {
         cout << "违反性质三:没有连在一起的红色节点" << endl;
         return false;
     }
     return _IsValidRBTree(pRoot->_pLeft, k, blackCount) && 
            _IsValidRBTree(pRoot->_pRight, k, blackCount);
}

4.2.7 红黑树的删除

🗑️ 红黑树的删除:
红黑树的删除本节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》
http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html

4.2.8 红黑树与AVL树的比较

📊 红黑树与AVL树的比较:
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

4.2.9 红黑树的应用

🌍 红黑树的应用:

  1. C++ STL库 – map/set、mutil_map/mutil_set
  2. Java库
  3. linux内核
  4. 其他一些库
    http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html

4.3 红黑树模拟实现STL中的map与set

4.3.1 红黑树的迭代器

🔍 红黑树的迭代器:
迭代器的好处是可以方便遍历,是数据结构的底层实现与用户透明。如果想要给红黑树增加迭代器,需要考虑以前问题:

  1. begin()与end()
    STL明确规定,begin()与end()代表的是一段前闭后开的区间,而对红黑树进行中序遍历后,可以得到一个有序的序列,因此:begin()可以放在红黑树中最小节点(即最左侧节点)的位置,end()放在最大节点(最右侧节点)的下一个位置,关键是最大节点的下一个位置在哪块?能否给成nullptr呢?答案是行不通的,因为对end()位置的迭代器进行–操作,必须要能找最后一个元素,此处就不行,因此最好的方式是将end()放在头结点的位置:
    在这里插入图片描述

  2. operator++()与operator–()

// 找迭代器的下一个节点,下一个节点肯定比其大
void Increasement()
{
    //分两种情况讨论:_pNode的右子树存在和不存在
    // 右子树存在
	 if(_pNode->_pRight)
	 {
	    // 右子树中最小的节点,即右子树中最左侧节点
		 _pNode = _pNode->_pRight;
		 while(_pNode->_pLeft)
		 _pNode = _pNode->_pLeft;
	 }
	 else
	 { 
	 // 右子树不存在,向上查找,直到_pNode != pParent->right
	  PNode pParent = _pNode->_pParent;
	  while(pParent->_pRight == _pNode)
	 {
		 _pNode = pParent;
		 pParent = _pNode->_pParent;
	 }
	 // 特殊情况:根节点没有右子树
	 if(_pNode->_pRight != pParent)
		 _pNode = pParent;
	 }
}
// 获取迭代器指向节点的前一个节点
void Decreasement()
{
     //分三种情况讨论:_pNode 在head的位置,_pNode 左子树存在,_pNode 左子树不
存在
     // 1. _pNode 在head的位置,--应该将_pNode放在红黑树中最大节点的位置
	 if(_pNode->_pParent->_pParent == _pNode && _pNode->_color == RED)
		 _pNode = _pNode->_pRight;
	 else if(_pNode->_pLeft)
	 {
	    // 2. _pNode的左子树存在,在左子树中找最大的节点,即左子树中最右侧节点
		 _pNode = _pNode->_pLeft;
		 while(_pNode->_pRight)
		 _pNode = _pNode->_pRight;
	 }
	 else
	 {
	    // _pNode的左子树不存在,只能向上找
		 PNode pParent = _pNode->_pParent;
		 while(_pNode == pParent->_pLeft)
		 {
			 _pNode = pParent;
			 pParent = _pNode->_pParent;
		 }
		 _pNode = pParent;
	}
}
4.3.2 改造红黑树
  1. 在红黑树中需要增加模板参数,用来接收Key和Value的类型
  2. 为了区分map和set的不同,在红黑树的节点中,data的类型可能是键值对,也可能是Key
  3. 当红黑树中存储的是键值对时,需要通过Key来比较大小,因此需要增加一个模板参数KeyOfValue来获取data中的Key
  4. 在插入时,需要按照Key的大小进行比较,因此需要增加一个模板参数Compare来比较Key的大小
// 因为关联式容器中存储的是<key, value>的键值对,因此
// k为key的类型,
// ValueType: 如果是map,则为pair<K, V>; 如果是set,则为k
// KeyOfValue: 通过value来获取key的一个仿函数类
// 红黑树的节点
template<class T>
struct RBTreeNode
{
    RBTreeNode(const T& data = T(), Color color = RED)
        : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
        , _data(data), _color(color)
    {}
    RBTreeNode<T>* _pLeft;
    RBTreeNode<T>* _pRight;
    RBTreeNode<T>* _pParent;
    T _data;
    Color _color;
};

// 红黑树的迭代器
template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
    typedef RBTreeNode<T> Node;
    typedef RBTreeIterator<T, Ref, Ptr> Self;
    
    Node* _node;
    
    RBTreeIterator(Node* node = nullptr)
        : _node(node)
    {}
    
    Ref operator*()
    {
        return _node->_data;
    }
    
    Ptr operator->()
    {
        return &_node->_data;
    }
    
    Self& operator++()
    {
        // 中序遍历的下一个节点
        // ...
        return *this;
    }
    
    Self operator++(int)
    {
        Self temp(*this);
        ++(*this);
        return temp;
    }
    
    Self& operator--()
    {
        // 中序遍历的前一个节点
        // ...
        return *this;
    }
    
    Self operator--(int)
    {
        Self temp(*this);
        --(*this);
        return temp;
    }
    
    bool operator!=(const Self& s) const
    {
        return _node != s._node;
    }
    
    bool operator==(const Self& s) const
    {
        return _node == s._node;
    }
};

// 红黑树
template<class K, class V, class KeyOfValue, class Compare = less<K>>
class RBTree
{
public:
    typedef RBTreeNode<V> Node;
    typedef RBTreeIterator<V, V&, V*> iterator;
    typedef RBTreeIterator<V, const V&, const V*> const_iterator;
    
    // ...
};
4.3.3 map的模拟实现

🗺️ map的模拟实现:
map的底层结构就是红黑树,因此在map中直接封装一棵红黑树,然后将其接口包装下即可。

namespace mymap
{
    template<class K, class V>
    class map
    {
        // 用于从pair<K, V>中提取Key
        struct MapKeyOfValue
        {
            const K& operator()(const pair<K, V>& kv)
            {
                return kv.first;
            }
        };
    public:
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfValue>::iterator iterator;
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfValue>::const_iterator const_iterator;
        
        iterator begin()
        {
            return _t.begin();
        }
        
        iterator end()
        {
            return _t.end();
        }
        
        const_iterator begin() const
        {
            return _t.begin();
        }
        
        const_iterator end() const
        {
            return _t.end();
        }
        
        pair<iterator, bool> insert(const pair<K, V>& kv)
        {
            return _t.Insert(kv);
        }
        
        V& operator[](const K& key)
        {
            pair<iterator, bool> ret = insert(make_pair(key, V()));
            return ret.first->second;
        }
        
        iterator find(const K& key)
        {
            return _t.Find(key);
        }
        
        bool erase(const K& key)
        {
            return _t.Erase(key);
        }
        
        size_t size() const
        {
            return _t.Size();
        }
        
        bool empty() const
        {
            return _t.Empty();
        }
        
        void clear()
        {
            _t.Clear();
        }
        
        void swap(map<K, V>& m)
        {
            _t.Swap(m._t);
        }
        
    private:
        RBTree<K, pair<const K, V>, MapKeyOfValue> _t;
    };
}
4.3.4 set的模拟实现

set的底层为红黑树,因此只需在set内部封装一棵红黑树,即可将该容器实现出来(具体实现可参
考map)。

namespace myset
{
  template<class K>
	 class set
	 {
			 typedef K ValueType;
			 // 作用是:将value中的key提取出来
			 struct KeyOfValue
			 {
				 const K& operator()(const ValueType& key)
				 { return key;}
			 };
			 // 红黑树类型重命名
			 typedef RBTree<K, ValueType, KeyOfValue> RBTree;
		 public:
			 typedef typename RBTree::Iterator iterator;
		 public:
			 Set(){}
			 /////////////////////////////////////////////
			 // Iterator
			 iterator Begin();
			 iterator End();
			 /////////////////////////////////////////////////
			 // Capacity
			 size_t size()const;
			 bool empty()const;
			 ////////////////////////////////////////////////////
			 // modify
			 pair<iterator, bool> insert(const ValueType& data)
			 {
			 	return _t.Insert(data);
			 }
			 void clear();
			 iterator find(const K& key

		  private:
			 RBTree _t;
	 };
}

📝 总结

本文系统地介绍了 C++ STL 中树形结构的关联式容器及其底层实现,核心要点如下:

🔑 核心知识点回顾

知识点 关键内容
关联式容器 存储 <key, value> 键值对,检索效率高于序列式容器
键值对 pair first(键)和 second(值)组成,是 map 的基本数据单元
set 只存 value(即 key),元素唯一且自动排序,可用于去重
map 存储键值对,key 唯一,支持 operator[] 下标访问
multiset / multimap 允许重复键/值,不支持 operator[]
AVL 树 严格平衡二叉树,左右子树高度差 ≤ 1,四种旋转(左/右/左右/右左)维持平衡
红黑树 近似平衡二叉树,通过颜色约束(根黑、不红红、黑路同)保证最长路径 ≤ 2 倍最短路径
红黑树 vs AVL 树 红黑树旋转次数更少,实际应用更广(STL、Java、Linux 内核均采用)

🎯 面试重点速记

  1. 关联式容器 vs 序列式容器:前者存键值对,后者存元素本身
  2. pair 的三种构造方式:直接构造、列表初始化、make_pair
  3. set 去重原理:底层红黑树保证 key 唯一
  4. map 的 operator[] 原理:key 不存在时自动插入默认值并返回引用
  5. AVL 树四种旋转场景:左左(右单旋)、右右(左单旋)、左右(先左后右)、右左(先右后左)
  6. 红黑树五条性质:节点非红即黑、根黑、不红红、黑路同、叶黑
  7. 红黑树插入三种情况:红叔(染色上移)、黑叔直线(单旋+染色)、黑叔折线(双旋+染色)

💡 一句话总结

关联式容器以红黑树为底层,实现了 O(log n) 的高效查找;AVL 树追求绝对平衡,红黑树追求近似平衡,后者因旋转次数更少而成为工业界的主流选择。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐