初识C++ · 基于红黑树封装map + set

_lazy. 2024-10-14 09:35:01 阅读 77

目录

前言:

1 正确认识关系层

2 节点类

3 迭代器类

4 红黑树本体

5 部分函数补充

5.1 析构函数

5.2 拷贝构造函数

5.3 构造函数

5.4 map的operator[]重载

6 部分细节梳理


前言:

这部分是挺有难度的,因为套了好几层关系,涉及到关系层大概有4层左右,但是呢,多花点时间即可,更重要的还是细心部分,其次就是逐个的去捋清楚每层的关系即可,细心 + 耐心,这里就通关了。


1 正确认识关系层

本章是基于红黑树封装的map + set,和以往不同的是,我们之前实现链表部分,可以创建了一个迭代器类然后直接进行使用,但是这里不可以,这里的大体的关系是,节点类 ->  迭代器类 -> 红黑树本体 -> map + set

也就是说,我们想要实现封装,就应该从节点类分析。因为这里的关系较为复杂,所以就关系图是比较难画的,我们就从源码入手,分别看stl_map stl_set stl_tree的源码:

<code>template <class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>

class rb_tree

{

protected:

typedef __rb_tree_node<Value> rb_tree_node;

public:

typedef Key key_type;

typedef Value value_type;

typedef rb_tree_node* link_type;

};

template <class Value>

struct __rb_tree_node : public __rb_tree_node_base

{

typedef __rb_tree_node<Value>* link_type;

Value value_field;

};

这是源码里面红黑树的部分代码,我们从节点开始看,节点的模板参数只有一个,数据类型即是该模板参数,link_type就是节点指针。

那么我们思考,为什么数据类型只有一个参数?map + set来说,不应该是key或者是key-value模型吗?我们上文介绍的红黑树就很单纯了,单纯到只能写一种模型,因为我们写死了数据类型只能是pair。

这里其实也是对模板的一种真正进阶,我们以往学习的模板是说,一个模型能存多种数据类型,这是泛型编程的一种思想,但是在这里,我们虽然使用了模板,但是解决不了数据类型不同的情况,在这里源码就提供了解决方案,在此之前我们看清了红黑树的模板参数有5个,我们真正实现的时候,最后两个是用不到的,一个是仿函数,一个是空间配置器,所以只有前面三个。

template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>

class map

{

public:

typedef Key key_type;

typedef T data_type;

typedef pair<const Key, T> value_type;

private:

typedef rb_tree<key_type, value_type,

select1st<value_type>, key_compare, Alloc> rep_type;

rep_type t;

};

template <class Key, class Compare = less<Key>, class Alloc = alloc>

class set

{

public:

typedef Key key_type;

typedef Key value_type;

private:

typedef rb_tree<key_type, value_type,identity<value_type>, key_compare, Alloc> rep_type;

rep_type t;

};

这是stl_map stl_set的源码,成员变量不出意外的只有一个,那么这个成员变量的类型是什么?

是typedef之后的一个类型,typedef之前是什么?是红黑树本体,此刻展示了部分封装,即map + set是基于红黑树实现的结构,所以在set + map 使用函数的时候,就是使用的红黑树的函数,那么我们想要搞清楚红黑树节点数据类型的原因,就应该看这段typedef,对于set来说,key被typedef两次了,一次是key_type,一次是value_type,传参的时候,相当于传了两个key进去。

这是为什么?

再看map那一层,传的参数分别是key,键值对,看起来是没有什么问题的,但是对于set来说为什么要传两个一样的?

这里我们返回去看tree的这段代码:

typedef __rb_tree_node<Value> rb_tree_node;

可以看到树的数据类型由模板参数value确定,value是模板的第二个参数,而map + set层传给RBTree层的时候,第二个参数分别是键值对,key,此时,树节点的类型就确定了!

这里利用的是多层关系的调用 + 模板,使得map + set使用同一种结构的时候,编译器可以实例化两种不同的模型。

简单捋一下就是:

map + set这一层传两个数据参数过去,第二个参数用来确定树的节点类型,但不是直接map +set这一层确定,而是通过树本体这一层,确定树的节点类型,树节点的模板参数的来源是树本体,树本体的多个模板参数都是来源于map + set这一层。

于此,除了迭代器,其他部分的关系已经梳理完毕!

那么,迭代器和其他层次的关系应该是什么样的?

迭代器是控制节点行为的类,即迭代器需要复用节点类,所以节点类是最底层的类,往上一点就是迭代器,再迭代器之上就是树本体的调用,迭代器的参数和链表部分几乎就是一样的,三个参数,一个数据类型,一个数据类型的引用,一个数据类型的指针,其次就没有什么特别要注意的了,除了他的函数。

此时,所有的关系就捋清了!


2 节点类

由关系层的分析可以知道,节点类的模板参数只有一个,该模板参数用来确定数据类型,成员变量就是固定的左指针,右指针,父节点指针,数据类型变量,以及颜色,知道这些,我们就可以开始写代码了:

enum Colour

{

BLACK,RED

};

template<class Value>

struct RBTreeNode

{

RBTreeNode<Value>* _left;

RBTreeNode<Value>* _right;

RBTreeNode<Value>* _parent;

Colour _col;

Value _data;

RBTreeNode(const Value& data)

:_left(nullptr)

, _right(nullptr)

, _parent(nullptr)

, _data(data)

, _col(RED)

{}

};

那么,节点类的代码我们就写完了,不用管了。


3 迭代器类

迭代器类是基于节点类实现的一个类,成员变量只有一个节点指针,成员函数用来实现每个节点的行为,分别有 ”++ -- * -> !=“,一共有5个函数,其中,需要单独介绍的只有++ -- 函数,其他的在list部分已经介绍过了,这里就跳过了,++ 和 -- 是完全相反的是,所以介绍一个即可:

对于++来说,树是按照中序遍历的,所以遍历顺序应该是左子树 根 右子树。

当节点在17的时候,下一个应该遍历的是22,即右子树的最左节点,这是一种情况,因为它的右子树不为空,即右子树还可以遍历,这里应该结合中序遍历来思考。

当节点在15的时候,右子树为空,说明这个节点应该是某个父节点的最左节点了,那么也就代表,该父节点左子树已经遍历完了,应该轮到遍历根节点了,这个时候,我们就应该找祖先的左节点是父节点的那个祖先,这里其实是包含了两种情况,比如节点为15,我们可以把15直接看成它既是父节点也是子节点,那么祖先就是17,对于一般情况,比如6的下下面还有类似的节点,我们下一个访问就是8,因为该节点访问结束,即8的左子树已经访问完了,我们就应该访问8,所以需要找到8这个祖先,这就是++的大体逻辑,--同理,代码如下:

<code>Self& operator++()

{

//右子树不为空 直接走最左边的

if (_node->_right)

{

Node* leftMin = _node->_right;

while (leftMin->_left)

{

leftMin = leftMin->_left;

}

_node = leftMin;

}

else

{

//右边不为空

//找到左子树是父节点的祖先

///两种情况结合

Node* parent = _node->_parent;

//根节点的情况 —> 最后节点++ 则会到_root的parent节点 -> 空节点

while (parent && parent->_right == _node)

{

_node = parent;

parent = parent->_parent;

}

_node = parent;

}

return *this;

}

Self& operator--()

{

//左子树不为空

if (_node->_left)

{

Node* rightMax = _node->_left;

while (rightMax->_right)

{

rightMax = rightMax->_right;

}

_node = rightMax;

}

else

{

Node* parent = _node->_parent;

while (parent && parent->_left == _node)

{

_node = parent;

parent = parent->_parent;

}

_node = parent;

}

return *this;

}

迭代器类里面最复杂的结果已经实现了,所以这里就可以直接给剩下代码了:

template<class T,class Ref, class Ptr>

struct RBTreeIterator

{

typedef RBTreeNode<T> Node;

typedef RBTreeIterator<T, Ref, Ptr> Self;

Node* _node;

RBTreeIterator(Node* node)

:_node(node)

{}

Ref operator*()

{

return _node->_data;

}

Ptr operator->()

{

return &_node->_data;

}

bool operator!=(const Self& s)

{

return _node != s._node;

}

};


4 红黑树本体

由关系层的分析可以知道,红黑树本体的模板参数有3个:

template<class T, class V, class Kot>

我们从插入函数入手,就可以知道Kot这个类的作用了,都知道,在map + set的封装里面,插入函数返回的是pair<iterator,bool>,这也是为什么我们优先实现迭代器的理由,那么问题来了,插入需要比较吧?我们能直接比较pair类型吗?

在库里面是支持比较的,但是比较方式不是我们希望的,我们希望只按照key去比较,所以现在的问题是,如果要比较,我们应该怎么去实现这个比较

重载函数吗?重载的定义是函数名相同,参数顺序不同,参数类型不同,参数数目不同,可是我们要比较的都是第二个模板参数,也就代表了类型是一样的,那顺序,数目什么的,根本就用不上了。

所以重载是不可以的,这里我们就用到仿函数了,即我们虽然不能直接实现比较,我们可以返回我们想要的比较的值,日期类的仿函数是重载(),返回的就是一个比较结果,从而实现更好的比较。

我们这里的问题只是,数据类型不同,那么我们实现一个仿函数,返回我们想要的数据类型即可,同样也重载()即可。

那么仿函数这个类我们应该写在哪里呢?毫无疑问肯定是写在map + set这一层,因为成员变量的确定就是要传仿函数过去,所以就有:

template <class T>

class set

{

public:

struct SetKeyOfT

{

const T& operator()(const T& key)

{

return key;

}

};

//typedef typename RBTreeIterator<T, const T&, T*> iterator;

//typedef typename RBTreeIterator<T, const T&, const T*> const_iterator;

typedef typename RBTree<T, const T, SetKeyOfT>::iterator iterator;

typedef typename RBTree<T, const T, SetKeyOfT>::Const_iterator const_iterator;

pair<iterator,bool> insert(const T& data)

{

return _t.Insert(data);

}

const_iterator begin() const

{

return _t.Begin();

}

const_iterator end() const

{

return _t.End();

}

iterator begin()

{

return _t.Begin();

}

iterator end()

{

return _t.End();

}

private:

RBTree<T,const T, SetKeyOfT> _t;

};

template <class T,class V>

class map

{

public:

struct MapKeyOfT

{

const T& operator()(const pair<T, V>& kv)

{

return kv.first;

}

};

//typedef typename RBTreeIterator<V, pair<T,V>&, pair<T,V>*> iterator;

//typedef typename RBTreeIterator<V, const pair<T,V>&, const pair<T,V>*> const_iterator;

typedef typename RBTree<T, pair<const T, V>, MapKeyOfT>::iterator iterator;

typedef typename RBTree<T, pair<const T, V>, MapKeyOfT>::Const_iterator const_iterator;

pair<iterator, bool> insert(const pair<T,V>& data)

{

return _t.Insert(data);

}

const_iterator begin() const

{

return _t.Begin();

}

const_iterator end() const

{

return _t.End();

}

iterator begin()

{

return _t.Begin();

}

iterator end()

{

return _t.End();

}

private:

RBTree<T, pair<const T,V>, MapKeyOfT> _t;

};

此时比较的准备工作才算是完全做好了,剩下要做的就是在比较里面讲比较的部分改动一下即可:

pair<iterator,bool> Insert(const V& data)

{

if (_root == nullptr)

{

_root = new Node(data);

//根节点必须为黑色

_root->_col = BLACK;

return make_pair(_root, true);

}

Node* root = _root;

Node* parent = nullptr;

Kot kot;

//判断部分

while (root)

{

if (kot(data) > kot(root->_data))

{

parent = root;

root = root->_right;

}

else if (kot(data) < kot(root->_data))

{

parent = root;

root = root->_left;

}

else

{

return make_pair(root, false);

}

}

Node* cur = new Node(data);

Node* newnode = cur;

//连接部分 开始判断大小关系

if (kot(parent->_data) > kot(data))

{

parent->_left = cur;

}

else

{

parent->_right = cur;

}

cur->_parent = parent;

//颜色更替

//parent的颜色是黑色就结束

while (parent && parent->_col == RED)

{

Node* grandfather = parent->_parent;

//父节点在左边

if (parent == grandfather->_left)

{

Node* uncle = grandfather->_right;

//uncle存在且为红色

if (uncle && uncle->_col == RED)

{

parent->_col = uncle->_col = BLACK;

grandfather->_col = RED;

//一层一层的遍历 father层遍历完了所以直接到grandparent层

cur = grandfather;

parent = grandfather->_parent;

}

//uncle不存在或者是为黑色

else

{

//旋转 -> 通过判断 cur的位置来判断是单旋还是双旋

if (cur == parent->_left)

{

RotateR(grandfather);

//即原位置变为黑色

parent->_col = BLACK;

//如果不变红,导致黑色节点多出来一个

grandfather->_col = RED;

}

//双旋

else

{

RotateL(parent);

RotateR(grandfather);

cur->_col = BLACK;

grandfather->_col = RED;

}

break;

}

}

//父节点在右边

else

{

//存在 且 为红

Node* uncle = grandfather->_left;

if (uncle && uncle->_col == RED)

{

parent->_col = uncle->_col = BLACK;

grandfather->_col = RED;

//一层一层的遍历 father层遍历完了所以直接到grandparent层

cur = grandfather;

parent = grandfather->_parent;

}

//不存在 或 为黑色

else

{

//旋转 -> 通过判断 cur的位置来判断是单旋还是双旋

if (cur == parent->_right)

{

RotateL(grandfather);

//即原位置变为黑色

parent->_col = BLACK;

//如果不变红,导致黑色节点多出来一个

grandfather->_col = RED;

}

//双旋

else

{

RotateR(parent);

RotateL(grandfather);

cur->_col = BLACK;

grandfather->_col = RED;

}

break;

}

}

}

_root->_col = BLACK;

return make_pair(newnode, true);

}

于此,插入函数结束,那么整体部分我们已经梳理的差不多了,接下来就是部分函数的补充以及部分细节的梳理了。


5 部分函数补充

除了插入函数之外,还有的函数是:析构函数 + 拷贝构造函数 + 构造函数 + map的operator[]重载

5.1 析构函数

析构函数,即是让每个节点销毁即可,这里我们使用递归析构,一棵树析构,左右子树都析构了,然后再析构根节点,因为是递归,所以模式和前面的中序遍历是一样的:

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;

}

使用的是后序遍历。

5.2 拷贝构造函数

拷贝构造函数也简单,就是拷贝每个节点的值,但是注意这里是没有包括颜色的,拷贝了之后还要注意连接的问题,只要不是空节点,那么就要进行连接:

public:

RBTree(const RBTree<T, V, Kot>& t)

{

_root = Copy(t._root);

}

private:

Node* Copy(Node* root)

{

if (root == nullptr)

return nullptr;

Node* newroot = new Node(root->_data);

newroot->_col = root->_col;

newroot->_left = Copy(root->_left);

if (newroot->_left)

newroot->_left->_parent = newroot;

newroot->_right = Copy(root->_right);

if (newroot->_right)

newroot->_right->_parent = newroot;

return newroot;

}

但是呢,这里只写拷贝构造函数不写构造函数就会出问题了:

因为实现拷贝构造函数之前编译器一直都调用的是默认构造函数,拷贝构造函数也是构造函数,所以就没有调用默认构造函数,这里我们就需要强行调用默认构造函数。

5.3 构造函数

<code>//构造函数

RBTree() = default;

没错,这就是强行调用,,

5.4 map的operator[]重载

重载也简单,注意返回值是iterator变量里面的first元素的second值就可以;

V& operator[](const T& key)

{

pair<iterator, bool> ret = _t.Insert(make_pair(key, V()));

return ret.first->second;

}


6 部分细节梳理

节点的创建 数据类型由谁确定?

数据类型在set + map这一层,由传入的第二个参数确定-> 键值对还是key模型 所以节点的模板参数只有一个

构造函数为什么加default?

因为创建了拷贝构造,创建拷贝构造之前,编译器一直默认调用的是默认构造,有了拷贝构造之后,就不会调用默认构造,所以需要强制调用默认构造

模板传参的时候应该注意什么?

类型 迭代器里面数据类型的确定等大多数都是传的第二个参数,第一个参数用到的地方只有set + map那一层的成员变量需要传第一个变量,find数据的时候,用T,其他部分基本上都是用的V或者是T+ V的组合

如何保证key模型的key 和 key - value模型的key不被修改?

在set这一层,传参就是传的const key,在map这一层,传的就是 pair<const key,value>,迭代器部分同理

为什么迭代器要写下面的迭代器而不是直接typedef迭代器类为迭代器呢

迭代器类只是一个类而已,基于红黑树封装的map + set来说,我们使用的迭代器应该是基于红黑树实现的, 如果莫名使用一个类来当作迭代器,就少了红黑树这层关系,即实例化都实例化不了

为什么两个迭代器的样子都是一样的?

      因为在RBTree这一层,真正封装迭代器的其实是RBTree,RB那一层的里面已经有了cons tmap + set这一层就没有必要在加const了,调用的时候直接调用即可。


感谢阅读!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。