[Abstract]
I. 引言
- A. 简介搜索二叉树(BST)
- B. BST 的基本性质和操作
II. 搜索二叉树的不足
- A. 最差情况下的时间复杂度分析
- B. 不平衡树导致的性能退化
III. AVL 树的优化
- A. AVL 树的定义和实现
- B. AVL 树插入新节点
- B. 通过旋转操作维持平衡
- C. AVL 树的性能分析
IV. 红黑树的优化
- A. 红黑树的定义和性质
- B. 红黑树相对于 AVL 树的平衡性放宽
- C. 插入和删除操作
V. C++ STL 中的 Map 与 Set
- A. Map 和 Set 的概述
- B. STL 中的红黑树实现
- C. STL 中的红黑树是如何维护平衡的
- D. 通过 Map 和 Set 学习红黑树的应用
VI. C++ STL 中的 Map 与 Set 模拟实现
- A. 了解 C++ STL 中 Map 和 Set 的常见应用场景
- B. 分析 STL 源码中的关键部分,如节点结构、插入、删除、查找等操作
- C. 实现简化版的 Map 和 Set,加深对搜索二叉树和其优化的理解
VII. 结论
- A. 总结搜索二叉树及其优化的重要性
- B. 总结 C++ STL 中 Map 与 Set 的模拟实现过程
- C. 对学习和使用搜索二叉树的建议
I. 引言
A. 简介搜索二叉树(Binary Search Tree)
概念:
二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
B. BST 的基本性质和操作
0. 节点和树设计
- 节点设计:
template<class K,class V>
struct BSTreeNode
{
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
K _key;
V _value;
BSTreeNode(const K& key = K(), const V& value = V())
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
- 树的设计:
template<class K,class V>
class BSTree
{
typedef BSTreeNode<K,V> Node;
public:
// 类的默认成员函数
BSTree() = default;
BSTree(const BSTree<K, V>& t)
{
_root = Copy(t._root);
}
BSTree<K,V>& operator=(BSTree<K,V> t)
{
swap(_root, t._root);
return *this;
}
~BSTree()
{
Destroy(_root);
}
// 插入
bool Insert(const K& key, const V& value)
// 递归版本的插入
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
// 查找
Node* Find(const K& key)
// 递归版本的查找
bool FindR(const K& key)
{
return _FindR(_root, key);
}
// 删除
bool Erase(const K& key)
// 递归版本的删除
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
// 递归实现中序遍历
void InOrder()
private:
Node* Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* newRoot = new Node(root->_key);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
void Destroy(Node*& root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
bool _FindR(Node* root, const K& key)
bool _InsertR(Node*& root, const K& key)
bool _EraseR(Node*& root, const K& key)
private:
Node* _root = nullptr; // 根节点
};
1. 二叉搜索树的查找
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找
b、最多查找h(高度)次,走到空还没找到,说明这个值不存在
- 非递归实现:
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
- 递归实现:
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
else if (root->_key < key)
{
return _FindR(root->_right, key);
}
else if (root->_key > key)
{
return _FindR(root->_left, key);
}
else
{
return true;
}
}
2. 二叉搜索树的插入
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点,插入的位置实际上是原树原本的一个叶子节点的左右树(nullptr)
- 非递归实现:
bool Insert(const K& key, const V& value)
{
// 如果树为空,直接创建一个新的节点作为根节点
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
// 初始化父节点和当前节点为根节点
Node* parent = _root;
Node* cur = _root;
// 遍历树,找到合适的插入位置
while (cur)
{
if (cur->_key < key)
{
// 当前节点键值小于要插入的键,移动到右子树
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
// 当前节点键值大于要插入的键,移动到左子树
parent = cur;
cur = cur->_left;
}
else
{
// 键已存在,返回插入失败
return false;
}
}
// 创建新节点
cur = new Node(key, value);
// 根据键的大小关系插入到父节点的左子树或右子树
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
- 递归实现:
bool FindR(const K& key)
{
// 调用递归查找函数
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& key)
{
// 递归基:当前节点为空,未找到键
if (root == nullptr)
{
return false;
}
else if (root->_key < key)
{
// 当前节点键值小于要查找的键,在右子树中递归查找
return _FindR(root->_right, key);
}
else if (root->_key > key)
{
// 当前节点键值大于要查找的键,在左子树中递归查找
return _FindR(root->_left, key);
}
else
{
// 键相等,找到键
return true;
}
}
3. 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键值最小),(也就是找到整个右子树中 最小的节点),用这个值填补到被删除节点中,再来处理该结点的删除问题–替换法删除
- 非递归实现:
bool Erase(const K& key)
{
// 初始化父节点和当前节点为 nullptr 和根节点
Node* parent = nullptr;
Node* cur = _root;
// 遍历树,找到要删除的节点
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
// 进入 else 说明已经定位到了要删除的节点
else
{
// 如果左子树为空
if (cur->_left == nullptr)
{
// 更新父节点的指针,删除当前节点
if (cur == _root)
_root = cur->_right;
else
(parent->_left == cur) ? parent->_left = cur->_right : parent->_right = cur->_right;
}
// 如果右子树为空
else if (cur->_right == nullptr)
{
// 更新父节点的指针,删除当前节点
if (cur == _root)
_root = cur->_left;
else
(parent->_left == cur) ? parent->_left = cur->_left : parent->_right = cur->_left;
}
// 如果左右子树都不为空
else
{
// 找到右子树的最小节点(最左节点)
Node* parent = cur;
Node* subLeft = cur->_right;
while (subLeft->_left)
{
parent = subLeft;
subLeft = subLeft->_left;
}
// 交换当前节点和最小节点的键值
swap(cur->_key, subLeft->_key);
// 删除右子树的最小节点
(parent->_left == subLeft) ? parent->_left = subLeft->_right : parent->_right = subLeft->_right;
}
// 删除成功,返回 true
return true;
}
}
// 未找到要删除的节点,返回 false
return false;
}
- 递归实现:
bool EraseR(const K& key)
{
// 调用递归删除函数
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const K& key)
{
// 递归基:当前节点为空,未找到要删除的键
if (root == nullptr)
return false;
// 根据键值比较决定在左子树或右子树中递归删除
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else
{
// 删除
if (root->_left == nullptr)
{
// 如果左子树为空,删除当前节点,用右子树替代
Node* del = root;
root = root->_right;
delete del;
return true;
}
else if (root->_right == nullptr)
{
// 如果右子树为空,删除当前节点,用左子树替代
Node* del = root;
root = root->_left;
delete del;
return true;
}
else
{
// 如果左右子树都不为空
// 找到右子树的最小节点(最左节点)
Node* subLeft = root->_right;
while (subLeft->_left)
{
subLeft = subLeft->_left;
}
// 交换当前节点和最小节点的键值
swap(root->_key, subLeft->_key);
// 在右子树中递归删除最小节点
return _EraseR(root->_right, key);
}
}
}
II. 搜索二叉树的不足
A. 最差情况下的时间复杂度分析
- 因为BST的插入和删除操作都必须先查找,因此查找效率代表了基本二叉搜索树中各个操作的性能。
- 对有 N 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即树越高,则比较次数越多。
- 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:
l
o
g
2
N
log_2 N
log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:
N
2
\frac{N}{2}
2N
[!question] 问题:如果退化成单支树,二叉搜索树相比于链表的性能优势就失去了
那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?
引入两种平衡二叉树:AVL树和红黑树。
III. AVL 树的优化
A. AVL 树的定义和实现
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
定义
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子 Balance Factor)的绝对值不超过1 (-1/0/1)
1. 节点定义
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
pair<K, V> _kv;
int _bf; //balance factor 平衡因子
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
};
2. 树的定义
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
// 插入
bool Insert(const pair<K, V>& kv)
// 左旋
void RotateL(Node* parent)
// 右旋
void RotateR(Node* parent)
// 右左双旋
void RotateRL(Node* parent)
// 左右双旋
void RotateLR(Node* parent)
// 中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
Node* _root = nullptr;
};
B. AVL 树插入新节点
- 代码实现:
bool Insert(const pair<K, V>& kv)
{
// 把cur定位到插入位置
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
// 先按照二叉搜索树的规则将节点插入到AVL树中
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
// 插入
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 更新平衡因子和旋转
while (parent)
{
// 向上更新bf
if (cur == parent->_left)
{
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)
{
cout << "RotateL" << endl;
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
cout << "RotateR" << endl;
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
cout << "RotateRL" << endl;
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
cout << "RotateLR" << endl;
RotateLR(parent);
}
break;
}
else
{
assert(false);
}
}
return true;
}
C. 通过旋转操作维持平衡
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
1. 新节点插入较高左子树的左侧 — 左左:右单旋
- 代码实现:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
parent->_bf = subR->_bf = 0;
}
2. 新节点插入较高右子树的右侧—右右:左单旋
- 代码实现:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
subL->_bf = parent->_bf = 0;
}
3. 新节点插入较高左子树的右侧—左右:先左单旋再右单旋
- 代码实现:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
// subRL自己就是新增
parent->_bf = subR->_bf = subRL->_bf = 0;
}
else if (bf == -1)
{
// subRL的左子树新增
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 1)
{
// subRL的右子树新增
parent->_bf = -1;
subRL->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
4. 新节点插入较高右子树的左侧—右左:先右单旋再左单旋
- 代码实现:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
// 先对左子树进行左旋
RotateL(parent->_left);
// 再对父节点进行右旋
RotateR(parent);
// 调整平衡因子
if (bf == 0)
{
// subLR自己就是新增
parent->_bf = subL->_bf = subLR->_bf = 0;
}
else if (bf == -1)
{
// subLR的左子树新增
parent->_bf = 0;
subL->_bf = 1;
subLR->_bf = 0;
}
else if (bf == 1)
{
// subLR的右子树新增
parent->_bf = -1;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
[!attention] 总结
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
- pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
- 当pSubR的平衡因子为1时,执行左单旋
- 当pSubR的平衡因子为-1时,执行右左双旋
- pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
- 当pSubL的平衡因子为-1是,执行右单旋
- 当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
D. AVL 树的性能分析
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合,因此我们引入红黑树。
IV. 红黑树的优化
A. 红黑树的定义和性质
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
五条性质:
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
B. 红黑树相对于 AVL 树的平衡性放宽
红黑树不追求绝对平衡,只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优。
[!question] 为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?
把一条从根到叶子的路径简称为路径,红黑树的每条路径都有相同的黑色节点,黑色节点之间的红色节点是可有可无的,那么最短的路径是全黑的,最长的路径是红黑相间的。
- 红黑树节点定义:
// 节点的颜色
enum Colour
{
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; // 节点的值域
Colour _col; // 节点的颜色
};
C. 插入和删除操作
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按照二叉搜索的树规则插入新节点
bool Insert(const ValueType& data)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
// 1. 按照二叉搜索的树方式插入新节点
// 2. 检测新节点插入后,红黑树的性质是否造到破坏,
// 若满足直接退出,否则对红黑树进行旋转着色处理
// 根节点的颜色可能被修改,将其改回黑色
_root->_col = BLACK;
return true;
}
- 检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
-
情况一: cur为红,p为红,g为黑,u存在且为红
-
情况二: 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 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 (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
// 新增节点给红色
cur = new Node(kv);
cur->_col = RED;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
// g
// p u
// c
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上更新处理
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// 单旋
// g
// p
// c
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// 双旋
// g
// p
// c
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else // parent == grandfather->_right
{
// g
// u p
// c
//
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// u p
// c
//
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
V. C++ STL 中的 Map 与 Set
A. Map 和 Set 的概述
根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
set的介绍
- set是按照一定次序存储元素的容器
- 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
- 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行
排序。 - set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
- set在底层是用二叉搜索树(红黑树)实现的。
[!attention] 注意:
- 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。
- set中插入元素时,只需要插入value即可,不需要构造键值对。
- set中的元素不可以重复(因此可以使用set进行去重)。
- 使用set的迭代器遍历set中的元素,可以得到有序序列
- set中的元素默认按照小于来比较
- set中查找某个元素,时间复杂度为: l o g 2 n log_2 n log2n
- set中的元素不允许修改
- set中的底层使用二叉搜索树(红黑树)来实现。
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通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
B. STL 中的红黑树实现
VI. C++ STL 中的 Map 与 Set 模拟实现
为map和set特化的红黑树,支持了迭代器。
- RBTree.h:
#pragma once
#include <utility>
using std::pair;
using std::make_pair;
namespace chen
{
// 节点的颜色
enum Colour
{
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; // 节点的值域
Colour _col; // 节点的颜色
};
// 迭代器
template<class T, class Ptr, class Ref>
struct __TreeIterator
{
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T, Ptr, Ref> Self;
// typedef __TreeIterator<T, T*, T&> Iterator;
__TreeIterator(const Self& it)
:_node(it._node)
{}
__TreeIterator(Node* node)
:_node(node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node != s._node;
}
Self& operator--()
{
if (_node->_left)
{
Node* subRight = _node->_left;
while (subRight->_right)
{
subRight = subRight->_right;
}
_node = subRight;
}
else
{
// 孩子是父亲的右的那个节点
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
Self& operator++()
{
if (_node->_right)
{
// 右树的最左节点(最小节点)
Node* subLeft = _node->_right;
while (subLeft->_left)
{
subLeft = subLeft->_left;
}
_node = subLeft;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
// 找孩子是父亲左的那个祖先节点,就是下一个要访问的节点
while (parent && cur == parent->_right)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
public:
Node* _node;
};
// 红黑树
//
// 对于set: RBTree<K, K,SetKeyOfT> _t
// 对于map: RBTree <K, pair<K, T>, MapKeyOfT> _t
//
// KeyOfT 是用于提取T中的key(first)的仿函数
template<class K, class T, class KeyOfT>
class RBTree
{
public:
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T, T*, T&> iterator;
typedef __TreeIterator<T, const T*, const T&> const_iterator;
public:
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);
}
pair<iterator, bool> Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return std::make_pair(iterator(_root), true);
}
Node* parent = nullptr;
Node* cur = _root;
KeyOfT kot;
while (cur)
{
if (kot(cur->_data) < kot(data))
{
parent = cur;
cur = cur->_right;
}
else if (kot(cur->_data) > kot(data))
{
parent = cur;
cur = cur->_left;
}
else
{
return make_pair(iterator(cur), false);
}
}
// 新增节点给红色
cur = new Node(data);
cur->_col = RED;
if (kot(parent->_data) < kot(data))
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
// g
// p u
// c
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上更新处理
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// 单旋
// g
// p
// c
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// 双旋
// g
// p
// c
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else // parent == grandfather->_right
{
// g
// u p
// c
//
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// u p
// c
//
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return std::make_pair(iterator(cur), true);
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL)
subRL->_parent = parent;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
void InOrder()
{
_InOrder(_root);
std::cout << std::endl;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
std::cout << root->_data << " ";
_InOrder(root->_right);
}
// 根节点->当前节点这条路径的黑色节点的数量
bool Check(Node* root, int blacknum, const int refVal)
{
if (root == nullptr)
{
//std::cout << balcknum << std::endl;
if (blacknum != refVal)
{
std::cout << "存在黑色节点数量不相等的路径" << std::endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)
{
std::cout << "有连续的红色节点" << std::endl;
return false;
}
if (root->_col == BLACK)
{
++blacknum;
}
return Check(root->_left, blacknum, refVal)
&& Check(root->_right, blacknum, refVal);
}
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root->_col == RED)
return false;
//参考值
int refVal = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
++refVal;
}
cur = cur->_left;
}
int blacknum = 0;
return Check(_root, blacknum, refVal);
}
int Height()
{
return _Height(_root);
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
size_t Size()
{
return _Size(_root);
}
size_t _Size(Node* root)
{
if (root == nullptr)
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
private:
Node* _root = nullptr;
};
}
- my_set.h:
#pragma once
#include "RBTree.h"
namespace chen
{
template<class K>
class set
{
public:
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;
typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;
const_iterator begin() const
{
return _t.begin();
}
const_iterator end() const
{
return _t.end();
}
pair<iterator, bool> insert(const K& key)
{
pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> ret = _t.Insert(key);
return pair<iterator, bool>(ret.first, ret.second);
}
private:
RBTree<K, K, SetKeyOfT> _t;
};
}
- my_map.h
#pragma once
#include "RBTree.h"
namespace chen
{
template<class K, class V>
class map
{
public:
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;
typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::const_iterator const_iterator;
iterator begin()
{
return iterator(_t.begin());
}
iterator end()
{
return iterator(_t.end());
}
const_iterator begin() const
{
return const_iterator(_t.begin());
}
const_iterator end() const
{
return const_iterator(_t.end());
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _t.Insert(kv);
}
private:
RBTree <K, std::pair<K, V>, MapKeyOfT> _t;
};
}
VII. 结论
搜索二叉树及其优化,如 AVL 树和红黑树,是重要的数据结构。通过对搜索二叉树的深入理解,我们不仅能够构建高效的数据存储和检索系统,还能够通过优化方法提高其性能。在本文中,我们探讨了搜索二叉树的基本概念,然后深入研究了 AVL 树和红黑树这两种常用的优化方案,它们在维护平衡性上起到了关键作用。
在 C++ STL 中,Map 和 Set 是两个基于搜索二叉树实现的重要容器。我们模拟了它们的基本实现过程,了解了底层数据结构的运作原理。通过这个过程,我们不仅更好地理解了 C++ STL 中这两个容器的使用方法,还深入探讨了搜索二叉树在实际编程中的应用。
总的来说,搜索二叉树及其优化在计算机科学领域具有广泛的应用,对于构建高效的数据结构和提高算法性能至关重要。然而,我们也要注意到在某些特定场景下,选择合适的数据结构是至关重要的。在使用搜索二叉树时,我们需要考虑数据的特点以及对性能的要求,选择合适的优化方案。
对于学习和使用搜索二叉树的建议,我们强调了深入理解基本概念的重要性。通过掌握 AVL 树和红黑树等优化方法,我们能够更好地应用这些数据结构,并在实际问题中取得更好的效果。此外,对于 C++ 开发者来说,熟悉 STL 中 Map 和 Set 的实现不仅有助于更好地使用这两个容器,还能够提高对底层数据结构的认识。