C++ 基于红黑树RBTree的关联容器Map类
这道题尽管只有一句话,我却花了整整5天的时间,查阅了大量资料,来实现这个Map。毫无疑问,这道题的难度要远远超出了上一节的基于Vector向量类实现的Map,也因此,这个基于自平衡二叉树数据结构的Map,运行效率也要高效许多。注:这里,我并没有找到关于“斜树”的任何资料。我仔细查阅了一下维基百科上的平衡二叉树条目,这里的“斜树”,我认为应该是指AVL树。当然,AVL树也是一种非常流行的平衡二叉
这道题尽管只有一句话,我却花了整整5天的时间,查阅了大量资料,来实现这个Map。毫无疑问,这道题的难度要远远超出了上一节的基于Vector向量类实现的Map,也因此,这个基于自平衡二叉树数据结构的Map,运行效率也要高效许多。
注:这里,我并没有找到关于“斜树”的任何资料。我仔细查阅了一下维基百科上的平衡二叉树条目,这里的“斜树”,我认为应该是指AVL树。当然,AVL树也是一种非常流行的平衡二叉树结构,但我这里实现用的是红黑树(据说自从红黑树发明后,就不再使用AVL树了。因为红黑树效率更高。当然,这是有争议的)。
上一篇中的Map类非常低效,尤其在下标插入元素时,需要遍历Vector来查找元素是否已存在:
template<class K,class V> V& Map<K,V>::operator[](const K& k)
{
for(iterator p=vec.begin();p!=vec.end();++p)
if(p->first==k) return p->second;
vec.push_back(Pair<const K,V>(k,V()));
return vec.back().second;
}
这是因为,这个基于Vector的Map类的数据结构,在本质上是一个线性表。也就是说,Vector中的元素是线性排列的,是没有规律的。因此,在查找一个元素时,就必须一个一个地遍历这个线性表,效率就会非常低。正是因为这个原因,需要弃用Vector而改用二叉树这样的数据结构。因为二叉树是根据键的大小来进行左右分支的,因此查找元素时只要比较键的大小,寻找子分支即可。这样一来,查找的效率就会大大提高。与此同时,插入、删除元素的效率也因此而提高了。
那为什么要用红黑树或者斜树呢?
这是因为,二叉树,在最坏情况下,也会退化成为一个线性链表。如果说不断插入降序或升序的一串元素,例如1、2、3、4、5,这样,二叉树就只能有一个分支,也就是只能有一个右分支,而那个右分支仍然只能有一个右分支。这样,二叉树就退化成了一个线性链表,插入效率也会非常差。
自平衡二叉树是指,在插入元素时,二叉树会自动调整元素的位置,使树的两叉维持平衡。而红黑树和斜树,是这目前相对流行的自平衡二叉树实现。这里,我采用的就是红黑树这种数据结构。红黑树最普遍的用途就是用来实现关联数组。
红黑树遵守5个基本命题:
1. 节点是红色或黑色。
2. 根是黑色。
3. 所有叶子都是黑色(叶子是NIL节点)。
4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
注意到命题5,“从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点”,这一点就保证了,红黑树的树高由于每个分支上黑色结点数目一定而维持平衡。
在具体实现中,我把这个红黑树实现为一个MapNode子节点构成的链表。MapNode包含3个指针成员,left、right和parent。以及一个颜色成员,color(实现为枚举类型COLOR)。
这里要注意的是,在插入操作的实现中,基本的插入操作是与二叉树一样的。然而,由于在插入新元素后可能导致红黑树的性质丢失,树失去平衡,因此在普通的二叉树插入操作之后需要调用insert_fix_up()函数来修复红黑树的性质,维持树的左右平衡。还有,树的旋转只能修复元素的位置和树的平衡,红黑树本身的红黑性质是无法修复的。因此在旋转后还要调整结点的颜色。
红黑树的具体原理和详细的阅读资料在附录中有。
//RBTree_Map.h
#include <iostream>
template<class K,class V> class Map;
template<class K,class V> class MapNode{
friend class Map<K,V>;
enum COLOR{RED,BLACK};
MapNode(COLOR c=RED,MapNode* p=0,MapNode* l=0,MapNode* r=0,
const K k=K(),V v=V()):
color(c),parent(p),left(l),right(r),key(k),val(v){}
COLOR color;
MapNode* parent;
MapNode *left,*right;
const K key;
V val;
};
template<class K,class V> class Map{
public:
Map():root(0){}
V& operator[](const K&);
void print() const{print(root);}
private:
MapNode<K,V>* root;
MapNode<K,V>* grandparent(MapNode<K,V>* p)
{return p->parent->parent;}
MapNode<K,V>* uncle(MapNode<K,V>* p)
{return p->parent==grandparent(p)->left?
grandparent(p)->right:grandparent(p)->left;}
void rotate_left(MapNode<K,V>*);
void rotate_right(MapNode<K,V>*);
void insert_fix_up(MapNode<K,V>*);
void print(MapNode<K,V>* p) const
{
if(!p) return;
print(p->left);
std::cout<<p->key<<"\t"
<<p->val<<std::endl;
print(p->right);
}
};
template<class K,class V>
V& Map<K,V>::operator[](const K& key)
{
MapNode<K,V>* p=root;
MapNode<K,V>* pp=0;
while(p){
pp=p;
if(key<p->key)
p=p->left;
else if(key>p->key)
p=p->right;
else
return p->val;
}
p=new MapNode<K,V>(MapNode<K,V>::RED,pp,0,0,key,V());
if(!pp)
root=p;
else if(key<pp->key)
pp->left=p;
else
pp->right=p;
insert_fix_up(p);
return p->val;
}
template<class K,class V>
inline void Map<K,V>::rotate_left(MapNode<K,V>* p)
{
MapNode<K,V>* pr=p->right;
p->right=p->right->left;
if(pr->left) pr->parent=p;
pr->parent=p->parent;
if(!p->parent)
root=pr;
else if(p==p->parent->left)
p->parent->left=pr;
else
p->parent->right=pr;
pr->left=p;
p->parent=pr;
}
template<class K,class V>
inline void Map<K,V>::rotate_right(MapNode<K,V>* p)
{
MapNode<K,V>* pl=p->left;
p->left=p->left->right;
if(pl->right) pl->right->parent=p;
pl->parent=p->parent;
if(!p->parent)
root=pl;
else if(p==p->parent->right)
p->parent->right=pl;
else
p->parent->left=pl;
pl->right=p;
p->parent=pl;
}
template<class K,class V>
inline void Map<K,V>::insert_fix_up(MapNode<K,V>* p)
{
if(!p->parent)
p->color=MapNode<K,V>::BLACK;
else if(p->parent->color==MapNode<K,V>::BLACK)
return;
else if(uncle(p)!=0&&uncle(p)->color==MapNode<K,V>::RED){
p->parent->color=MapNode<K,V>::BLACK;
uncle(p)->color=MapNode<K,V>::BLACK;
grandparent(p)->color=MapNode<K,V>::RED;
insert_fix_up(grandparent(p));
}
else if(p==p->parent->right&&p->parent==grandparent(p)->left){
rotate_left(p->parent);
p=p->left;
}else if(p==p->parent->left&&p->parent==grandparent(p)->right){
rotate_right(p->parent);
p=p->right;
}
else{
p->parent->color=MapNode<K,V>::BLACK;
grandparent(p)->color=MapNode<K,V>::RED;
if(p==p->parent->left&&p->parent==grandparent(p)->left)
rotate_right(grandparent(p));
else
rotate_left(grandparent(p));
}
}
这里,由于红黑树本身相当复杂,我只实现了红黑树Map的插入操作(也就是下标操作符[])。然而,还是花了整整5天时间。删除操作要比插入操作更难。我并不想实现删除操作,一方面是因为我认为我已经理解了红黑树的原理,实现了树的左旋和右旋,并且正确地实现了插入操作。另一方面是我认为红黑树的插入操作并没有太多关于C++程序设计技术的展示(已经有点超出《C++程序设计语言》这本书的范畴了)。过多研究数据结构和算法的做法并不可取。相反,标准库的Map类已经提供了一个非常高效的实现。因此花大量时间在这样一道模板的习题上,有些不明智。
主程序就是简单地测试Map的插入操作:
#include "RBTree_Map.h"
int main()
{
Map<int,std::string> m;
m[1]="one";
m[2]="two";
m[0]="zero";
m[3]="three";
m[4]="four";
m[5]="five";
m[6]="six";
m[10]="ten";
m.print();
return 0;
}
附录
阅读资料
维基百科:《红黑树》 http://zh.wikipedia.org/wiki/%e7%ba%a2%e9%bb%91%e6%a0%91
《教你透彻了解红黑树》 ——July http://blog.csdn.net/v_july_v/article/details/6105630
《红黑树的C++完整源码》 ——July http://blog.csdn.net/v_july_v/article/details/6105630
拓展阅读
Knuth,《计算机程序设计艺术》,Vol.1
更多推荐
所有评论(0)