【C++】map 和 set(二叉搜索树、AVL树、红黑树)
- 关联式容器
- 键值对
- 树形结构的关联式容器
- 底层数据结构
- 二叉搜索树
- AVL树
- 红黑树
- [map 和 set 的封装](#map 和 set 的封装)
关联式容器
在之前的文章中,我们了解并模拟实现了一些序列式容器 ,例如 vector、list 等。这些容器的底层都是线性数据结构 ,所以叫做序列式容器,其中存储的都是数据本身,数据与数据之间并没有太强的关联性
而关联式容器 ,不仅可以存储数据本身,还可以用来表示数据与数据之间的关系,体现出它们的关联性,而且在搜索时比序列式容器的效率高很多
键值对
关联式容器中存储的是 <key, value> 形式的键值对结构,这种结构在 STL 中叫做 pair,定义如下
cpp
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair(): first(T1()), second(T2())
{}
pair(const T1& a, const T2& b): first(a), second(b)
{}
};
pair 有两个成员变量:first 和 second,其中 first 表示键key ,second 表示与 key 对应的值value
以下是 pair 的使用方法
cpp
vector<pair<int, int>> v;
pair<int, int> p1(1, 1); // 1.构造有名对象
v.push_back(p1);
v.push_back(pair<int, int>(2, 2)); // 2.构造匿名对象
v.push_back(make_pair(3, 3)); // 3.调用 make_pair
v.push_back({ 4,4 }); // 4.隐式类型转换
// 输出
for (auto e : v)
{
cout << e.first << "->" << e.second << endl;
}
其中,make_pair()
是一个函数,用来构造一个pair对象
树形结构的关联式容器
根据不同的场景和使用需求,关联式容器分为两种:树形结构 的关联式容器和哈希结构的关联式容器
而 map 和 set 就属于树形结构,它们的底层数据结构一般都是红黑树
set
相关文档:set
简介
set 有如下特性:
- set 中只会存储 key,它的 key 就是它的 value
- set 中每个元素的 key 都是唯一的,set 中不允许出现重复元素,因此 set 可以用来数据去重
- set 中的元素不允许修改,但是允许插入和删除元素
- 中序遍历 set,可以得到有序的序列
- set 的查找数据效率是 O(log n)
使用
模板参数
- T:set 中的元素类型
- Compare:set 中元素的比较逻辑,默认是小于
- Alloc:set 中元素的管理方式,使用默认即可
构造
函数声明 | 功能介绍 |
---|---|
set (const Compare& comp = Compare(), const Allocator& = Allocator()); | 构造空的 set |
set (Inputlterator first, Inputlterator last, const Compare& comp = Compare(), const Allocator& = Allocator()); | 用[first,last)区间中的元素构造 set |
set (const set<Key,Compare, Allocator>& x); | set 的拷贝构造 |
cpp
vector<int> v = { 1,2,3,4,5,6,7 };
// 迭代器区间构造
set<int> s1(v.begin(), v.end());
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
迭代器
函数声明 | 功能介绍 |
---|---|
iterator begin() | 返回 set 中起始位置元素的迭代器 |
iterator end() | 返回 set 中最后一个元素下一个位置的迭代器 |
reverse_iterator rbegin() | 返回 set 第一个元素的迭代器,即 end |
reverse_iterator rend() | 返回 set 最后一个元素下一个位置的反向迭代器,即 begin |
容量
函数声明 | 功能介绍 |
---|---|
bool empty() const | 检测 set 是否为空,空返回 true,否则返回 false |
size_type size() const | 返回 set 中有效元素的个数 |
修改
函数声明 | 功能介绍 |
---|---|
pair<iterator, bool> insert(const value_type& x) | 在 set 中插入元素 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 中的元素清空 |
一般情况下,insert 的返回值类型都是 bool 类型,用来表示插入成功或者插入失败。
而这里的 insert 的返回值类型是一个 pair<iterator, bool>,其中
- bool 用来表示插入成功或者失败 ,iterator则是指向插入元素的迭代器,方便我们对元素进行修改
但是 set 不支持修改元素,只能当作 find 来使用
cpp
set<string> s1;
string s;
while (cin >> s)
{
auto ret = s1.insert(s);
if (ret.second)
cout << "插入成功:" << *(ret.first) << endl;
else
cout << "插入失败,key已存在:" << *(ret.first) << endl;
}
修改操作在 map 中是允许的,所以这种写法在 map 中就会体现出它的精妙之处
其他操作
函数声明 | 功能介绍 |
---|---|
iterator find (const key_type& key) const; | 查找指定元素 key,找到就返回它的迭代器,找不到则返回 end() 迭代器 |
size_type count (const key_type& key) const; | 统计指定键值 key 在容器中出现的次数,因为 set 不允许重复元素,所以结果只能是 1 或 0 |
iterator lower_bound (const key_type& key) const; | 返回一个迭代器,指向第一个大于等于给定键值 key 的元素;如果没有,则返回 end() |
iterator upper_bound(const key_type& key); | 返回一个迭代器,指向第一个大于给定键值 key 的元素;如果没有,则返回 end() |
pair<iterator,iterator> equal_range (const key_type& key) const; | 返回一个包含两个迭代器的 pair,这两个迭代器分别指向范围等于给定键值的第一个元素和最后一个元素之后的位置 |
lower_bound 和 upper_bound 通常都是一起配合使用,寻找一个左闭右开的区间 [first, last)。例如有一个set = [1,2,3,4,5,6,7],通过lower_bound(3) 和 upper_bound(5)就可以找到区间 [3, 6)
cpp
vector<int> v = { 1,2,3,4,5,6,7 };
// 迭代器区间构造
set<int> s1(v.begin(), v.end());
// 寻找 [3, 6) 区间
set<int>::iterator first = s1.lower_bound(3);
auto last = s1.upper_bound(5);
// 输出
while (first != last)
{
cout << *first << " ";
++first;
}
cout << endl;
equal_range 寻找的是一个范围,范围中的元素键值相同。由于 set 不允许键值重复,所以这个范围一般只有一个元素或者为空
map
map 的许多特性和 set 是相同的,因此在 set 讲过的东西可能在这里就不再详细说明了
相关文档:map
简介
- map 中存储的是键值对<key, value>
- map 中每个元素的 key 都是唯一的,不允许出现重复 key,但是不同 key 对应的 value 可以重复
- map 中的元素的 key 不允许修改,但是允许修改 value,也可以进行元素的插入与删除
- 中序遍历 map,可以得到有序的序列
- map 的查找数据效率是 O(log n)
使用
模板参数
- Key:键值对中 key 的类型
- T:键值对中 value 的类型
- Compare:set 中元素的比较逻辑,默认是小于
- Alloc:set 中元素的管理方式,使用默认即可
构造
与 set 一样,map 有空构造、迭代器区间构造、拷贝构造,这里演示一下迭代器区间构造
迭代器
函数声明 | 功能介绍 |
---|---|
begin()和 end() | begin:首元素的位置,end:最后一个元素的下一个位置 |
rbegin()和 rend() | 反向迭代器,rbegin 在 end 位置,rend 在 begin 位置,其 ++和 -- 操作与 begin 和 end 操作移动相反 |
容量
函数声明 | 功能简介 |
---|---|
bool empty() const | 检测 map 中的元素是否为空,是返回 true,否则返回 false |
size_type size() const | 返回 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 中的元素清空 |
operator[]
map 支持[]
访问。[]内填的不是下标,而是键值key,通过 key 可以访问到对应的 value,如下
而 operator[] 是这样实现的
cpp
mapped_type& operator[] (const key_type& key)
{
return (*((this->insert(make_pair(key, mapped_type()))).first)).second;
}
看着有点复杂,尝试拆分一下:
cpp
// 伪代码
mapped_type& operator[] (const key_type& key)
{
pair<iterator, bool> ret = insert(make_pair(key, mapped_type()));
iterator it = ret.first;
return it->second;
}
mapped_type 表示键值对中的值value的类型
insert 不管插入成功还是失败,都会返回一个 pair<iterator, bool>,其中 iterator 是指向key所在节点的迭代器,bool 表示插入成功或失败。
- key不存在,插入成功,返回 pair<新插入key所在节点的迭代器,true>
- key存在,插入失败,返回 pair<存在的key所在节点的迭代器,false>
通过 pair 中的迭代器,可以访问到节点的值value
再来看下面这条语句的作用
cpp
insert(make_pair(key, mapped_type()))
如果使用 operator[key],key 不存在,进行插入,只不过插入的是默认构造出的值。例如:value类型为string,则插入空串""
;类型为 int,则插入0等
那么这有什么用呢?------可以用来统计键值的出现次数
例如,有一组单词,我们可以用 set 来统计每个单词的频次,代码可以这样写:
cpp
vector<string> v = { "string",{"left"},{"right"},{"string"},{"left"},{"right"},{"string"},{"string"} };
map<string, int> s;
// 统计
for (auto& e : v)
{
auto it = s.find(e);
if (it != s.end()) // 单词存在
it->second++;
else
s.insert({ e,1 }); // 单词不存在
}
// 打印
for (auto e : s)
{
cout << e.first << ":" << e.second << endl;
}
如果使用 operator[],代码就会简单很多:
- 存在就++
- 不存在,先插入0,再++
cpp
vector<string> v = { "string",{"left"},{"right"},{"string"},{"left"},{"right"},{"string"},{"string"} };
map<string, int> s;
// 统计
for (auto& e : v)
{
//auto it = s.find(e);
//if (it != s.end()) // 单词存在
// it->second++;
//else
// s.insert({ e,1 }); // 单词不存在
s[e]++;
}
// 打印
for (auto e : s)
{
cout << e.first << ":" << e.second << endl;
}
测试:
其他操作
函数声明 | 功能介绍 |
---|---|
iterator find (const key_type& key) const; | 查找键值为 key 的元素,找到就返回它的迭代器,找不到则返回 end() 迭代器 |
size_type count (const key_type& key) const; | 统计指定键值 key 在容器中出现的次数 |
iterator lower_bound (const key_type& key) const; | 返回一个迭代器,指向第一个大于等于给定键值 key 的元素;如果没有,则返回 end() |
iterator upper_bound(const key_type& key); | 返回一个迭代器,指向第一个大于给定键值 key 的元素;如果没有,则返回 end() |
pair<iterator,iterator> equal_range (const key_type& key) const; | 返回一个包含两个迭代器的 pair,这两个迭代器分别指向范围等于给定键值的第一个元素和最后一个元素之后的位置 |
multiset 和 multimap
multiset 与 set 的区别就是:multiset 的元素可以重复,而 set 中的元素是唯一的;它们的接口使用起来类似,这里不再一一列举
multimap 和 map 的区别也是一样的:multimap 键值可以重复,map 键值不可重复,但是需要注意:multimap 并没有重载 operator[]
因为 multimap 中的不同元素的键值可能相同,使用键值访问元素时不知道该访问哪个,会引发歧义
底层数据结构
在 STL 中,map 和 set 通常采用红黑树 作为底层实现,说到红黑树,就不得不说 AVL树 ,它俩都是二叉搜索树,下面我们就来看一下这三棵树
二叉搜索树
概念
二叉搜索树也叫二叉排序树,它是一棵特殊的二叉树,有如下性质
- 二叉搜索树可以是一棵空树,如果不为空,则:
- 若左子树不为空,左子树 的所有节点的键值 均小于根节点的键值
- 若右子树不为空,右子树 的所有节点的键值 均大于根节点的键值
- 左右子树也都是二叉搜索树
- 对二叉搜索树进行中序遍历,遍历出的数据都是有序的,如下图进行中序遍历:[1, 3, 4, 6, 7, 8, 10, 13, 14]
操作
查找
- 从根节点开始寻找指定值
- 若指定值比当前节点大 ,则向右边寻找
- 若指定值比当前节点小 ,则向左边寻找
- 最多查找树的高度次,就可以找到指定数据;走到空还没有找到,说明值不存在
在二叉搜索树中寻找7
在二叉搜索树中寻找不存在的11
插入
- 树为空,直接插入值即可
- 树不为空,根据二叉搜索树的性质,寻找合适的位置插入
- 从根节点开始寻找
- 插入值比当前节点大,向右走;插入值比当前节点小,向左走
- 直到找到空位置
依次插入0,9
删除
删除值比较麻烦,分为下面三种情况:
- 删除的节点左右子树为空,也就是叶子节点
- 删除的节点左为空 或者右为空
- 删除的节点左右都不为空
其实前两种情况可以合并为一种情况,假设要删除的节点是cur,以下是删除方法:
-
cur的左为空或者右为空
a. 左子树为空,由 cur 的父节点 parent 接管 cur 的右子树,不管右子树为不为空
b. 右子树为空,由 cur 的父节点 parent 接管 cur 的左子树,不管左子树为不为空
-
cur的左右都不为空,例如删除3
这种情况下,不可以先删除cur,再由 parent 接管 cur 的子树了,需要用到替换删除法 :找到一个节点与cur交换,然后删除替代节点。详细操作可以看实现部分
实现
在实现之前,我们使用命名空间隔离一下
cpp
namespace key
{
};
结构
树节点中包含指向左右子树的指针,还有键值
cpp
template <class K>
struct BSTreeNode
{
BSTreeNode(const K& key = K())
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
二叉搜索树的成员变量只有一个节点指针,代表树的根节点
cpp
template <class K>
class BSTree
{
typedef BSTreeNode<K> Node;
private:
Node* _root = nullptr;
};
插入
- 树为空,直接插入值即可
- 树不为空,根据二叉搜索树的性质,寻找合适的位置插入
- 从根节点开始寻找
- 插入值比当前节点大,向右走;插入值比当前节点小,向左走
- 直到找到空位置
- 插入成功返回 true,失败返回 false
- 一般二叉搜索树不允许插入重复的数据,因为二叉搜索树的作用就是查看某个值在不在结构中,重复值没有意义
注意:
- 定义 cur 来寻找合适的插入位置,定义 parent 作为 cur 的父节点
- 找到合适位置后,判断 cur 是 parent 的左子树还是右子树,然后进行 parent 与 cur 的链接
cpp
bool insert(const K& key)
{
// 树为空
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
// 树不为空
// 寻找合适插入位置
Node* cur = _root, * parent = nullptr;
while (cur)
{
if (key > cur->_key)
{
// 插入值 > 当前节点
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
// 插入值 < 当前节点
parent = cur;
cur = cur->_left;
}
else
return false; // 不允许插入重复值
}
// 找到插入位置
cur = new Node(key);
// 判断 cur 是 parent 的左子树还是右子树,链接
if (cur->_key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
测试:
cpp
void test1()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
key::BSTree<int> t1;
for (auto e : a)
t1.insert(e);
}
暂时没发现错误
中序遍历
我们可以写一个中序遍历,把结果打印出来看看
- 中序遍历需要从根节点开始
- 写一个公有函数 inOrder(),一个私有函数 _inOrder()
- 公有函数 inOrder() 负责将根节点 _root 传递给私有函数 _inOrder()
- 私有函数 _inOrder() 负责递归地实现中序遍历的逻辑
为什么要这样写呢?如果只实现一个公有函数 inOrder() 可不可以呢?
cpp
void inOrder(Node* root)
{
inOrder(root->_left);
inOrder(root->_right);
cout << root->_key << " ";
}
中序遍历是从根节点开始遍历的,我们在外部调用时拿不到根节点 _root
cpp
BSTree<int> t1;
t1.inOrder(_root); // 拿不到 _root
那如果我们给 inOrder 的参数设置一个缺省值 _root 呢?
cpp
void inOrder(Node* root = _root)
{
//...
}
这样也是不行的,我们想拿到 _root 要通过隐含的 this 指针 ,但是 this 指针不可以在函数的参数列表 直接使用,只能在函数体中使用
当然也可以写一个 GetRoot 来允许外部拿到 _root,Java就很喜欢这样做,C++不太常用这种方式
所以还是写一个公有函数 inOrder(),再写一个私有函数 _inOrder()
cpp
public:
void inOrder()
{
_inOrder(_root);
cout << endl;
}
private:
void _inOrder(Node* root)
{
if (root == nullptr) return;
_inOrder(root->_left);
cout << root->_key << " ";
_inOrder(root->_right);
}
测试:
查找
找到指定值就返回 true,找不到返回 false
- 树为空,直接返回 false
- 树不为空,根据二叉搜索树的性质,寻找指定值
- 插入值比当前节点大,向右走;插入值比当前节点小,向左走
- 直到找到指定值,返回 true
可以直接把 insert 的代码拿来用
cpp
bool find(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树不为空
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
// key值 > 当前节点
cur = cur->_right;
}
else if (key < cur->_key)
{
// 插入值 < 当前节点
cur = cur->_left;
}
else
return true; // 找到
}
// 找不到
return false;
}
测试:
删除
删除指定值,成功返回 true,失败返回 false
删除分为以下情况:
- 删除的节点左右子树为空,也就是叶子节点
- 删除的节点左为空或者右为空
- 删除的节点左右都不为空
其中情况2可以覆盖情况1,所以两种情况算作一种情况:
- 删除的节点左为空或者右为空
- 删除的节点左右都不为空
下面来看一下,两种情况的实现
-
cur的左为空或者右为空
a. 左子树为空,由 cur 的父节点 parent 接管 cur 的右子树,不管右子树为不为空;同时 cur 有可能是 parent 的左子树或者右子树,记得判断,再由 parent 的左或者右接管 cur 的右子树
b. 右子树为空,由 cur 的父节点 parent 接管 cur 的左子树,不管左子树为不为空;同时 cur 有可能是 parent 的左子树或者右子树,记得判断,再由 parent 的左或者右接管 cur 的左子树
cpp
bool erase(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树非空,找到指定值并删除
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到,删除
// 1.a cur 的左子树为空,parent 接管 cur 的右子树
if (cur->_left == nullptr)
{
// 判断 cur 是左子树还是右子树
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
}
else if (cur->_right == nullptr)
{
// 1.b cur 的右子树为空,parent接管 cur 的左子树
// 判断 cur 为左还是右
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
}
else
{
// 2. cur左右不为空
}
return true;
}
}
// 找不到
return false;
}
- cur的左右都不为空,例如删除3
这种情况下,不可以先删除cur,再由 parent 接管 cur 的子树了,需要用到替换删除法:找到一个节点与cur交换,然后删除替代节点
为了维持二叉搜索树的性质,这个节点的值要比 cur 左子树都大,比 cur 的右子树都小。有两种选择:
- cur 左子树的最右节点 ,这个节点是 cur 的左子树中最大的,同时小于 cur 的右子树
- cur 右子树的最左节点 ,这个节点是 cur 的右子树中最小的,同时大于 cur 的左子树
这里使用右子树的最左节点,将其命名为 rightMin,其父节点为 rightMinParent
为什么需要 rightMin 的父节点呢?因为 rightMin 虽然是最左节点,但这不代表它没有子树 ,它的子树可以都是右子树
在 cur 与 rightMin 交换,删除 rightMin 之后,rightMinParent 的左需要接管 rightMin 的右子树
cpp
else
{
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = nullptr;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// rightMinParent 的左树接管 rightMin 的右树
rightMinParent->_left = rightMin->_right;
// 删除
delete rightMin;
}
至此,完成了第一版代码,为什么叫第一版呢?因为还存在一些小 bug
cpp
bool erase(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树非空,找到指定值并删除
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到,删除
// 1.a cur 的左子树为空,parent 接管 cur 的右子树
if (cur->_left == nullptr)
{
// 判断 cur 是左子树还是右子树
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
}
else if (cur->_right == nullptr)
{
// 1.b cur 的右子树为空,parent接管 cur 的左子树
// 判断 cur 为左还是右
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
}
else
{
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = nullptr;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// rightMinParent 的左树接管 rightMin 的右树
rightMinParent->_left = rightMin->_right;
// 删除
delete rightMin;
}
return true;
}
}
// 找不到
return false;
}
bug1
先来测试一下:
目前可以正常运行,但是一旦删除根节点,就会出问题
经过调试,可以在看到问题出在 rightMinParent 上
当删除根节点时,根据我们的代码,此时 rightMinParent 和 rightMin 的情况如下
结合代码,此时 rightMin 已经是最左节点,可以进行替换删除了,然后 rightMinParent 接管 rightMin 的右子树,但是此时的 rightMinParent 是空指针 ,所以会发生错误。解决方法就是一开始把 rightMinParent 设置为 cur
cpp
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = cur; // 初始化为 cur
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// rightMinParent 的左树接管 rightMin 的右树
rightMinParent->_left = rightMin->_right;
// 删除
delete rightMin;
还有另一个问题就是:代码默认是 rightMinParent 的左子树 接管 rightMin 的右子树,但是在这种删除头节点的情况下,显然是不对的
所以在接管之前,要判断 rightMin 是 rightMinParent 的左子树还是右子树
cpp
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// 判断 rightMin 是 rightMinParent 的左子树还是右子树
if (rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
// 删除
delete rightMin;
到这里,完成了第二版代码,先别急,还有一点点小bug
cpp
bool erase(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树非空,找到指定值并删除
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到,删除
// 1.a cur 的左子树为空,parent 接管 cur 的右子树
if (cur->_left == nullptr)
{
// 判断 cur 是左子树还是右子树
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
}
else if (cur->_right == nullptr)
{
// 1.b cur 的右子树为空,parent接管 cur 的左子树
// 判断 cur 为左还是右
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
}
else
{
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// 判断 rightMin 是 rightMinParent 的左子树还是右子树
if (rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
// 删除
delete rightMin;
}
return true;
}
}
// 找不到
return false;
}
测试,删除头节点:
可以正常运行
bug2
现在尝试把树中的值全部删除
在删除最后一个数据时出错了,进入调试查看一下
又是由于父节点是空指针 造成的,不过这次是删除情况1 ,上次是删除情况2,此时的parent和cur的状态如下
parent 为空,cur 根本没有父节点,但是代码还是对父节点进行了解引用,引发空指针错误
解决方法:只需要判断 cur 是否是根节点即可,是根节点,就直接删除 cur,根节点_root 指向子树,具体是指向左树还是右树,根据实际情况判断即可
cpp
else
{
// 找到,删除
// 1.a cur 的左子树为空,parent 接管 cur 的右子树
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
// 判断 cur 是左子树还是右子树
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
// 1.b cur 的右子树为空,parent接管 cur 的左子树
// 判断 cur 为左还是右
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else
{
// 2. cur左右不为空,替换法
}
以下是终版删除代码
cpp
bool erase(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树非空,找到指定值并删除
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到,删除
// 1.a cur 的左子树为空,parent 接管 cur 的右子树
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
// 判断 cur 是左子树还是右子树
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
// 1.b cur 的右子树为空,parent接管 cur 的左子树
// 判断 cur 为左还是右
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else
{
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// 判断 rightMin 是 rightMinParent 的左子树还是右子树
if (rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
// 删除
delete rightMin;
}
return true;
}
}
// 找不到
return false;
}
测试:
总结:两个 bug 一个出现在情况1,一个出现在情况2,都是因为删除根节点 时,父节点为空 ,对父节点解引用造成了空指针问题
代码
cpp
#pragma once
namespace key
{
template <class K>
struct BSTreeNode
{
BSTreeNode(const K& key = K())
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
template <class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool insert(const K& key)
{
// 树为空
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
// 树不为空
// 寻找合适插入位置
Node* cur = _root, * parent = nullptr;
while (cur)
{
if (key > cur->_key)
{
// 插入值 > 当前节点
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
// 插入值 < 当前节点
parent = cur;
cur = cur->_left;
}
else
return false; // 不允许插入重复值
}
// 找到插入位置
cur = new Node(key);
// 判断 cur 是 parent 的左子树还是右子树,链接
if (cur->_key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
bool find(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树不为空
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
// key值 > 当前节点
cur = cur->_right;
}
else if (key < cur->_key)
{
// 插入值 < 当前节点
cur = cur->_left;
}
else
return true; // 找到
}
// 找不到
return false;
}
bool erase(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树非空,找到指定值并删除
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到,删除
// 1.a cur 的左子树为空,parent 接管 cur 的右子树
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
// 判断 cur 是左子树还是右子树
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
// 1.b cur 的右子树为空,parent接管 cur 的左子树
// 判断 cur 为左还是右
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else
{
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// 判断 rightMin 是 rightMinParent 的左子树还是右子树
if (rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
// 删除
delete rightMin;
}
return true;
}
}
// 找不到
return false;
}
void inOrder()
{
_inOrder(_root);
cout << endl;
}
private:
void _inOrder(Node* root)
{
if (root == nullptr) return;
_inOrder(root->_left);
cout << root->_key << " ";
_inOrder(root->_right);
}
Node* _root = nullptr;
};
}
应用
K模型
K模型就是树节点中只存键值key,可以用来查看某个键值是否存在
例如判断单词拼写是否正确:
- 将词库中的单词作为 key,构建一棵二叉搜索树
- 检查某个单词是否正确,就只需要在树中搜索:若能搜索到,拼写正确;搜索不到,拼写错误
cpp
void testSpell()
{
// 建立词库搜索树
key::BSTree<string> t;
t.insert("string");
t.insert("tree");
t.insert("node");
t.insert("word");
// 检查拼写
string s;
while (cin >> s)
{
if (t.find(s))
cout << "拼写正确" << endl;
else
cout << "拼写错误" << endl;
}
}
KV模型
每一个键key 都有与之对应的值value ,也就是键值对<key, value>
通过键值对可以做到英汉单词互译
- 将英语单词作为 key,中文作为 val,建立二叉搜索树
- 输入英语单词,即可输出相应的中文
先将我们上面写的二叉搜索树改为 KV模型
- 在模板中加入另一个参数,V
- 在树节点中加入值 _val
- 修改find:找到就返回节点指针,找不到返回 nullptr
- 修改insert:new 节点时,加入值val
cpp
namespace key_val
{
template <class K, class V>
struct BSTreeNode
{
BSTreeNode(const K& key = K(), const V& val = V())
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _val(val)
{}
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key;
V _val;
};
template <class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
bool insert(const K& key, const V& val)
{
// 树为空
if (_root == nullptr)
{
_root = new Node(key, val);
return true;
}
// 树不为空
// 寻找合适插入位置
Node* cur = _root, * parent = nullptr;
while (cur)
{
if (key > cur->_key)
{
// 插入值 > 当前节点
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
// 插入值 < 当前节点
parent = cur;
cur = cur->_left;
}
else
return false; // 不允许插入重复值
}
// 找到插入位置
cur = new Node(key, val);
// 判断 cur 是 parent 的左子树还是右子树,链接
if (cur->_key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
Node* find(const K& key)
{
// 树为空
if (_root == nullptr)
return nullptr;
// 树不为空
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
// key值 > 当前节点
cur = cur->_right;
}
else if (key < cur->_key)
{
// 插入值 < 当前节点
cur = cur->_left;
}
else
return cur; // 找到
}
// 找不到
return nullptr;
}
bool erase(const K& key)
{
// 树为空
if (_root == nullptr)
return false;
// 树非空,找到指定值并删除
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 找到,删除
// 1.a cur 的左子树为空,parent 接管 cur 的右子树
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
// 判断 cur 是左子树还是右子树
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
// 1.b cur 的右子树为空,parent接管 cur 的左子树
// 判断 cur 为左还是右
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else
{
// 2. cur左右不为空,替换法
// 找到cur右子树的最左节点 rightMin,还有它的父节点 rightMinParent
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 找到 rightMin,与 cur 交换
swap(rightMin->_key, cur->_key);
// 判断 rightMin 是 rightMinParent 的左子树还是右子树
if (rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
// 删除
delete rightMin;
}
return true;
}
}
// 找不到
return false;
}
void inOrder()
{
_inOrder(_root);
cout << endl;
}
private:
void _inOrder(Node* root)
{
if (root == nullptr) return;
_inOrder(root->_left);
cout << root->_key << " ";
_inOrder(root->_right);
}
Node* _root = nullptr;
};
}
测试:
cpp
void testKV()
{
key_val::BSTree<string, string> t;
t.insert("string", "字符串");
t.insert("node", "节点");
t.insert("tree", "树");
string s;
while (cin >> s)
{
auto ret = t.find(s);
if (ret != nullptr)
cout << ret->_key << ":" << ret->_val << endl;
else
cout << "抱歉,单词库中无此单词" << endl;
}
}
性能
在二叉搜索树的插入、删除操作中都用到了查找,所以查找的效率可以代表树的操作效率
对于如下数据
cpp
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
如果插入的顺序不同,得到的二叉搜索树也不同,它们的查找效率也不同
- 二叉搜索树是完全二叉树,或者接近完全二叉树,查找效率是最高的,最多查找树的高度次,时间复杂度接近 O(log n)
- 二叉搜索树是单支树,或者接近单支树,查找效率最低,接近 O(n)
也就是说,如果二叉搜索树退化成了单支树 ,效率就会大大降低,那有没有办法尽可能平衡一下二叉搜索树,让它接近完全二叉树呢?
为了满足这个需求,在二叉搜索树的基础上衍生出了AVL树和红黑树
AVL树
概念
AVL树是由两位俄罗斯的数学家G.M.A delson-V elskii和E.M.Landis在1962年发明的,方法如下:
- 如果在插入节点时,对树的结构进行调整,使得每个节点的左右子树的高度差不超过1 ,就可以尽可能限制树的高度,减少搜索次数了
AVL树可以是空树,如果不是空树,则具有以下性质:
- 左子树和右子树的高度差的绝对值不超过1
- 左子树和右子树都是 AVL树
AVL树一般都会给节点设置一个平衡因子 ,以表示左右子树的高度差 ,这里采用右子树高度减左子树高度作为平衡因子
下面我们边了解 AVL树 边写实现代码,AVL树是在二叉搜索树的基础上修改的,这里只讲 AVL 树的部分,涉及到二叉搜索树的东西这里就不再赘述
节点的定义
在二叉搜索树的节点基础上稍作调整就可以得到AVL树的节点
- 添加 _parent 指针,用来指向父节点
- 添加 _bf(balance factor),用来表示当前节点的平衡因子
- 将 KV 模型中的 _key 和 _value 合并为一个 pair 对象
如下:
cpp
template <class K, class V>
struct AVLTreeNode
{
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _kv(kv)
, _bf(0)
{}
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
pair<K, V> _kv;
int _bf;
};
插入
AVL树的插入分为两步:
- 按照二叉搜索树的规则插入节点
- 调整节点的平衡因子
以下是调整平衡因子的步骤
当插入一个节点 cur 时,cur的平衡因子一定是0,因为它没有左右子树。而 cur 的父节点 parent,就需要调整平衡因子了:cur 是 parent 的右子树 ,平衡因子++ ;是左子树 ,平衡因子-- 。插入节点后,parent 的平衡因子有如下情况:
- 平衡因子为 0
因为 AVL 树的平衡因子不会大于2或者小于-2 ,所以插入后平衡因子为0的情况必然是从 -1 或者 1 转换来的
以5为根节点的树可能是一棵子树 ,高度变化可能会影响当前树的祖先节点的平衡因子
但是当前树的高度在插入新节点前后没有发生变化,所以不会影响祖先节点,因此不需要向上更新祖先节点的平衡因子,插入操作运行到这里就结束了
- 平衡因子为 1 或者 -1
平衡因子为 1 或 -1,只能是由平衡因子为 0 的情况转换来的
以 7 为根节点的子树,在插入节点前后,树的高度发生了变化,因而会影响祖先节点的平衡因子,所以需要向上调整
平衡因子要一直调整,直到遇到其他两种情况或者调整到到根节点为止
- 平衡因子为 2 或者 -2
插入节点后,parent 的平衡因子为-2或者2,说明左右子树的高度差绝对值超过了1,不符合AVL树的性质,需要进行调整。经过调整后,AVL 树内不会出现平衡因子为 -2 或者 2 的情况,整棵 AVL 树是符合规范的,插入操作运行到这里结束
cpp
bool insert(const pair<K, V>& kv)
{
// 1.按照二叉搜索树的规则插入节点
// 树为空
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
// 树不为空
// 寻找合适插入位置
Node* cur = _root, * parent = nullptr;
while (cur)
{
if (kv.first > cur->_kv.first)
{
// 插入值 > 当前节点
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
// 插入值 < 当前节点
parent = cur;
cur = cur->_left;
}
else
return false; // 不允许插入重复值
}
// 找到插入位置
cur = new Node(kv);
cur->_parent = parent;
// 判断 cur 是 parent 的左子树还是右子树,链接
if (cur->_kv.first > parent->_kv.first)
parent->_right = cur;
else
parent->_left = cur;
// 2.更新平衡因子, 更新到根节点结束
while (parent)
{
// 更新父节点的平衡因子
if (cur == parent->_right)
parent->_bf++;
else
parent->_bf--;
// 检查父节点平衡因子
if (parent->_bf == 0)
{
// 树的高度无变化,不需要向上更新了
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 当前树的高度发生变化,需要向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 需要进行调整
break;
}
else
{
// 出现这种情况,说明AVL树的实现出现了问题
assert(false);
}
}
return true;
}
旋转
当插入节点后,parent 的平衡因子变为 2 或者 -2,这就违反了 AVL 树的性质,需要对其进行调整
此时根据插入位置不同,有如下四种情况:
- 插入较高左子树的左侧------左左,需要右旋
上图中,矩形代表高度为 h 的子树 ,而不是某个节点。通过上图可以看出,当 cur(相对于parent就是 subL) 的平衡因子为 -1,parent的平衡因子为 -2 时,就是需要右旋的情况。注:这里的 cur 可能是经过层层调整上来的,h >= 0
- 右旋的过程:
- 在 30 的左子树中插入一个节点,30 左子树高度增加一层,导致 60 的左子树高度增加一层而不平衡。要想让 60 恢复平衡,只能让 60 的左子树提高一层,右子树降低一层。也就是把 30 提上去,把 60 转下来
- 30 在上,60 在下,60 > 30,所以 60 只能是 30 的右子树,同时如果 30 存在右子树 subLR,那么就只能放在 60 的左子树
- 最后更新 30 和 60 的平衡因子
需要注意的是:
- 改变链接关系时一定不能忘记改变节点的 _parent(指向父节点的指针)
- 30 的右子树 subLR 可能存在,也可能不存在,改变 subLR 的父节点之前,要检查 subLR,防止访问空指针
- 60 可能是根节点,也可能是其他节点 pparent 的子树
- 如果是根节点,旋转完毕要更新根节点为 subL
- 如果是子树,要检查 60 是父节点 pparent 的左子树还是右子树,改变 pparent 的孩子为 cur,继续向上调整
上面的旋转过程是严格按照二叉搜索树的规则进行的
右旋代码,一定结合图去写
cpp
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 改变链接关系
parent->_left = subLR;
if (subLR) // 检查是否存在
subLR->_parent = parent;
subL->_right = parent;
Node* pparent = parent->_parent; // 记录 parent 的父节点
parent->_parent = subL;
if (pparent)
{
// parent 是子树
if (pparent->_right == parent)
pparent->_right = subL;
else
pparent->_left = subL;
subL->_parent = pparent;
}
else
{
// parent是根节点
_root = subL;
_root->_parent = nullptr;
}
// 更新平衡因子
subL->_bf = parent->_bf = 0;
}
- 插入较高右子树的右侧------右右,需要左旋
当 cur(相对于parent就是 subR) 的平衡因子为 1,parent的平衡因子为 2 时,就是需要左旋的情况
因为左旋和右旋只是方向不同,具体情况参考右旋,这里不再赘述
左旋代码:
cpp
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 旋转,链接
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* pparent = parent->_parent;
parent->_parent = subR;
// 检查 parent 是根据节点还是子树
if (pparent)
{
// 子树
if (pparent->_right == parent)
pparent->_right = subR;
else
pparent->_left = subR;
subR->_parent = pparent;
}
else
{
// 根节点
_root = subR;
_root->_parent = nullptr;
}
// 更新平衡因子
subR->_bf = parent->_bf = 0;
}
- 插入较高左子树的右侧------左右,需要左右双旋:先左旋再右旋
cur(相对于parent就是 subL) 的平衡因子为 1,parent的平衡因子为 -2 时,就是需要左右双旋的情况
这种情况下,90的左子树较高,右旋后会变为 90 的右子树较高,再左旋又会变成 90 的左子树较高
所以单旋解决不了问题,需要进行双旋:先以 30(subL) 为基准左旋,再以 90(parent) 为基准右旋
双旋的过程很简单,只需要复用左旋和右旋就可以,麻烦的是平衡因子的更新:
- 新节点插入在右树的左子树,也就是插入在 subLR 的左子树,经过双旋得到的平衡因子如上图,subLR 和 subL 的平衡因子为 0,而 parent 的平衡因子为 1
- 如果新节点插入在右树的右子树,经过双旋,平衡因子如下图,subLR 和 parent 平衡因子为 0,subL 的平衡因子为 -1
- 还有一种情况:60(subLR) 是新插入的节点,最后subLR 、parent、subL 的平衡因子都是0
那如何处理呢?------双旋之前,记录下 subLR 的平衡因子的值,三种值对应三种情况,在双旋之后,依据之前记录下的 subLR 的值进行平衡因子的更新
左右双旋代码,对照图进行编写会很容易
cpp
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf; // 记录
// 左旋
RotateL(subL);
// 右旋
RotateR(parent);
// 更新平衡因子
subLR->_bf = 0;
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
}
else
{
subL->_bf = parent->_bf = 0;
}
}
- 插入较高右子树的左侧------右左,需要右左双旋:先右旋再左旋
cur(相对于parent就是 subR) 的平衡因子为 -1,parent的平衡因子为 2 时,就是需要右左双旋的情况
右左双旋的思路和左右双旋相同,具体情况参考左右双旋,这里不再赘述
右左双旋代码:
cpp
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
// 右旋,左旋
RotateR(subR);
RotateL(parent);
// 更新平衡因子
subRL->_bf = 0;
if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
parent->_bf = 0;
}
else
{
parent->_bf = subR->_bf = 0;
}
}
AVL树的验证
写一个成员函数来验证当前对象是不是一个 AVL 树
- 计算左子树和右子树的高度
- 如果左右子树高度差的绝对值超过1,那么一定不是AVL树
- 如果左右子树高度差的绝对值没有超过1,再看左右子树是不是 AVL 树
cpp
public:
bool isBalance()
{
return _isBalance(_root);
}
private:
bool _isBalance(Node* root)
{
if (root == nullptr) return true;
int height_left = _height(root->_left);
int height_right = _height(root->_right);
if (abs(height_left - height_right) > 1)
return false;
return _isBalance(root->_left) && _isBalance(root->_right);
}
int _height(Node* root)
{
if (root == nullptr) return 0;
return max(_height(root->_left), _height(root->_right)) + 1;
}
测试:
cpp
AVLTree<int, int> t;
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
t.insert({ e,e });
t.inOrder();
cout << "isBalance:" << t.isBalance() << endl;
性能测试
返回当前树的高度和节点数
cpp
public:
int height()
{
return _height(_root);
}
int size()
{
return _size(_root);
}
private:
int _height(Node* root)
{
if (root == nullptr) return 0;
return max(_height(root->_left), _height(root->_right)) + 1;
}
int _size(Node* root)
{
if (root == nullptr) return 0;
return _size(root->_left) + _size(root->_right) + 1;
}
构造一个AVLTree对象,插入100000个随机数,看看效率如何
cpp
const int N = 100000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand() + i);
}
// 开始插入
size_t begin1 = clock();
AVLTree<int, int> t;
for (auto e : v)
{
t.insert(make_pair(e, e));
}
// 插入结束
size_t end1 = clock();
cout << "Insert:" << end1 - begin1 << endl;
cout << "isBalance:" << t.isBalance() << endl;
cout << "Height:" << t.height() << endl;
cout << "Size:" << t.size() << endl;
插入 66542 个数据只用了 38 毫秒,树的高度是 19,也就是说,查找一个数最多查 19 次就可以得到结果
下面我们看看查找数据的效率
cpp
size_t begin2 = clock();
// 确定在的值
for (auto e : v)
{
t.find(e);
}
size_t end2 = clock();
cout << "Find:" << end1 - begin1 << endl;
查找 100000 个数据在不在树中,只用了 19 毫秒
代码
cpp
// AVLTree.h
#pragma once
#include <assert.h>
template <class K, class V>
struct AVLTreeNode
{
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
pair<K, V> _kv;
int _bf;
};
template <class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool insert(const pair<K, V>& kv)
{
// 树为空
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
// 树不为空
// 寻找合适插入位置
Node* cur = _root, * parent = nullptr;
while (cur)
{
if (kv.first > cur->_kv.first)
{
// 插入值 > 当前节点
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
// 插入值 < 当前节点
parent = cur;
cur = cur->_left;
}
else
return false; // 不允许插入重复值
}
// 找到插入位置
cur = new Node(kv);
cur->_parent = parent;
// 判断 cur 是 parent 的左子树还是右子树,链接
if (cur->_kv.first > parent->_kv.first)
parent->_right = cur;
else
parent->_left = cur;
// 更新平衡因子, 更新到根节点结束
while (parent)
{
// 更新父节点的平衡因子
if (cur == parent->_right)
parent->_bf++;
else
parent->_bf--;
// 检查父节点平衡因子
if (parent->_bf == 0)
{
// 树的高度无变化,不需要向上更新了
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 当前树的高度发生变化,需要向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 需要进行调整
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)
{
// 左右双旋
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
// 右左双旋
RotateRL(parent);
}
else
{
// 没有这种情况
assert(false);
}
break;
}
else
{
// 出现这种情况,说明AVL树的实现出现了问题
assert(false);
}
}
return true;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 改变链接关系
parent->_left = subLR;
if (subLR) // 检查是否存在
subLR->_parent = parent;
subL->_right = parent;
Node* pparent = parent->_parent; // 记录 parent 的父节点
parent->_parent = subL;
if (pparent)
{
// parent 是子树
if (pparent->_right == parent)
pparent->_right = subL;
else
pparent->_left = subL;
subL->_parent = pparent;
}
else
{
// parent是根节点
_root = subL;
_root->_parent = nullptr;
}
// 更新平衡因子
subL->_bf = parent->_bf = 0;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 旋转,链接
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* pparent = parent->_parent;
parent->_parent = subR;
// 检查 parent 是根据节点还是子树
if (pparent)
{
// 子树
if (pparent->_right == parent)
pparent->_right = subR;
else
pparent->_left = subR;
subR->_parent = pparent;
}
else
{
// 根节点
_root = subR;
_root->_parent = nullptr;
}
// 更新平衡因子
subR->_bf = parent->_bf = 0;
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf; // 记录
// 左旋
RotateL(subL);
// 右旋
RotateR(parent);
// 更新平衡因子
subLR->_bf = 0;
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
}
else
{
subL->_bf = parent->_bf = 0;
}
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
// 右旋,左旋
RotateR(subR);
RotateL(parent);
// 更新平衡因子
subRL->_bf = 0;
if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
parent->_bf = 0;
}
else
{
parent->_bf = subR->_bf = 0;
}
}
int height()
{
return _height(_root);
}
int size()
{
return _size(_root);
}
Node* find(const K& key)
{
// 树为空
if (_root == nullptr)
return nullptr;
// 树不为空
Node* cur = _root;
while (cur)
{
if (key > cur->_kv.first)
{
// key值 > 当前节点
cur = cur->_right;
}
else if (key < cur->_kv.first)
{
// 插入值 < 当前节点
cur = cur->_left;
}
else
return cur; // 找到
}
// 找不到
return nullptr;
}
bool isBalance()
{
return _isBalance(_root);
}
void inOrder()
{
_inOrder(_root);
}
private:
bool _isBalance(Node* root)
{
if (root == nullptr) return true;
int height_left = _height(root->_left);
int height_right = _height(root->_right);
if (abs(height_left - height_right) > 1)
return false;
return _isBalance(root->_left) && _isBalance(root->_right);
}
int _height(Node* root)
{
if (root == nullptr) return 0;
return max(_height(root->_left), _height(root->_right)) + 1;
}
int _size(Node* root)
{
if (root == nullptr) return 0;
return _size(root->_left) + _size(root->_right) + 1;
}
void _inOrder(Node* root)
{
if (root == nullptr) return;
_inOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;;
_inOrder(root->_right);
}
Node* _root = nullptr;
};
总结
AVL 树通过平衡因子和旋转操作,可以控制每棵树的左右子树高度差不超过1,使得树的高度尽量低,这样就可以保证查找的效率非常高
但是这也是有代价的,插入数据时频繁的旋转次数较多,会有很大的消耗;删除同理
所以如果一个结构不经常进行修改操作,使用可以使用 AVL 树;但是如果经常修改一个结构,就不太适合使用 AVL 树了
红黑树
这里红黑树的代码是在 AVL树 代码基础上修改的,所以在 AVL树 部分讲过的东西,这部分不再赘述
与 AVL树 的比较
AVL 树追求严格的高度平衡,所以它的查找效率非常极致,但是因为要维护平衡,所以要经常旋转,频繁的旋转操作会有消耗,而多数场景下并不总是需要 AVL树 这么高的查找效率
而红黑树的平衡约束条件就没有AVL树那么严格,减少了调整高度的操作,高度适当增加,既保证了查找效率,也减少了平衡操作的开销
概念
红黑树也是一种二叉搜索树,它控制平衡的方式和 AVL树 的平衡因子不同------用颜色控制平衡 。树节点增加一个变量用来表示颜色,不是红色就是黑色,通过控制节点的颜色,红黑树可以保证最长路径长度不超过最短路径长度2倍,接近平衡
红黑树可以是空树,当它不是空树时,有如下性质:
- 节点不是黑色就是红色
- 根节点是黑色
- (重点)如果一个节点是红色的,那么它的两个孩子节点是黑的,也就是不能存在连续的红色节点
- (重点)对于每个节点,从该节点到其所有叶节点的简单路径上,黑色节点数量相同
- 每个叶子节点 NIL 都是黑色的,红黑树中的叶子节点是指空节点。NIL虽然是黑色的,但是不会算作路径中黑色节点数量
其中,维护红黑树平衡最重要的是第三条和第四条。尤其是第四条不可以违背,违背第三条可以调整,违背第四条调整起来会很麻烦
为什么上面的规则可以保证红黑树的最长路径长不超过最短路径长的2倍呢?
假设我们有一棵确定的红黑树,根据第四条性质,那么每一条路径黑色节点的数量 就是确定的。假设红黑树从根节点到叶子节点路径上的黑色节点数量为2,在极致情况下,最短和最长路径是这样的:
- 最短路径只有黑色节点 ,每条路径的黑色节点数量又是固定的,所以不会有路径这条还短了
- 最长路径是在最短路径的基础上,在黑色节点中间插入红色节点 ,因为不可以出现连续红色节点,所以节点按照一黑一红的顺序更替出现
如下图:
在满足红黑树性质的前提下,这种就是最极致的情况,最短路径长度为2,最长路径长度为4,最长路径长不超过最短路径长度的2倍。
节点的定义
为了表示颜色,可以使用枚举变量定义一个 Color 变量
cpp
enum Color
{
RED,
BLACK
};
节点的成员变量:
- _left 指向左子树
- _right 指向右子树
- _parent 指向父节点
- _ky 储存键值对
- _col 表示节点的颜色
cpp
template<class K, class V>
struct RBTreeNode
{
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)
{}
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Color _col;
};
默认插入的节点是红色的,为什么不是黑色的呢?
如果在插入新节点之前,红黑树是平衡的,再新插入一个黑色节点,那么就会导致该路径的黑色节点数量多一个,这就导致别的路径和当前路径的黑色节点数量不同,破坏了红黑树的平衡
简单来说,就相当于人家别的路径啥事没干,你这边插入了一个黑色节点,导致所有路径的平衡被破环了,直接得罪了所有路径
如果新插入一个红色节点,其父节点是黑色的就没事了;父节点是红色的,充其量就是违背了性质3,接下来调整一下即可,相对违背性质4来说,这种情况的后果没那么严重
插入
红黑树的插入操作分两步进行:
- 按照二叉搜索树的规则插入新节点
cpp
bool insert(const pair<K, V>& kv)
{
// 树为空
if (_root == nullptr)
{
// 直接插入到根节点,返回
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
// 树不为空,寻找合适位置插入
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
return false; // 不允许重复键值
}
// 插入新节点
cur = new Node(kv);
cur->_parent = parent;
if (cur->_kv.first > parent->_kv.first)
parent->_right = cur;
else
parent->_left = cur;
// 检查是否违反红黑树性质
// ...
return true;
}
- 插入新节点后,检查红黑树的性质是否遭到破环
规定:当前插入节点是 cur,父节点是 parent,祖父节点是 grandpa,叔叔节点是 uncle,下面分别简写为p、g、u
因为默认插入的新节点是红色的 ,所以如果其父节点是黑色 的就不需要调整;如果父节点是红色的,就出现了两个连续的红色节点,需要进行调整
当新插入节点和其父节点都是红色时,违反性质3,此时有三个节点的颜色是确定的:
- cur、p 节点都是红色的;g 节点是黑色的,因为插入 cur 节点之前,红黑树是符合性质的,p 是红色的,那么 g 肯定是黑色的
此时只有 u 节点的颜色是不确定的,根据 u 节点可以分为以下情况:
情况1:u 节点存在,且为红色
在上图中,a、b、c、d、e都是红黑树子树,它们的黑色节点数量都是相同的。为什么新插入节点cur也会有子树呢?因为可能 cur 节点原来是黑色的,在 a、b任意子树插入节点后,违反了红黑树规则,经过调整后到了这里,cur被调整为红色,不懂没关系,情况1说完后,这里自然就明白了
那么遇到这种情况时如何处理,cur 和 p 都是红色的,不符合规则,必须把其中一个变成黑色:
- 把 cur 变成黑色肯定不行,要不然就相当于插入的新节点是黑色的,我们一开始就说了不可以插入黑色节点
- 所以只能把 p 变为黑色
此时只把 p 变为黑色就会出现问题:原本 g-p 路径上的黑色节点数量多了一个,为了保持路径上的黑色节点数量不变,需要把 g 变为红色
而此时 g-u 路径上的黑色节点就少了一个,为了保持路径上的黑色节点数量不变,需要把 u 变为黑色
所以情况1的调整方式就是:p、u变黑,g 变红
到这里还要对 g 单独进行处理:
- g 为根节点,就把 g 变为黑色,调整结束
- g 为子树,那么 g 必有父节点,且如果是红色,还需要向上调整:将 g 成为新的 cur,开始新一轮调整
情况1代码
cpp
// 检查是否违反红黑树性质
// parent 是红色就需要调整
// parent 不存在或者是黑色就结束
while (parent && parent->_col == RED)
{
// 寻找 g 和 u
Node* grandpa = parent->_parent;
Node* uncle = nullptr;
// 区分 u 是 g 的左还是右,情况2需要用到
if (parent == grandpa->_left)
uncle = grandpa->_right;
else
uncle = grandpa->_left;
// 情况1,u 存在且为红
if (uncle && uncle->_col == RED)
{
// p u 变黑,g变红
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 更新 cur 和 p
// 需不需要继续向上更新,交给 while 条件判断
// cur 是根节点,结束
// cur 是子树,且父节点为红色,继续调整
cur = grandpa;
parent = grandpa->_parent;
}
}
// 不管如何调整,根节点必是黑色
_root->_col = BLACK;
return true;
情况2:u 节点不存在或者 u 节点为黑色,u为右树
这里提前说一下:u 为 g 的右树 和 u 为 g 的左树 ,会导致情况2有两个版本:思路和方法都一样,只是方向不同 ,相当于右旋和左旋的区别。所以下面只讲 u 为右树的情况,u为左树直接参考 u 为右树的情况,具体实现可以自己画一下。下面是情况2的处理方法:
- u 不存在,则 g-u 路径至少有一个黑色节点 ,那么 cur 必然是新插入节点;因为 cur 如果不是新插入节点,说明 cur 原来不是黑色,是调整上来之后变成红色的,可是这样 g-cur 路径就至少有两个黑色节点,违背性质4
- u存在且为黑色,则 g-u 路径至少有两个黑色节点 ,那么 cur 原来必然是黑色的,g-cur路径也是至少两个黑色节点 ,只是调整上来后 cur 变为了红色,a、b、c至少有一个黑色节点
无论u存在或是不存在,处理方法都是一样的,根据 cur 是 p 的左还是右,分为 2.a 和 2.b 两种情况:
此时每条路径黑色节点数量不变,且代替 g 的 p 变为了黑色,不管 p 的父节点是否存在,不管颜色如何,都不用继续向上调整了,直接 break 终止循环即可
正确处理方式:先以 p 为基准左旋 ,变为情况 2.a
之后按照情况2.a 的处理方式处理
简单来说,就是先p左旋 ,g右旋,最后改变颜色
情况2代码:
cpp
// 情况2,u不存在或者存在且为黑
else if (uncle == nullptr || uncle->_col == BLACK)
{
// u 为右树版本
if (uncle == grandpa->_right)
{
// 情况2.a,cur 为左树
if (cur == parent->_left)
{
// g
// p u
// c
// 右旋,改颜色
RotateR(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
// 情况 2.b,cur 为右树
else if (cur == parent->_right)
{
// g
// p u
// c
// 以p左旋,以g右旋,改颜色
RotateL(parent);
RotateR(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
}
// u 为左树版本
else if (uncle == grandpa->_left)
{
}
// 情况2结束后就不需要调整了
break;
}
情况2:u 节点不存在或者 u 节点为黑色,u为左树
以上的情况2处理方法,都是基于 u 是 g 的右树 实现的,当 u 是 g 的左树 时,处理方法类似,只是旋转的方向不同,不再详细说明
2.a cur 是 p 的右
以 g 为基准,变色,左旋
2.b cur 是 p 的左
以 p 右旋,变为情况2.a,之后按照 2.a 处理。
也就是先以 p 为基准右旋 ,变色,以 g 为基准左旋,变色
代码
cpp
// u 为左树版本
else if (uncle == grandpa->_left)
{
// 情况2.a,cur 为 p 的右树
if (cur == parent->_right)
{
// g
// u p
// c
// 以 g 左旋,更改颜色
RotateL(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
// 情况2.b,cur 为 p 的左树
else if (cur == parent->_left)
{
// g
// u p
// c
// 以 p 右旋,再以 g 右旋,更改颜色
RotateR(parent);
RotateL(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
}
红黑树的插入到这里就写完了,简单测试一下:
cpp
void test1()
{
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14, 8, 3, 1, 10, 6, 4, 7, 14, 13 };
RBTree<int, int> t;
for (auto& e : a)
{
if (e == 1)
int x = 1;
t.insert({ e,e });
}
t.inOrder();
}
插入数据和中序遍历没有问题,树的基本结构没有问题,下面我们验证一下红黑树的结构
红黑树的验证
cpp
public:
bool isBalance()
{
return _isBalance(_root);
}
private:
bool _isBalance(Node* root)
{
}
验证红黑树最重要的两点:
- 不可以存在连续红色节点
- 从任一节点到其每个叶子的所有简单路径都包含相同数量的黑色节点
关于连续红色节点的处理:一个节点可能有两个孩子节点,不太好验证;但是每个节点的父节点只存在一个,只需要判断当前节点和其父节点是否都是红色即可
cpp
bool _isBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
// 存在连续红色节点
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << "存在连续红色节点" << endl;
return false;
}
}
如何判断每条路径上的黑色节点数量是否相同呢?可以先求出任一条路径的黑色节点数量 ,当作参考值refNum。之后递归遍历红黑树的所有路径,一旦出现一条路径的黑色节点数量与参考值不同,就违反了性质4,返回false
而递归地求一条路径的黑色节点数,只需给递归函数传递一个形参blackNum ,记录到达当前节点时,路径上的黑色节点数量
递归结束条件:遇到空即可返回,规定空树也是红黑树;若非空树,那么到达空时,也就是到了一条路径的叶子节点(在红黑树中,叶子节点NIL就是空),这时判断黑色节点数量 blackNum 是否等于 refNum
cpp
public:
bool isBalance()
{
// 计算任一路径黑色节点数
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
++refNum;
// 计算最左路径黑色节点数
cur = cur->_left;
}
return _isBalance(_root, 0, refNum);
}
void inOrder()
{
return _inOrder(_root);
}
private:
bool _isBalance(Node* root, int blackNum, int refNum)
{
if (root == nullptr)
{
// 到达红黑树叶子节点
// 判断黑色节点数量是否相同
if (blackNum != refNum)
{
cout << "存在黑色节点数量不同的路径" << endl;
return false;
}
return true;
}
// 存在连续红色节点
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << "存在连续红色节点" << endl;
return false;
}
// 记录黑色节点数
if (root->_col == BLACK)
++blackNum;
// 判断左右子树是否是红黑树
return _isBalance(root->_left, blackNum, refNum) && _isBalance(root->_right, blackNum, refNum);
}
测试:
其他接口
因为后面我们还要将红黑树封装为 map/set,所以这里完善一下其他接口
析构函数
一般删除一棵树是递归进行删除,所以写一个递归函数destroy()
,由析构函数调用此函数
- 采用后序顺序删除节点
cpp
public:
~RBTree()
{
destroy(_root);
_root = nullptr;
}
private:
void destroy(Node* root)
{
if (root == nullptr) return;
destroy(root->_left);
destroy(root->_right);
delete root;
root = nullptr;
}
拷贝构造
如何拷贝一棵树呢?同样是递归拷贝,写一个递归函数copy()
,由拷贝构造调用它
- 拷贝当前根节点,然后拷贝左子树和右子树
- 左子树和右子树的拷贝过程同上
- 拷贝完左子树和右子树时,将它们和根节点链接
注意:我们之前没有写默认构造函数,编译器会帮我们自动生成;而拷贝构造也是构造函数,只要写了,编译器就不会自动生成了。如果不想自己写一个构造函数,可以使用 default
强制让编译器生成
cpp
public:
RBTree() = default; // 强制生成
RBTree(const RBTree<K, V>& t)
{
_root = copy(t._root);
}
private:
Node* copy(Node* root)
{
if (root == nullptr) return nullptr;
// 拷贝根节点
Node* copynode = new Node(root->_kv);
copynode->_col = root->_col;
// 拷贝左子树,链接
copynode->_left = copy(root->_left);
if (copynode->_left) // 左子树存在,链接
copynode->_left->_parent = copynode;
// 拷贝右子树
copynode->_right = copy(root->_right);
if (copynode->_right) // 右子树存在,链接
copynode->_right->_parent = copynode;
return copynode;
}
测试:
赋值重载
直接传值传参 给赋值重载函数,这样就会调用拷贝构造来创建一个临时的红黑树对象t,然后把临时对象和要赋值的对象的 _root 进行交换即可,出了函数临时对象t就会调用析构函数自动完成空间的释放
cpp
RBTree<K, V> operator=(RBTree<K, V> t)
{
swap(_root, t._root);
return *this;
}
测试:
红黑树代码
cpp
// RBTree.h
#pragma once
enum Color
{
RED,
BLACK
};
template <class K, class V>
struct RBTreeNode
{
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)
{}
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Color _col;
};
template <class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
RBTree() = default;
~RBTree()
{
//cout << "~RBTree()" << endl;
destroy(_root);
_root = nullptr;
}
RBTree(const RBTree<K, V>& t)
{
_root = copy(t._root);
}
RBTree<K, V> operator=(RBTree<K, V> t)
{
swap(_root, t._root);
return *this;
}
bool insert(const pair<K, V>& kv)
{
// 树为空
if (_root == nullptr)
{
// 直接插入到根节点,返回
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
// 树不为空,寻找合适位置插入
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
return false; // 不允许重复键值
}
// 插入新节点
cur = new Node(kv);
cur->_parent = parent;
if (cur->_kv.first > parent->_kv.first)
parent->_right = cur;
else
parent->_left = cur;
// 检查是否违反红黑树性质
// parent 是红色就需要调整
// parent 不存在或者是黑色就结束
while (parent && parent->_col == RED)
{
// 寻找 g 和 u
Node* grandpa = parent->_parent;
Node* uncle = nullptr;
// 区分 u 是 g 的左还是右,情况2需要用到
if (parent == grandpa->_left)
uncle = grandpa->_right;
else
uncle = grandpa->_left;
// 情况1,u 存在且为红
if (uncle && uncle->_col == RED)
{
// p u 变黑,g变红
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 更新 cur 和 p
// 需不需要继续向上更新,交给 while 条件判断
// cur 是根节点,结束
// cur 是子树,且父节点为红色,继续调整
cur = grandpa;
parent = grandpa->_parent;
}
// 情况2,u不存在或者存在且为黑
else if (uncle == nullptr || uncle->_col == BLACK)
{
// u 为右树版本
if (uncle == grandpa->_right)
{
// 情况2.a,cur 为左树
if (cur == parent->_left)
{
// g
// p u
// c
// 右旋,改颜色
RotateR(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
// 情况 2.b,cur 为右树
else if (cur == parent->_right)
{
// g
// p u
// c
// 以p左旋,再以g右旋,改颜色
RotateL(parent);
RotateR(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
}
// u 为左树版本
else if (uncle == grandpa->_left)
{
// 情况2.a,cur 为 p 的右树
if (cur == parent->_right)
{
// g
// u p
// c
// 以 g 左旋,更改颜色
RotateL(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
// 情况2.b,cur 为 p 的左树
else if (cur == parent->_left)
{
// g
// u p
// c
// 以 p 右旋,再以 g 右旋,更改颜色
RotateR(parent);
RotateL(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
}
// 情况2结束后就不需要调整了
break;
}
}
// 不管如何调整,根节点必是黑色
_root->_col = BLACK;
return true;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 改变链接
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* pparent = parent->_parent;
parent->_parent = subL;
// 检查 p 是根节点还是子树
if (pparent)
{
// 子树
subL->_parent = pparent;
if (parent == pparent->_right)
pparent->_right = subL;
else
pparent->_left = subL;
}
else
{
// 根节点
_root = subL;
_root->_parent = nullptr;
}
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 更改链接关系
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* pparent = parent->_parent;
parent->_parent = subR;
// 检查 p 是子树还是根
if (pparent)
{
// 子树
subR->_parent = pparent;
if (parent == pparent->_right)
pparent->_right = subR;
else
pparent->_left = subR;
}
else
{
// 根节点
_root = subR;
_root->_parent = nullptr;
}
}
bool isBalance()
{
// 计算任一路径黑色节点数
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
++refNum;
// 计算最左路径黑色节点数
cur = cur->_left;
}
return _isBalance(_root, 0, refNum);
}
void inOrder()
{
return _inOrder(_root);
}
private:
void destroy(Node* root)
{
if (root == nullptr) return;
destroy(root->_left);
destroy(root->_right);
delete root;
root = nullptr;
}
Node* copy(Node* root)
{
if (root == nullptr) return nullptr;
// 拷贝根节点
Node* copynode = new Node(root->_kv);
copynode->_col = root->_col;
// 拷贝左子树,链接
copynode->_left = copy(root->_left);
if (copynode->_left) // 左子树存在,链接
copynode->_left->_parent = copynode;
// 拷贝右子树
copynode->_right = copy(root->_right);
if (copynode->_right) // 右子树存在,链接
copynode->_right->_parent = copynode;
return copynode;
}
bool _isBalance(Node* root, int blackNum, int refNum)
{
if (root == nullptr)
{
// 到达红黑树叶子节点
// 判断黑色节点数量是否相同
if (blackNum != refNum)
{
cout << "存在黑色节点数量不同的路径" << endl;
return false;
}
return true;
}
// 存在连续红色节点
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << "存在连续红色节点" << endl;
return false;
}
// 记录黑色节点数
if (root->_col == BLACK)
++blackNum;
// 判断左右子树是否是红黑树
return _isBalance(root->_left, blackNum, refNum) && _isBalance(root->_right, blackNum, refNum);
}
void _inOrder(Node* root)
{
if (root == nullptr) return;
_inOrder(root->_left);
cout << root->_kv.first << ": " << root->_kv.second << endl;
_inOrder(root->_right);
}
Node* _root = nullptr;
};
总结
与 AVL树 相比,红黑树不追求极致的高度平衡,只需保证最长路径不超过最短路径的2倍,从而减少了旋转的次数,因此插入和删除的性能开销小于 AVL 树;而且红黑树实现起来比 AVL树 简单,所以在实际应用中红黑树用的比较多
map 和 set 的封装
STL 源码参考
在使用红黑树封装 map 和 set 之前,我们先来看一下 STL 库中的 map 和 set 是怎么实现的,参考一下
我们实现的版本不是追求百分百还原,可能会为了简单,而做出一些修改,毕竟我们的目的是为了更好地了解 STL 的底层,而不是还原一个 STL 出来
成员变量
map 的成员变量是一棵红黑树_Rb_tree
,经过 typedef,红黑树的类型名变为了 _Rep_type
给红黑树传递类型参数时,有一个 key_type
和一个value_type
,不难看出,这就是键值对中的 key 和 value,这两个类型都不是原生类型,而是经过 typedef 过的
下面是 set 的typedefs,由于set中只存在键 key,就不需要 value了。但是为了和 map 统一,set 中也设置了 value,set 的 value 就是 key
当我们使用 map 时会给 map 传递类型,例如 map<string, int>,其中 string 是键,int 是值;但是按照map中的意思,string 是键,pair<string, int>是值,就等于是把 pair 作为了红黑树的值,这是为什么呢?
和我们自己写的红黑树做一下比较:
我们写的红黑树,模板参数中一个是键K,一个是值V,然后在红黑树节点中存在键值对 pair<K,V>。这种写法有什么换坏处呢?------红黑树写死了,只能存键值对,不能存单个数据,也就是只能实现 map,不能实现 set。我们想实现 set 还得修改红黑树节点中的数据类型
而 STL 版本在 map 层做了处理,在 map 层我知道红黑树中要存的是键值对 ,所以就把 value 的类型定义为 pair<int, int>,然后传递给红黑树模板;在红黑树层,我不需要知道 value 是什么类型 ,只需要使用即可,这样实例化出来的红黑树中的 value 就是 pair<int, int> 类型,可以当作 map 使用
set 同理,在 set 层我知道红黑树中要存的是键值,所以把 value 的类型定义为 int,传递给红黑树模板;红黑树不需要知道 value 的类型是什么。这样实例化出来的红黑树的 value 就是 int 类型,可以当作 set 使用
这样,一份红黑树代码就可以实现多种用途。这里还有一个问题:在 map 中,value 的类型是 pair,其中就包括了 key;而在 set 中 value 就是 key。这种情况下,第一个模板参数key,是不是有点多余呢?
cpp
tempalte <class key, class value>
class RBTree
{};
其实不然,红黑树中的有些接口是需要使用第一个模板参数 key 的,例如find(const key& k)
。find 只能用过键来查找对应的值,所以 key 这个模板参数还是有必要存在的
模板参数
接下来我们把红黑树封装为 map 和 set。因为我们的红黑树是写死的,只能用来存键值对,所以要边实现 map 和 set 边改造红黑树
map 和 set 的结构
map 的模板参数有两个:
- K:表示key的类型
- V:表示value的类型
set 的模板参数只有一个:
- K:表示 key 的类型,同时也是 value 的类型
map 和 set 的成员变量是一棵红黑树,map 向红黑树中传递 pair 作为树的值类型 ,set 则传递 K 作为值类型 在 map 和 set 中,键不可更改,所以要传递 const K
cpp
// map
template <class K, class V>
class map
{
private:
RBTree<K, pair<const K, V>> _t;
};
// set
template <class K>
class set
{
private:
RBTree<K, const K> _t;
};
相应地,红黑树和节点的模板参数 、节点存储的数据类型都要改变
- T:表示节点存储的数据类型。map 传递的是 pair 类型,set 传递的是 K 类型
- 将节点的成员变量中的 pair 类型改为 T 类型,变量名改为 _data
cpp
// 节点
template <class T>
struct RBTreeNode
{
RBTreeNode(const T& data)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
, _col(RED)
{}
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
T _data;
Color _col;
};
红黑树中的模板参数变化与上面一样,将 KV 替换为 KT,所有涉及到 KV 的都要修改,就不贴代码了
仿函数KeyOfT
当我们修改 insert 函数时,就会发生问题:插入时要使用 key 进行比较,那么要如何取到节点的 key 进行比较呢?
- 当插入数据 T 是 pair 类型时,可以使用
.first
来取到 key - 当插入数据 T 不是 pair 类型时而是 K 类型时,反而可以直接用
但是,在红黑树这一层,并不知道数据类型 T 是什么类型,这该怎么取 key?------使用仿函数,在红黑树的模板参数中添加一个仿函数
虽然在红黑树层不知道 T 是什么类型,但是在 map 层 和 set 层,可是知道数据类型的。
- map 可以写一个仿函数
MapKeyOfT
,接收 pair 类型,返回其 first; - set 则是写一个仿函数
SetKeyOfT
,接收 K 类型,返回 K 类型
cpp
template <class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
private:
RBTree<K, pair<K, V>, MapKeyOfT> _t;
};
template <class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
private:
RBTree<K, K, SetKeyOfT> _t;
};
红黑树的模板参数中添加一个 KeyOfT 类型
cpp
template <class K, class T, class KeyOfT>
class RBTree
然后修改红黑中取 key 的操作,例如 insert 中的寻找合适插入位置
- 构造一个 KeyOfT 对象 kot
- 涉及到取 key 的操作时,使用 kot 即可
红黑树的迭代器
map 和 set 支持迭代器访问数据,所以这里我们也要实现。先看看STL中是怎么实现迭代器的
可以看到,map 和 set 的迭代器和相关接口都是使用的红黑树的。说到底,map 和 set 就是经过封装后,套了一层壳的红黑树
封装迭代器
和 【list】一样,直接使用节点指针 Node* 作为迭代器是不可以的,原生指针的行为(例如++,解引用等)并不能满足我们的需要。所以需要把原生指针封装为一个类iterator,然后通过重载运算符的操作,实现迭代器应有的功能
迭代器模板参数:
- T:表示节点存储的数据的类型
- Ref:表示 T 的引用
- Ptr:表示 T 的指针
成员变量:
- Node* _it:迭代器的底层就是一个节点指针
cpp
template <class T, class Ref, class Ptr>
struct __RBTreeIterator
{
typedef RBTreeNode<T> Node;
typedef __RBTreeIterator<T, Ref, Ptr> Self;
// 使用节点指针构造
__RBTreeIterator(Node* node)
:_node(node)
{}
// 迭代器底层就是一个 Node 节点指针
Node* _node;
};
重载迭代器的行为
重载*
和->
已经在 【list】 学习,这里就不说了
cpp
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &(_node->_data);
}
operator++
迭代器++后到下一个节点,下一个节点肯定比当前节点大。这里的++采用的是中序遍历的顺序:左子树-根节点-右子树,根据右子树的状态,可以分为两种情况:右子树存在或者右子树不存在
右子树存在
根据中序遍历的顺序,当前节点算是根节点 ,接下来要遍历右子树 ,而右子树也要按照中序遍历顺序遍历 。按照左根右的顺序,下一个节点应该是右子树的最左节点
右子树不存在
右子树不存在,说明当前树遍历完毕,可以向上返回了。下图只是用来举例子,不用在乎红黑树是否平衡
7 的右子树不存在,说明遍历完毕,向上返回到6;6 是 1 的右子树,遍历完毕,向上返回到 1;1 是 8 的左子树 ,按照左根右的顺序,8 的左子树遍历完毕,下一个就是根节点 8 了。所以 it++,指向的下一个节点就是 8
- 总结:在右子树不存在的情况下,要一直向上找某个祖先,找那个孩子(1)是父节点(8)左子树的祖先节点(8),这个祖先就是 ++ 后的下一个节点
还有一种特殊情况,当前 it 指向最后一个节点,一直向上找到根节点都没有找到符合条件的祖先节点,这时我们要让循环结束,返回一个空节点的迭代器
cpp
Self& operator++()
{
// 存在右子树,返回右子树最左节点迭代器
if (_node->_right)
{
Node* leftMin = _node;
while (leftMin && leftMin->_left)
leftMin = leftMin->_left;
_node = leftMin;
}
else
{
// 右子树不存在,向上找 孩子是父节点的左子树 的祖先节点
Node* cur = _node;
Node* parent = cur->_parent;
// 当 cur 是根节点时,parent 为空,循环终止
// 当找到符合条件的根节点时,循环也会终止
while (parent && cur == parent->_right)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
operator--
与operator++
思路类似,只不过是顺序反了过来,倒着走中序遍历。it 指向当前树的根节点,前一个节点比当前节点小,所以应该到左子树去找,还是根据左子树存不存在分为两种情况:
左子树存在
按照左根右的倒序,应该找左子树的最右节点
左子树不存在
左子树不存在,按照左根右的倒序,左子树遍历完毕,应该向上返回了
20 的左子树为空,遍历完毕,向上返回到 22;22 是 25 的左子树,遍历完毕,向上返回到25;25 是 17 的右子树,按照左根右的倒序,17 的右子树遍历完毕,下一个就是根节点 17了。it 进行--,前一个节点就是 17
- 总结:和 ++ 的思路类似,当左子树不存在时,要一直向上寻找某个祖先节点,找那个孩子(25)是父节点(17)的右子树的祖先节点(17)
特殊情况:当前 it 指向第一个节点,一直向上找到根节点都没有找到符合条件的祖先节点,这时我们要让循环结束,返回一个空节点的迭代器
cpp
Self& operator--()
{
// 左子树存在,寻找左子树最右节点
if (_node->_left)
{
Node* rightMax = _node->_left;
while (rightMax && rightMax->_right)
rightMax = rightMax->_right;
_node = rightMax;
}
else
{
// 左子树不存在,找 孩子是父节点右子树 的那个祖先
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
operator!=
直接比较 Node* 即可
cpp
bool operator!=(const Self& s)
{
return _node != s._node;
}
红黑树其他接口
首先要在红黑树内对迭代器进行 typedef
cpp
template <class K, class T, class KeyOfT>
class RBTree
{
public:
typedef __RBTreeIterator<T, T&, T*> iterator;
typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
};
begin()
红黑树中第一个节点的迭代器,就是中序遍历顺序第一个节点的迭代器,也就是红黑树的最左节点
cpp
iterator begin()
{
Node* leftMin = _root;
while (leftMin && leftMin->_left)
leftMin = leftMin->_left;
return iterator(leftMin);
}
const_iterator begin() const
{
Node* leftMin = _root;
while (leftMin && leftMin->_left)
leftMin = leftMin->_left;
return const_iterator(leftMin);
}
end()
红黑树中最后一个节点的下一个位置的迭代器,也就是空节点
cpp
iterator end()
{
return iterator(nullptr);
}
const_iterator end() const
{
return const_iterator(nullptr);
}
find()
有了迭代器后,find 的返回值就变成了迭代器
- 找到了,返回相应节点的迭代器;
- 找不到,返回 end()
cpp
iterator find(const K& key)
{
// 树为空
if (_root == nullptr)
return end();
// 树不为空
KeyOfT kot;
Node* cur = _root;
while (cur)
{
if (key > kot(cur->_data))
cur = cur->_right;
else if (key < kot(cur->_data))
cur = cur->_left;
else
return iterator(cur);
}
// 找不到
return end();
}
insert()
- 返回值类型变为 pair<iterator, bool>,iterator是插入节点的迭代器,bool 表示插入成功或者失败
- 成功,返回 pair<新插入节点的迭代器,bool>
- 失败,返回 pair<已存在节点的迭代器,false>
cpp
pair<iterator,bool> insert(const T& data)
{
// 树为空
if (_root == nullptr)
{
// 直接插入到根节点,返回
_root = new Node(data);
_root->_col = BLACK;
return make_pair(iterator(_root), true);
}
// 树不为空,寻找合适位置插入
Node* parent = nullptr;
Node* cur = _root;
KeyOfT kot;
while (cur)
{
if (kot(data) > kot(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else if (kot(data) < kot(cur->_data))
{
parent = cur;
cur = cur->_left;
}
else
return make_pair(iterator(cur), false); // 不允许重复键值
}
// 插入新节点
cur = new Node(data);
cur->_parent = parent;
if (kot(cur->_data) > kot(parent->_data))
parent->_right = cur;
else
parent->_left = cur;
// 记录新插入的节点,最后返回
Node* newnode = cur;
// 检查是否违反红黑树性质
// ...
// 不管如何调整,根节点必是黑色
_root->_col = BLACK;
return make_pair(iterator(newnode), true);
}
map 和 set 的其他接口
完成了红黑树的迭代器后,map 和 set 直接调用红黑树迭代器相关接口即可。
首先是在 map 和 set 中将红黑树的迭代器进行 typedef,注意 typename 的使用。在模板实例化之前,编译器分不清 iterator 是 RBTree中的类型名还是静态成员,加 typename 是让编译器知道我们取的是类型名
cpp
class map
{
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
typedef typename RBTree<K, pair<const K, const V>, MapKeyOfT>::const_iterator const_iterator;
};
class set
{
public:
typedef typename RBTree<K, const K, SetKeyOfT>::iterator iterator;
typedef typename RBTree<K, const K, SetKeyOfT>::const_iterator const_iterator;
};
begin 和 end
直接调用红黑树的迭代器接口即可
cpp
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
const_iterator begin() const
{
return _t.begin();
}
const_iterator end() const
{
return _t.end();
}
find
cpp
iterator find(const K& key)
{
return _t.find(key);
}
insert
cpp
// map
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _t.insert(kv);
}
// set
pair<iterator, bool> insert(const K& key)
{
return _t.insert(key);
}
map的operator[]
上面 map 的使用部分,说过了 operator[] 的实现原理,这里不再赘述,直接实现
cpp
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
iterator it = ret.first;
return it->second;
}
拷贝构造等默认成员函数
由于 map 和 set 底层是红黑树,所以这些默认成员函数不用我们手动去写,编译器会自动生成。而自动生成的默认成员函数对于自定义类型,会自动调用其成员函数
测试
map
迭代器测试
cpp
void test_map1()
{
// 测试迭代器
map<string, int> m;
m.insert({ "苹果",1 });
m.insert({ "香蕉",1 });
m.insert({ "梨",1 });
m.insert({ "苹果",3 });
map<string, int>::iterator it = m.begin();
while (it != m.end())
{
it->second += 1;
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
operator[] 测试
cpp
void test_map2()
{
// 统计次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
map<string, int> countMap;
for (auto& e : arr)
{
countMap[e]++;
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
拷贝、赋值
cpp
void test_map3()
{
map<string, int> m1;
m1.insert({ "苹果",1 });
m1.insert({ "香蕉",1 });
m1.insert({ "梨",1 });
m1.insert({ "苹果",3 });
// 拷贝构造
map<string, int> copy(m1);
for (auto& e : copy)
cout << e.first << ":" << e.second << endl;
cout << "---------" << endl;
// 赋值重载
map<string, int> m2;
m2 = m1;
for (auto& e : m2)
cout << e.first << ":" << e.second << endl;
}
set
迭代器测试
cpp
void test_set1()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(5);
s.insert(15);
s.insert(7);
s.insert(1);
s.insert(5);
s.insert(7);
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl << "-------------" << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
拷贝、赋值
cpp
void test_set2()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(5);
s.insert(15);
s.insert(7);
s.insert(1);
s.insert(5);
s.insert(7);
// 拷贝构造 + 赋值重载
set<int> copy = s;
for (auto e : copy)
{
cout << e << " ";
}
cout << endl;
}
代码
RBTree.h
cpp
#pragma once
enum Color
{
RED,
BLACK
};
template <class T>
struct RBTreeNode
{
RBTreeNode(const T& data)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
, _col(RED)
{}
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
T _data;
Color _col;
};
template <class T, class Ref, class Ptr>
struct __RBTreeIterator
{
typedef RBTreeNode<T> Node;
typedef __RBTreeIterator<T, Ref, Ptr> Self;
// 使用节点指针构造
__RBTreeIterator(Node* node)
:_node(node)
{}
// 迭代器底层就是一个 Node 节点指针
Node* _node;
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &(_node->_data);
}
Self& operator++()
{
// 存在右子树,返回右子树最左节点迭代器
if (_node->_right)
{
Node* leftMin = _node->_right;
while (leftMin && leftMin->_left)
leftMin = leftMin->_left;
_node = leftMin;
}
else
{
// 右子树不存在,向上找 孩子是父节点左子树 的祖先节点
Node* cur = _node;
Node* parent = cur->_parent;
// 当 cur 是根节点时,parent 为空,循环终止
// 当找到符合条件的根节点时,循环也会终止
while (parent && cur == parent->_right)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
Self& operator--()
{
// 左子树存在,寻找左子树最右节点
if (_node->_left)
{
Node* rightMax = _node->_left;
while (rightMax && rightMax->_right)
rightMax = rightMax->_right;
_node = rightMax;
}
else
{
// 左子树不存在,找 孩子是父节点右子树 的那个祖先
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
};
template <class K, class T, class KeyOfT>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
typedef __RBTreeIterator<T, T&, T*> iterator;
typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
Node* leftMin = _root;
while (leftMin && leftMin->_left)
leftMin = leftMin->_left;
return iterator(leftMin);
}
iterator end()
{
return iterator(nullptr);
}
const_iterator begin() const
{
Node* leftMin = _root;
while (leftMin && leftMin->_left)
leftMin = leftMin->_left;
return const_iterator(leftMin);
}
const_iterator end() const
{
return const_iterator(nullptr);
}
RBTree() = default;
~RBTree()
{
destroy(_root);
_root = nullptr;
}
RBTree(const RBTree<K, T, KeyOfT>& t)
{
_root = copy(t._root);
}
RBTree<K, T, KeyOfT> operator=(RBTree<K, T, KeyOfT> t)
{
swap(_root, t._root);
return *this;
}
iterator find(const K& key)
{
// 树为空
if (_root == nullptr)
return end();
// 树不为空
KeyOfT kot;
Node* cur = _root;
while (cur)
{
if (key > kot(cur->_data))
cur = cur->_right;
else if (key < kot(cur->_data))
cur = cur->_left;
else
return iterator(cur);
}
// 找不到
return end();
}
pair<iterator,bool> insert(const T& data)
{
// 树为空
if (_root == nullptr)
{
// 直接插入到根节点,返回
_root = new Node(data);
_root->_col = BLACK;
return make_pair(iterator(_root), true);
}
// 树不为空,寻找合适位置插入
Node* parent = nullptr;
Node* cur = _root;
KeyOfT kot;
while (cur)
{
if (kot(data) > kot(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else if (kot(data) < kot(cur->_data))
{
parent = cur;
cur = cur->_left;
}
else
return make_pair(iterator(cur), false); // 不允许重复键值
}
// 插入新节点
cur = new Node(data);
cur->_parent = parent;
if (kot(cur->_data) > kot(parent->_data))
parent->_right = cur;
else
parent->_left = cur;
// 记录新插入的节点,最后返回
Node* newnode = cur;
// 检查是否违反红黑树性质
// parent 是红色就需要调整
// parent 不存在或者是黑色就结束
while (parent && parent->_col == RED)
{
// 寻找 g 和 u
Node* grandpa = parent->_parent;
Node* uncle = nullptr;
// 区分 u 是 g 的左还是右,情况2需要用到
if (parent == grandpa->_left)
uncle = grandpa->_right;
else
uncle = grandpa->_left;
// 情况1,u 存在且为红
if (uncle && uncle->_col == RED)
{
// p u 变黑,g变红
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 更新 cur 和 p
// 需不需要继续向上更新,交给 while 条件判断
// cur 是根节点,结束
// cur 是子树,且父节点为红色,继续调整
cur = grandpa;
parent = grandpa->_parent;
}
// 情况2,u不存在或者存在且为黑
else if (uncle == nullptr || uncle->_col == BLACK)
{
// u 为右树版本
if (uncle == grandpa->_right)
{
// 情况2.a,cur 为左树
if (cur == parent->_left)
{
// g
// p u
// c
// 右旋,改颜色
RotateR(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
// 情况 2.b,cur 为右树
else if (cur == parent->_right)
{
// g
// p u
// c
// 以p左旋,再以g右旋,改颜色
RotateL(parent);
RotateR(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
}
// u 为左树版本
else if (uncle == grandpa->_left)
{
// 情况2.a,cur 为 p 的右树
if (cur == parent->_right)
{
// g
// u p
// c
// 以 g 左旋,更改颜色
RotateL(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
// 情况2.b,cur 为 p 的左树
else if (cur == parent->_left)
{
// g
// u p
// c
// 以 p 右旋,再以 g 右旋,更改颜色
RotateR(parent);
RotateL(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
}
// 情况2结束后就不需要调整了
break;
}
}
// 不管如何调整,根节点必是黑色
_root->_col = BLACK;
return make_pair(iterator(newnode), true);
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 改变链接
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* pparent = parent->_parent;
parent->_parent = subL;
// 检查 p 是根节点还是子树
if (pparent)
{
// 子树
subL->_parent = pparent;
if (parent == pparent->_right)
pparent->_right = subL;
else
pparent->_left = subL;
}
else
{
// 根节点
_root = subL;
_root->_parent = nullptr;
}
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 更改链接关系
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* pparent = parent->_parent;
parent->_parent = subR;
// 检查 p 是子树还是根
if (pparent)
{
// 子树
subR->_parent = pparent;
if (parent == pparent->_right)
pparent->_right = subR;
else
pparent->_left = subR;
}
else
{
// 根节点
_root = subR;
_root->_parent = nullptr;
}
}
bool isBalance()
{
// 计算任一路径黑色节点数
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
++refNum;
// 计算最左路径黑色节点数
cur = cur->_left;
}
return _isBalance(_root, 0, refNum);
}
private:
void destroy(Node* root)
{
if (root == nullptr) return;
destroy(root->_left);
destroy(root->_right);
delete root;
root = nullptr;
}
Node* copy(Node* root)
{
if (root == nullptr) return nullptr;
// 拷贝根节点
Node* copynode = new Node(root->_data);
copynode->_col = root->_col;
// 拷贝左子树,链接
copynode->_left = copy(root->_left);
if (copynode->_left) // 左子树存在,链接
copynode->_left->_parent = copynode;
// 拷贝右子树
copynode->_right = copy(root->_right);
if (copynode->_right) // 右子树存在,链接
copynode->_right->_parent = copynode;
return copynode;
}
bool _isBalance(Node* root, int blackNum, int refNum)
{
if (root == nullptr)
{
// 到达红黑树叶子节点
// 判断黑色节点数量是否相同
if (blackNum != refNum)
{
cout << "存在黑色节点数量不同的路径" << endl;
return false;
}
return true;
}
// 存在连续红色节点
if (root->_col == RED && root->_parent->_col == RED)
{
KeyOfT kot;
cout << kot(root->_data) << "存在连续红色节点" << endl;
return false;
}
// 记录黑色节点数
if (root->_col == BLACK)
++blackNum;
// 判断左右子树是否是红黑树
return _isBalance(root->_left, blackNum, refNum) && _isBalance(root->_right, blackNum, refNum);
}
Node* _root = nullptr;
};
Map.h
cpp
#pragma once
namespace ns1
{
template <class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
typedef typename RBTree<K, pair<const K, const V>, MapKeyOfT>::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();
}
iterator find(const K& key)
{
return _t.find(key);
}
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()));
iterator it = ret.first;
return it->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
void test_map1()
{
// 测试迭代器
map<string, int> m;
m.insert({ "苹果",1 });
m.insert({ "香蕉",1 });
m.insert({ "梨",1 });
m.insert({ "苹果",3 });
map<string, int>::iterator it = m.begin();
while (it != m.end())
{
it->second += 1;
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
void test_map2()
{
// 统计次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
map<string, int> countMap;
for (auto& e : arr)
{
countMap[e]++;
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
void test_map3()
{
map<string, int> m1;
m1.insert({ "苹果",1 });
m1.insert({ "香蕉",1 });
m1.insert({ "梨",1 });
m1.insert({ "苹果",3 });
// 拷贝构造
map<string, int> copy(m1);
for (auto& e : copy)
cout << e.first << ":" << e.second << endl;
cout << "---------" << endl;
// 赋值重载
map<string, int> m2;
m2 = m1;
for (auto& e : m2)
cout << e.first << ":" << e.second << endl;
}
}
Set.h
cpp
#pragma once
namespace ns1
{
template <class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename RBTree<K, const K, SetKeyOfT>::iterator iterator;
typedef typename RBTree<K, const K, SetKeyOfT>::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();
}
iterator find(const K& key)
{
return _t.find(key);
}
pair<iterator, bool> insert(const K& key)
{
return _t.insert(key);
}
private:
RBTree<K, const K, SetKeyOfT> _t;
};
void test_set1()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(5);
s.insert(15);
s.insert(7);
s.insert(1);
s.insert(5);
s.insert(7);
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl << "-------------" << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
void test_set2()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(5);
s.insert(15);
s.insert(7);
s.insert(1);
s.insert(5);
s.insert(7);
// 拷贝构造 + 赋值重载
set<int> copy = s;
for (auto e : copy)
{
cout << e << " ";
}
cout << endl;
}
}
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
iterator it = ret.first;
return it->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
void test_map1()
{
// 测试迭代器
map<string, int> m;
m.insert({ "苹果",1 });
m.insert({ "香蕉",1 });
m.insert({ "梨",1 });
m.insert({ "苹果",3 });
map<string, int>::iterator it = m.begin();
while (it != m.end())
{
it->second += 1;
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
void test_map2()
{
// 统计次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
map<string, int> countMap;
for (auto& e : arr)
{
countMap[e]++;
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
void test_map3()
{
map<string, int> m1;
m1.insert({ "苹果",1 });
m1.insert({ "香蕉",1 });
m1.insert({ "梨",1 });
m1.insert({ "苹果",3 });
// 拷贝构造
map<string, int> copy(m1);
for (auto& e : copy)
cout << e.first << ":" << e.second << endl;
cout << "---------" << endl;
// 赋值重载
map<string, int> m2;
m2 = m1;
for (auto& e : m2)
cout << e.first << ":" << e.second << endl;
}
}
Set.h
cpp
#pragma once
namespace ns1
{
template <class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename RBTree<K, const K, SetKeyOfT>::iterator iterator;
typedef typename RBTree<K, const K, SetKeyOfT>::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();
}
iterator find(const K& key)
{
return _t.find(key);
}
pair<iterator, bool> insert(const K& key)
{
return _t.insert(key);
}
private:
RBTree<K, const K, SetKeyOfT> _t;
};
void test_set1()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(5);
s.insert(15);
s.insert(7);
s.insert(1);
s.insert(5);
s.insert(7);
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl << "-------------" << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
void test_set2()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(5);
s.insert(15);
s.insert(7);
s.insert(1);
s.insert(5);
s.insert(7);
// 拷贝构造 + 赋值重载
set<int> copy = s;
for (auto e : copy)
{
cout << e << " ";
}
cout << endl;
}
}