C++ | 基于红黑树实现mymap与myset

🦌云深麋鹿
专栏 :C++ | 用C语言学数据结构 | Java

回顾:上一篇我们结束了 红黑树 的相关介绍,接下来这篇文章让我们尝试 封装 红黑树 实现容器mymap跟myset,体会新的设计思路吧~

放个目录

前言

摘要:本文基于红黑树实现mymap与myset容器,通过模板参数设计统一底层结构:以T存储节点数据,K提取键值,利用_KeyOfValue函数适配map的pair与set的key。通过迭代器封装、const迭代器机制及节点key不可修改特性,模拟标准库源码思路,实现高效、安全的键值对与集合管理,深入理解STL容器底层设计逻辑。

一 分析源码

这里可以用Everything搜源码位置打开来看:

  1. 打开Everything
  2. 输入相应文件名称查找


要同时满足两个容器的特点,底层上关键在于树结点存不同数据的同时,函数接口能够取得所需数据。

1.1 可以分别实现map和set的红黑树是怎样的

源码的设计巧思从模板参数开始:

  1. 给个模板参数(即_Val)。
  2. 树结点就是用这个_Val构造出来的。
  3. 另外,Key模板参数是给find/erase函数用的。

1.2 函数怎么适配map的pair跟set的key

关键在于模板参数_KeyOfValue,我们代码实现部分再细讲。

1.3 源码怎么实现iterator

源码比较长,直接拷贝:

cpp 复制代码
template<typename _Tp>
  struct _Rb_tree_iterator
  {
    typedef _Tp  value_type;
    typedef _Tp& reference;
    typedef _Tp* pointer;

    typedef bidirectional_iterator_tag iterator_category;
    typedef ptrdiff_t                  difference_type;

    typedef _Rb_tree_iterator<_Tp>        _Self;
    typedef _Rb_tree_node_base::_Base_ptr _Base_ptr;
    typedef _Rb_tree_node<_Tp>*           _Link_type;

    _Rb_tree_iterator() _GLIBCXX_NOEXCEPT
    : _M_node() { }

    explicit
    _Rb_tree_iterator(_Link_type __x) _GLIBCXX_NOEXCEPT
    : _M_node(__x) { }

    reference
    operator*() const _GLIBCXX_NOEXCEPT
    { return *static_cast<_Link_type>(_M_node)->_M_valptr(); }

    pointer
    operator->() const _GLIBCXX_NOEXCEPT
    { return static_cast<_Link_type> (_M_node)->_M_valptr(); }

    _Self&
    operator++() _GLIBCXX_NOEXCEPT
    {
	_M_node = _Rb_tree_increment(_M_node);
        return *this;
    }

    _Self
    operator++(int) _GLIBCXX_NOEXCEPT
    {
	_Self __tmp = *this;
	_M_node = _Rb_tree_increment(_M_node);
        return __tmp;
    }

    _Self&
    operator--() _GLIBCXX_NOEXCEPT
    {
	_M_node = _Rb_tree_decrement(_M_node);
        return *this;
    }

    _Self
    operator--(int) _GLIBCXX_NOEXCEPT
    {
	_Self __tmp = *this;
	_M_node = _Rb_tree_decrement(_M_node);
        return __tmp;
    }

    bool
    operator==(const _Self& __x) const _GLIBCXX_NOEXCEPT
    { return _M_node == __x._M_node; }

    bool
    operator!=(const _Self& __x) const _GLIBCXX_NOEXCEPT
    { return _M_node != __x._M_node; }

    _Base_ptr _M_node;
};
  • 后面模拟实现部分细讲。

1.4 源码怎么实现const_iterator

普通迭代器:

const迭代器:

  • 可以看到const迭代器直接在typedef的时候加上了const。
  • 我们可以使用模板参数实现类似效果。

1.5 Key

  • map中的结点要求key不能修改,值可以修改;set结点只有值,也不能修改。
  • 源码中,map这里typedef的时候加上了const。
  • set则是普通迭代器也使用了const迭代器,以此限制Key的修改。

二 模拟实现

2.1 搭框架

原来的RBTree头文件需要改下。

2.1.1 RBTreeNode结构

cpp 复制代码
template<class T>
struct RBTreeNode {
    T _t;
    RBTreeNode<T>* _left;
    RBTreeNode<T>* _right;
    RBTreeNode<T>* _parent;
    Colour _col;

    RBTreeNode(const T& t) 
        :_t(t)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED)
    {}
};
  • 只留下T,T具体是什么,由底层(map/set)决定。

2.1.2 RBTree结构

(1)修改框架
cpp 复制代码
template<class K, class T>
class RBTree {
    typedef RBTreeNode<T> Node;
public:
    //...
private:
    Node* _root = nullptr;
};
(2)修改Insert
cpp 复制代码
bool Insert(const T& t) {
    if (_root == nullptr) {
        _root = new Node(t);
        _root->_col = BLACK;
        return true;
    }
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur) {
        if (cur->_kv.first < kv.first) {
        //...
}

修改到一半卡住了。

①_kv.first怎么修改?
  1. 源码给出了解决方案,加上模板参数KeyOfValue。
cpp 复制代码
template<class K, class T,class KeyOfValue>
  1. 再加上MyMap跟MySet对应代码:
  • MyMap
cpp 复制代码
template<typename K>
class MySet { 
public:
    struct SetKeyOfT {
        const K& operator()(const K& k) const {
            return k;
        }
    };
private:
    RedBlackTree<K,K,SetKeyOfT> _mySet;
};
  • MySet
cpp 复制代码
template<typename K>
class MySet { 
public:
    struct SetKeyOfT {
        const K& operator()(const K& k) const {
            return k;
        }
    };
private:
    RedBlackTree<K,K,SetKeyOfT> _mySet;
};
  • 重载操作符()。
  1. 改函数
cpp 复制代码
KeyOfT kot;
while (cur) {
    if (kot(cur->_t) < kot(t)) {
        parent = cur;
        cur = cur->_right;
    }else if (kot(cur->_t) > kot(t)) {
        parent = cur;
        cur = cur->_left;
    }else {
        return false;
    }
}
②测试Insert

分别给MyMap跟MySet写个Print方便输出观察:

cpp 复制代码
template<class K, class V>
void Print(const MyMap<K,V>& m) {
    for (auto& e : m) {
        cout << e.first << " " << e.second;
        cout << endl;
    }
    cout << endl;
}

template<class K>
void Print(const MySet<K>& s) {
    for (auto& e:s) {
        cout << e << " ";
    }
    cout << endl;
}

测试函数:

cpp 复制代码
void TestMyMap01(void) {
    cout << "TestMyMap:" << endl;
    
    MyMap<string, string> m;
    m.insert(make_pair("1", "one"));
    m.insert(make_pair("2", "two"));
    m.insert(make_pair("3", "three"));
    
    Print(m);
}
void TestMySet01(void) {
    cout <<  "TestMySet:" << endl;
    
    MySet<string> s;
    s.insert("one");
    s.insert("two");
    s.insert("three");
    
    Print(s);
}

在main函数里调用运行:

符合预期。

(3)封装iterator
①搭个框架
cpp 复制代码
template<class T>
struct RBTreeIterator {
    typedef RBTreeNode<T> Node;
    Node* _node;
    
    RBTreeIterator(Node* node):_node(node){}
}
  • 提供模板参数方便适配不同node类型的Iterator。
  • 给结点类型起一个简短别名Node。
  • RBTreeIterator初始化,带一个参数node给_node赋值。
②重载需要的操作符(简单)
cpp 复制代码
struct RBTreeIterator {
    // ...
    T& operator*() const{
    	return _node->_t;
    }
    T* operator->() const{
    	return &_node->_t;
    }
    bool operator==(const Self& it) const{
    	return _node == it._node;
    }
    bool operator!=(const Self& it) const {
    	return _node != it._node;
    }
}
  1. 对一个Iterator解引用*,为_node的_t内容,把Node定义找出来瞅一眼:
cpp 复制代码
template<class T>
struct RBTreeNode {
	T _t;
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	Colour _col;
         // ...
 }
  1. 重载成员访问符->,结果返回_node的_t地址。
  2. 重载成员访问符==跟!=,就是比较两个_node。
③重载需要的操作符(稍难)
  • 这里就不区分前后置了。
  1. 重载自增++
  • ++遵循左根右原则。
cpp 复制代码
Self& operator++() {
    if(_node->_right){
        Node* minLeft = _node->_right;
        while(minLeft->_left) {
            minLeft = minLeft->_left;
        }
        _node = minLeft;
    }else {
        Node* cur = _node;
        Node* parent = cur->_parent;
        while (parent && cur == parent->_right) {
            cur = parent;
            parent = cur->_parent;
        }
        _node = parent;
    }
    return *this;
}
  • 为了更好演示,假设当前遍历到了A结点。
  • 第一种情况:右子树不为空,则++到右子树的最左结点。

如图,++后遍历到B结点。

  • 第二种情况:右子树为空,则向上找祖先。

    图中向上找到B,当前结点A是B的左孩子,则++遍历到B结点。
  • 第三种情况:右子树为空,向上找不到祖先。

    图中A结点不属于任何一个祖先的左孩子,则已遍历完整棵树,再++就是nullptr(即End())。
  1. 重载自减- -
  • - -遵循右根左原则
cpp 复制代码
Self& operator--() {
    if(!_node){ 
        Node* maxRight = _root;
        while (maxRight->_right) {
            maxRight = maxRight->_right;
        }
        _node = maxRight;
    }else if(_node->_left) {
        Node* maxRight = _node->_left;
        while (maxRight->_right) {
            maxRight = maxRight->_right;
        }
        _node = maxRight;
    }else {
        Node* cur = _node;
        Node* parent = cur->_parent;
        while (parent && cur == parent->_left) {
            cur = parent;
            parent = cur->_parent;
        }
        _node = parent;
    }
    return *this;
}
  • 同样假设当前遍历到了A结点。
  • 第一种情况:当前结点为空结点。

找到当前子树的最右结点,该结点为当前空结点–后下一个结点。

  • 第二种情况:当前结点不为空,且有左孩子

当前结点的左孩子就是下一个结点。

  • 第三种情况:当前结点不为空,且没有左孩子

向上找祖先,直到自己为祖先的右孩子,该祖先即为下一个结点。

  • 第四种情况:当前结点不为空,且没有左孩子,且遍历不到下一个祖先。

一般不会有这种情况,当前代码下–后变为空结点。

(4)封装const_iterator
①加俩模板参数
cpp 复制代码
template<class T, class Ref, class Ptr>
②在RBTree里
  • 根据模板参数不同,构造Iterator跟ConstIterator。
cpp 复制代码
typedef RBTreeIterator<T, T&, T*> Iterator;
typedef RBTreeIterator<T, const T&,const T*> ConstIterator;
  • 加一个ConstIterator的Begin和End。
cpp 复制代码
ConstIterator Begin() const {
    Node* minLeft = _root;
    while (minLeft && minLeft->_left) {
        minLeft = minLeft->_left;
    }
    return ConstIterator(minLeft, _root);
}

ConstIterator End() const {
    return ConstIterator(nullptr, _root);
}
(5)修改key
①修改Insert返回值

我们写的Insert返回值是bool,但是库里的返回值是一个pair:

我们也改成这样,方便获取插入位置。

修改底层RedBlackTree里的Insert:

cpp 复制代码
pair<Iterator,bool> Insert(const T& t) {
    //...
    // traverse to find the right position for the node to insert:
    while(cur){
        if //...  
        else{
            return make_pair(Iterator(cur,_root), false);    
        }   
    }
    // create a new node and assign it to cur...
    pair<Iterator,bool> ret = make_pair(Iterator(cur,_root), true);
    // change colour...
    return ret;
}

同步修改MyMap跟MySet:

cpp 复制代码
class MySet { 
    //...
    pair<iterator,bool> insert(const K& key) {
        return _mySet.Insert(key);
    }
}
class MyMap { 
    //...
    pair<iterator,bool> insert(const pair<K, V>& kv) {
        return _myMap.Insert(kv);
    }
}

以上代码仅为展示修改部分。

②MySet的Key不能修改

现在MySet的Key是可以修改的。

测试代码如下:

cpp 复制代码
void TestMySet02(void) {
    cout << "TestMySet:" << endl;
    
    MySet<string> s;
    pair<MySet<string>::iterator, bool> ret = s.insert("one");
    MySet<string>::iterator it = ret.first;
    *it = "two";
    
    Print(s);
}

运行:

修改代码。

直接在定义类成员变量以及typedef迭代器时给模板参数加上const:

cpp 复制代码
public:
    typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
    typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;
private:
    RBTree<K,const K,SetKeyOfT> _mySet;

之前的测试代码现在就报错了:

③MyMap的Key不能修改

现在MyMap的Key同样是可以修改的。

测试代码如下:

cpp 复制代码
void TestMyMap02(void) {
    cout << "TestMyMap:" << endl;
    
    MyMap<string, string> m;
    pair<MyMap<string, string>::iterator, bool> ret = m.insert(make_pair("1", "one"));
    MyMap<string, string>::iterator it = ret.first;
    it->first = "2";
    
    Print(m);
}
  • 代码注释:声明一个MyMap容器对象 m,向m中插入键值对1:one,再修改该键值对的键为2。

运行:

修改代码。

同样直接在定义类成员变量以及typedef迭代器时给模板参数加上const:

cpp 复制代码
public:
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;
private:
    RBTree<K, pair<const K,V>,MapKeyOfT> _myMap;

回到之前的测试代码:

  • 实现不能修改键,但是可以修改值。
(6)重载\[\]
  • 借助insert函数直接返回值本身:
cpp 复制代码
V& operator[] (const K& key) {
    pair<iterator, bool> ret = insert(make_pair(key, V()));
    iterator it = ret.first;
    return it->second;
}
  • V()返回的是V类型的临时对象。

简单测试

测试代码如下:

cpp 复制代码
void TestMyMap03(void) {
    MyMap<string, int> m;
    m["one"] = 1;
    m["one"]++;
    
    Print(m);
}

运行:

符合预期。

封装红黑树的学习就到这里,下一篇我们要介绍两个不太一样的容器,很快会更出来~

相关推荐
weixin_417396831 小时前
桌面云终端PXE启动失败怎么排查?从DHCP Option到镜像下发
开发语言·前端·php·技术·酒店·综合布线·弱电
某不知名網友2 小时前
ROS2进阶:服务通信、自定义SRV接口、参数服务器与基于服务的参数通信详解
java·开发语言·microsoft
傲世仙尊2 小时前
string增删查改-insert-erase-replace与find加substr组合技
c++
java_nnnn2 小时前
LeetCode刷题滑动窗口(水果成篮+找到字符串中所有字母的异位词)Java
java·开发语言·leetcode
qetfw2 小时前
Bash 字符串处理:参数展开、序列与脚本执行方式
linux·开发语言·chrome·bash
小羊没烦恼!2 小时前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#
学Linux的语莫3 小时前
python中类的常见写法
开发语言·python·状态模式
需要8263 小时前
容器化 Java:镜像瘦身与 JVM 容器感知(cgroup)
java·开发语言·jvm
dyxal3 小时前
A2A 协议 v1.0 完全指南:方法名、Agent Card、三种绑定与 FastAPI 实战
开发语言·qt·fastapi