红黑树完全剖析
前言:
在 STL 的容器中两大阵营 (vector deque list forward_list) 顺序容器 与 关联容器 (map set unorder_map unorder_set) 。
关联容器指的是以关键字匹配指定数据的一种关联数据结构的集合, 在关联容器中通过关键字去查找对应的数据是非常块的, 下面我们会彻底地去剖析其底层实现机制 : 红黑树。
第一部分:由基础树 到 二叉搜索树
介绍红黑树之前,容许我介绍树的概念。
1. 树(tree), 在计算机科学中,是十分基础的数据结构:
- 几乎所有 OS 都会将文件存在 树状结构中(Linux 的 VFS 也是如此)。
- 编译器的实现需要一个表达式树(用于词法分析)
- 文件压缩的 哈夫曼算法 也是使用的 树状结构。
- 数据库所使用的 B-tree 则是一种相当复杂的树状结构。
树的结构特点:
- 树由节点和边构成
- 相连节点中,在上者称为父节点,下者为子节点, 无子的节点称为叶节点
- 子节点可以有很多,如果最多只允许有两个子节点, 即二叉树
- 从根到任一节点的路径成度即 节点的深度,根的深度是 0 , 某节点到最深子节点的路径长度,称为该节点的高度
2. 二叉搜索树
我们提到二叉树的特点是: 任意节点仅允许有两个孩子。
二叉搜索树:
- 任何节点的键值一定大于它的左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值。
- 因此从根节点一直往左走直到无路可走,可得到最小元素, 从根节点一直往右走,直至无右路可走, 可得最大元素。
- 提供 O(logn) 的元素插入与访问。
如图所示:我们找一个值,只需要从根节点出发,与根节点去比较,若大于根节点去右子树找,若小于去左子树中找(每次淘汰一半的子树),这样只需要 O(logn)就能找到你要找的元素。
二叉搜索树的插入和删除
插入: 从根节点出发,遇到键值大的就向左, 遇到键值小的就向右,一直到尾端,即为插入点。
删除:
- 如果删除的节点只有一个子节点,我们就将 A 的子节点连接到 A 的父节点,并将 A 删除
删除 12 节点:
- 如果 A 有两个子节点, 那么我们就以右子树的最小节点取代 A,(从右子节点开始,一直走到左直到底就是 其 右子树的最小节点),将 A 赋予其值,并将其删除。
删除 10 节点:
第二部分:平衡二叉搜索树的引入 AVL_tree
二叉搜索树的不平衡问题
也许因为输入值不够随机,或者因为经过某些插入或删除操作,二叉搜索树可能失去平衡,造成搜索效率低落的情况(退化成链表),如图:
平衡二叉搜索树的维护平衡的策略
”平衡“ 的大致意义是:没有任何一个节点过深(深度过大)。
为了保证树形的平衡,衍生出了各种解决该问题的特殊结构: AVL_tree, RB_tree 等等。
AVL tree 是一个”加上了额外平衡条件“ 的二叉搜索树,其平衡条件的建立是为了保证整棵树的深度为 O(logN)
接下来让我们了解一下额外平衡条件到底是怎么操作的:
如图所示 AVL tree:
插入了节点 11 后,灰色节点违反了 AVL tree 的平衡条件,由于只有 “插入点至根节点”路径上的各节点可能改变平衡状态,因此,只需要调整其中最深的那个节点,便可以使得整个树重新获取平衡。
此时我们看到 18 节点的两颗子树因为 11 的插入平衡被破坏,而 18 这个节点就是从插入点到根路径上的最深节点X, 因此我们可以轻而易举地将情况分为以下四种:
- 插入点位于 X 的 左子节点的左子树 ------左左
- 插入点位于 X 的 左子节点的右子树 -------左右
- 插入点位于 X 的 右子节点的左子树--------右左
- 插入点位于 X 的 右子节点的右子树--------右右
情况 左左,和右右,可以看作是 外侧插入,使用单旋转即可调整,
而 右左,左右 则称为内侧插入, 可以采用双旋转操作调整解决。
单旋转双旋转
旋转: 将旋转点(x)以某个方向进行偏移一单位,若左旋,则其右子节点将变为其 x 的父节点, 由于 右子节点的左子树必须挂在其左侧,所以旋转后原右子节点的左子树必须挂在旋转点(x) 的右侧,才能继续维持二叉搜索树的性质。
单旋转的精华是:
左旋:将旋转的根节点的右孩子的左孩子变成根节点的左孩子的右子树,再将根节点向左旋转,令根节点的右孩子为新根,原根节点成为现根节点的左孩子。
右旋:将旋转的根节点的左孩子的右子树变成根节点的右孩子的左子树,再将根节点向右旋转,令根节点的左孩子为新根,原根节点成为现根节点的右孩子。
双旋转的精华是:
两次单旋转
第三部分:RB-Tree (红黑树)
颇具历史并且被广泛应用的自平衡二叉搜索树是 RB-tree (红黑树),平衡的概念指叶子节点间的最大深度不能超过1,但红黑树不是这样, 它以牺牲部分的平衡性换取了操作上/旋转次数的降低, 插入操作旋转次数不超过2,删除操作不超过3,它不仅仅是一个二叉搜索树,它必须满足以下规则:
- 节点是红色或黑色
- 根是黑色
- 所有叶子都是黑色(叶子是 NULL 节点)
- 每一个红色节点必须有两个黑色的子节点(不能连续红)
- 从任一节点到每个叶子的所有简单路径都包含相同数目的黑色节点。(维持一种黑色平衡),这点证明了它并非是平衡二叉搜索树。
1*该路径上的黑色节点数量 <= 任意路径长度 <= 2 * 该路径上的黑色节点数目(红色节点和黑色节点一样多)
从上述特点得知:
- 新增节点必须为红
- 新增节点之父节点必须为黑。
上图:白节点默认是红色节点
操作
- 因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的查找和修改与普通二叉搜索树的操作一样
- 插入和删除操作会导致不再匹配红黑树的性质, 恢复红黑树的性质需要少量 O(logn)的颜色变更和不超过三次的树旋转。
红黑树的节点插入
在插入新节点时, 以二叉查找树的方法增加节点并标记其为红色(设置黑色违反性质 5)
下面是否要进行调整和如何调整取决于其临近节点的颜色,同人类的家族树一样,我们将使用术语叔父节点来指一个节点的父节点的兄弟节点。
注意:
- 性质 1 和性质总是保持着
- 性质 4 只在增加红色节点, 重绘黑色节点为红色,或做旋转时受到威胁。
- 性质 5 只在增加黑色节点,重绘红色节点为黑色, 或做旋转时受到威胁。
我们设定将要插入的节点标为 N, N 的父节点标为 P, N 的祖父节点为 G, N 的叔父节点为 U
通过下面操作,能找到一个节点的叔父节点和祖父节点
node *grandparent(node *n) {
return n->parent->parent;
}
node *uncle(node *n) {
if(n->parent == grandparent(n)->left)
return grandparent(n)->right;
else
return grandparent(n)->left;
}
情形 1: 新节点 N 位于树的根上,没有父节点,在这种情况下,我们将其绘制成黑色节点以满足性质2, 因为它在每个路径上对黑节点数目增加 1, 性质 5 也是符合的。
void insert_case1(node *n) {
if(n->parent == NULL)
n->color = BLACK;
else
insert_case2(n)
}
情形2: 新节点的父节点 P 是黑色, 所以性质 4 没有失效(新节点的确是红色的),这种情况下红黑树是有效的,性质 5 也没有受到威胁,尽管新节点 N 有两个黑色叶子子节点, 但由于新节点 N 是红色, 通过它的每个子节点的路径就都有通过它所取代的黑色的叶子的路径同样数目的黑色节点,所有依然满足这个性质。
void insert_case2(node *n) {
if(n->parent->color == BLACK)
return;
else
insert_case3(n);
}
注意: 在下列情形中,我们假定新节点的父节点为红色,所以其有祖父节点,如果父节点是根节点,那父节点就应当是黑色,所以新节点总有一个叔父节点,尽管在情形 4 和 5 它可能是叶子节点。
情形3: 父节点 P 和 叔父节点 U 都是红色(此时新插入节点N 作为P 的左子节点或右子节点都属于情形三这里我们演示 N 作为 P 左子节点的情形)则我们可以将它们两个重新绘制为黑色并重绘祖父节点 G 为红色(用来保持性质 5).现在我们的新节点 N 有了一个黑色的父节点P, 因为通过父节点P或叔父节点 U 的任何路径都必定通过祖父节点 G,在这些路径上的黑节点数目没有改变,
注意: 红色的祖父节点 G 的父节点也有可能是红色的, 这就违反了性质 4, 为了解决这个问题,我们在祖父节点 G 上递归地进行情形 1 的整个过程。(把 G 当成是新加入的节点进行各自情况的检查。)
void insert_case3(node *n) {
if(uncle(n) != NULL && uncle(n)->color == RED) {
n->parent->color = BLACK;
uncle(n)->color = BLACK;
grandparent(n)->color = RED;
insert_case1(grandparent(n));
}
else
insert_case4(n);
}
注意:在余下的情况下,我们假定父节点 P 是其父亲 G 的左子节点,如果是右子节点,情形 4 和情形 5的左和右应当对调。
情形4:父节点 P 是红色而叔父U是黑色或没有, 并且新节点 N 是其父节点 P 的右子节点而父节点 P 又是其父节点的左子节点,在这种情形下,我们进行一次左旋转调换新节点, 在这种情况下我们进行一次左旋转调换新节点和其父节点的角色,
接着, 我们按情形 5 处理以前的 父节点 P 以解决性质 4 失效的问题, 注意这个改变会导致某些路径通过它们以前不通过的新节点 N ,或不通过节点 P,但由于这两个节点都是红色,所以性质 5 仍然有效。
void insert_case4(node *n) {
if(n == n->parent->right && n->parent == grandparent(n)->left) {
rotate_left(n->parent):
n = n->left; // 方便 insert_case 5 操作
} else if(n == n->parent->left && n->parent == grandparent(n)->right) {
rotate_right(n->parent);
n = n->right;
}
insert_case5(n);
}
情形 5 : 父节点 P 是红色而叔父节点 U 是黑色或缺少,而父节点 P 是其父 G 的左子节点,在这种情况下,我们进行针对 祖父节点G 的一次右旋转; 在旋转产生的树中,以前的父节点 P 现在是新节点 N 和 以前的祖父节点 G 的父节点,我们知道以前的祖父节点 G 是黑色,否则父节点 P 就不可能是红色, (P 和 G 都是红色违反了性质4), 我们切换以前的父节点 P 和祖父节点 G 的颜色,结果的树满足性质 4, 性质 5 也满足, 因为通过这三个节点中任何一个的 所有路径以前都通过祖父节点 G, 现在它们都通过以前的父节点 P。
void insert_case5(node *5) {
n->parent->color = BLACK;
grandparent(n)->color = RED;
if(n == n->parent->left && n->parent == grandparent(n)->left)
rotate_right(grandparent(n));
else
rotate_left(grandparent(n));
}
第四部分: STL RB-tree 的实现
RB-tree 的节点设计
RB-tree 有红黑二色, 并且拥有左右子节点, 我们很容易就可以勾勒出其结构风貌。
下面是 STL 的节点实现源码
1 #ifndef RBTREENODE_H_
2 #define RBTREENODE_H_
3 typedef bool rb_tree_color_type;
4 const rb_tree_color_type rb_tree_red = false;
5 const rb_tree_color_type rb_tree_blcak = true;
6
7 struct RBTreeNodeBase {
8 typedef rb_tree_color_type color_type;
9 typedef RBTreeNodeBase* base_ptr;
10
11 color_type color; //节点颜色,非红即黑
12 base_ptr parent; // RB 树的许多操作都必须知道父节点
13 base_ptr left;
14 base_ptr right;
15 static base_ptr minimum(base_ptr x) {
16 while(x->left != 0)
17 x = x->left;
18 return x;
19 }
20 static base_ptr maximum(base_ptr x) {
21 while(x->right != 0)
22 x = x->right;
23 return x;
24 }
25 };
26
27 template <class Value >
28 struct RBTreeNode : RBTreeNodeBase {
29 typedef RBTreeNode<Value>* link_type;
30 Value value_field; //节点值
31 };
32 #endif
RB-tree 的数据结构
RB-Tree 的定义如下所述:
- 专属的空间配置器,每次用于配置一个节点的大小
- 各中型别定义
- 用于维持整颗 RB-tree 的 3 种数据(其中有个仿函数, functor, 用来表现节点的大小比较方式)
- 一些 member 和 function 的定义
#ifndef RBTREE_H_
2 #define RBTREE_H_
3 #include "RBTreeNode.h"
4 #include <memory>
5 template <class Key, class Value, class KeyOfValue, class Compare,
6 class Alloc = std::allocator<RBTreeNode<Value>>>
7 class RBTree {
8 protected:
9 typedef void *void_pointer;
10 typedef RBTreeNodeBase *base_ptr;
11 typedef RBTreeNode<Value> rb_tree_node;
12 typedef Alloc rb_tree_node_allocator;
13 typedef rb_tree_color_type color_type;
14 public:
15 typedef Key key_type;
16 typedef Value value_type;
17 typedef value_type *pointer;
18 typedef const value_type *const_pointer;
19 typedef value_type &reference;
20 typedef const value_type &const_reference;
21 typedef rb_tree_node *link_type;
22 typedef size_t size_type;
23 typedef ptrdiff_t difference_type;
24 protected:
25 link_type get_node() {
return rb_tree_node_allocator::allocate(); }
26 void put_node(link_type p) {
rb_tree_node_allocator::deallocate(p); }
27
28 link_type creat_node(const value_type &x) {
29 link_type tmp = get_node();
30 try {
31 rb_tree_node_allocator::construct(&tmp->value_field, x);
32 }
33 catch(...) {
34 put_node(tmp);
35 }
36
37 return tmp;
38 }
39 link_type clone_type(link_type x) {
40 link_type tmp = creat_node(x->value_field);
41 tmp->color = x->color;
42 tmp->left = 0;
43 tmp->right = 0;
44 return tmp;
45 }
46
47 void destory_node(link_type p) {
48 rb_tree_node_allocator::destory(&p->value_field);
49 put_node(p);
50 }
51 protected:
52 // RB-Tree 只以三笔数据表现
53 size_type node_count; // 追踪记录树的大小(节点数量)
54 link_type header; // 这是实现上的一个技巧
55 Compare key_compare; //节点间的键值大小比较准则, 应该会是一个function object
56
57 //以下三个函数用来方便取得 header 的成员
8 link_type& root() const {
return (link_type&) header->parent; }
59 link_type& leftmost() const {
return (link_type&) header->left; }
60 link_type& rightmost() const {
return (link_type&)header->right;}
61
62
63 //以下六个函数用来方便取得节点 x 的成员
64 static link_type& left(link_type x) {
return (link_type&)(x->left); }
65 static link_type& right(link_type x) {
return (link_type&)(x->right); }
66 static link_type& parent(link_type x) {
return (link_type&)(x->parent); }
67 static reference value(link_type x) {
return x->value_field; }
68 static const Key& key(link_type x) {
return KeyOfValue()(value(x)); }
69 static color_type& color(link_type x) {
return (color_type&)(x->color); }
70
71 //求取极大值和极小值。node class 有实现该功能, 交付给它们完成就行
72 static link_type minimum(link_type x) {
73 return rb_tree_node::minimum(x);
74 }
75 static link_type maximum(link_type x) {
76 return (link_type)rb_tree_node::maximum(x);
77 }
78 private:
79 link_type insert(base_ptr x, base_ptr y, const value_type& v);
80 link_type copy(link_type x, link_type p);
81 void erase(link_type x);
82 void init() {
83 // 产生一个节点空间, 令 header 指向它
84 // 令header 为红色,用于区分其与 root 的区别,
85 header = get_node();
86 color(header) = rb_tree_red;
87
88 root() = 0;
89 leftmost() = header;
90 rightmost() = header;
91 }
92
93 public:
94 RBTree(const Compare& comp = Compare()) :node_count(0), key_compare(comp) {
init(); }
95 ~RBTree() {
// clear();
96 put_node(header); }
97 RBTree<Key, Value, KeyOfValue, Compare, Alloc >&
98 operator=(const RBTree<Key,Value,KeyOfValue,Compare, Alloc>& x);
99
100 public:
101 Compare key_comp() const {
return key_compare; }
102 link_type begin() {
return leftmost(); } // RB 树的起点是最左节点处
103 link_type end() {
return header; } // RB 树的终点是 Header所指处
104 bool empty() const {
return node_count = 0; }
105 size_type size() const {
return node_count; }
106 size_type max_size() const {
return size_type(-1); }
107 public:
108 // 将 x 插入到 RB-tree 中(保持节点值独一无二)
109 std::pair<link_type, bool> insert_unique(const value_type &x);
110 //将 x 插入到RB-tree 中(允许节点值重复)
111 link_type insert_equal(const value_type &x);
112
113
RB-tree 值得一提的实现技巧
这里对上面的 header 节点说明一下,树状结构的各种操作,最需注意的就是边界情况的发生,也就是走到根节点是要有特殊的处理,这里STL 特别为根节点再设计了一个父节点 header,而当插入节点后,header 的父节点指向根节点,其左子节点指向树的最小节点,右子结点指向树的最大节点。类似于链表的表头,这里也多了个头节点不实际存储数据,为管理而存在
开始时header的父亲为 NULL, 左子节点与右子节点都指向自身
当插入根节点时:
- header 作为 end(), 红黑树的末端,对迭代器的遍历帮助十分大,当迭代器从最左侧遍历到结尾时直接设置其指向 header, 。。。等等情况,都可以。
5.27 RB-tree 的元素操作
我们主要谈元素(节点) 的插入和搜寻, RB-tree 只提供两种插入操作,
- insert_unique() : 前者表示被插入节点的键值在整棵树独一无二
- insert_equal() : 被插入节点的键值在整棵树中可以重复
第五部分: STL set/map
set 的特性
- 所有元素都会根据元素的键值自动被排序
- set 不允许两个元素有相同的键值
- set ::iterator 被定义为底层 RB-tree 的const_iterator, 杜绝进行对其指向的元素进行写入操
- set 中对元素进行插入或删除操作时, 操作之前的其余迭代器在操作完成后都依然有效。
- 以RB-tree 为底层机制, 几乎所有的 set 操作行为, 都只是转调用 RB-tree 的操作行为而已。
操作:
- 构造函数:
1. 传入比较函数
2. 传入 迭代器范围
3. 传入迭代器范围和比较函数
4. 拷贝构造函数
- 只读操作:
1. 获取键值比较函数
2. 获取值比较函数
3. 获取迭代器
4. 判空
5. 获取大小
6. 交换 set(交换底层红黑树)
- 插入删除清空
1. 根据键值插入
2. 指定插入位置并指定其值
3. 插入迭代器范围内所有元素
4. 删除迭代器指定的元素
5. 删除指定键的元素
6. 清空
- 查找操作
1. 查找键值指定的迭代器
2. 获取该键值存在的个数
3. 查找第一个不小于指定键值的迭代器
4. 查找第一个大于指定键值的迭代器
5. 查找指定键值在set中的范围(返回一对迭代器)
- 重载运算符
==
<
源码摘录
1 #include <functional>
2 #include <memory>
3 #include <set>
4 #include "RBTree.h"
5 template <class Key,
6 class Compare = std::less<Key>,
7 class Alloc = std::allocator<Key>>
8 class set {
9 public:
10 //typedefs:
11 typedef Key key_type;
12 typedef Key value_type;
13 //注意,以下 key_compare 和 value_compare 使用同一个比较函数
14 typedef Compare key_compre;
15 typedef Compare value_compare;
16 private:
17 //注意,以下的 identity 定义
18 /* template <class T> */
19 /* struct identity: public std::unary_function<T,T> { */
20 /* const T& operator()(const T &x) const { return x; } */
21 /* }; */
22 typedef RBTree<key_type, value_type, std::_Identity<value_type>, key_compre, Alloc> rep_type;
23 rep_type t;
24 public:
25 typedef typename rep_type::const_pointer pointer;
26 typedef typename rep_type::const_pointer const_pointer;
27 typedef typename rep_type::const_reference reference;
28 typedef typename rep_type::const_pointer const_reference;
29 typedef typename rep_type::const_iterator iterator;
30
31 typedef typename rep_type::const_iterator const_iterator;
32 typedef typename rep_type::const_reverse_iterator reverse_iterator;
33 typedef typename rep_type::const_reverse_iterator const_reverse_iterator;
34
35 typedef typename rep_type::size_type size_type;
36 typedef typename rep_type::difference_type difference_type;
37
38 // set 使用的一定是 rb_tree 的 insert_unique() 而非 insert_equal()
39 // multiset 才使用RB-tree 的 insert_equal()
40 // 因为 set 不允许有相同的键值出现, multiset才允许相同键值存在
41 set() : t(Compare()) {
}
42 explicit set(const Compare& comp):t(comp) {
}
43
44 template <class InputIterator>
45 set(InputIterator first, InputIterator last) : t(Compare()) {
t.insert_unique(first, last); }
46
47 template <class InputIterator>
48 set(InputIterator first, InputIterator last, const Compare& comp) :t(comp) {
t.insert_unique(first, last); }
49
50 set(const set<Key, Compare, Alloc>& x): t(x.t) {
}
51 set<Key, Compare, Alloc>& operator=(const set<Key,Compare,Alloc>& x) {
52 t = x.t;
53 return *this;
54 }
55
56 //以下所有的 set 操作行为,RB-tree 都已经提供, 所以set 只要传递调用即可
57 key_compre key_comp() const {
return t.key_compare; }
58 value_compare value_comp() const {
return t.key_compare; }
59 iterator begin() const {
return t.begin();}
60 iterator end() const {
return t.end(); }
61 reverse_iterator rbegin() const {
return t.rbegin(); }
62 reverse_iterator rend() const {
return t.rend(); }
63 bool empty() const {
return t.empty(); }
64 size_type size() const {
return t.size(); }
65 size_type max_size() const {
return t.max_size(); }
66 void swap(set<Key, Compare, Alloc> &x) {
t.swap(x.t); }
67 //insert/erase
68 typedef std::pair<iterator, bool> pair_iterator_bool;
69
70 std::pair<iterator, bool> insert(const value_type& x) {
71 std::pair<typename rep_type::iterator, bool> p = t.insert_unique(x);
72 return std::pair<iterator, bool>(p.first, p.second);
73 }
74
75 iterator insert(iterator position, const value_type &x) {
76 typedef typename rep_type::iterator rep_iterator;
77 return t.insert_unique((rep_iterator&)position, x);
78 }
79
80 template <class InputIterator>
81 void insert(InputIterator first, InputIterator last) {
82 t.insert_unique(first, last);
83 }
84 void erase(iterator position) {
85 typedef typename rep_type::iterator rep_iterator;
86 t.erase((rep_iterator&)position);
87 }
88 size_type erase(const key_type &x) {
89 return t.erase(x);
90 }
91 void erase(iterator first, iterator last) {
92 typedef typename rep_type::iterator rep_iterator;
93 t.erase((rep_iterator&)first, (rep_iterator&)last);
94 }
95
96 void clear() {
t.clear(); }
97
98 iterator find(const key_type &x) const {
return t.find(x); }
99 size_type count(const key_type &x) const {
return t.count(x); }
100 iterator lower_bound(const key_type &x) const {
return t.lower_bound(x); }
101 iterator upper_bound(const key_type &x) const {
return t.upper_bound(x); }
102 std::pair<iterator, iterator> equal_range(const key_type &x) const {
return t.equal_range(x); }
103
104 friend bool operator==(const set&, const set&);
105 friend bool operator<(const set&, const set&);
106
107 };
~
第六部分: STL multimap / multiset
multimap 和 multiset 特性与其对应的 map 和 set 相同, 唯一的差别在于它允许键值重复, 它的插入操作采用的是底层机制 RB-tree 的insert_equal() 而
不是 insert_unique()