C++ 红黑树封装(二):迭代器、const_iterator 与 operator[] 完整实现 mymap 和 myset

C++ 红黑树封装(二):迭代器、const_iterator 与 operator\[\] 完整实现 mymap 和 myset


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 《C++从入门到入土》 《Linux的学习之旅》 ✨ 数据即知识,压缩即智能


前言:为什么有了红黑树,还不能称为 map/set?

1. 容器真正好用的地方在接口

上一篇我们已经完成了:

text 复制代码
RBTree<K,T,KeyOfT>

并利用它封装:

text 复制代码
myset
mymap

此时我们已经能够:

cpp 复制代码
s.insert(10);

m.insert({"apple", 5});

但是如果想像 STL 一样:

cpp 复制代码
for (auto e : s)
{
    cout << e << " ";
}

或者:

cpp 复制代码
for (auto& kv : m)
{
    cout << kv.first
         << ":"
         << kv.second
         << endl;
}

就必须继续实现:

text 复制代码
iterator

同时 map 还需要:

cpp 复制代码
m["apple"]

这样的接口。

所以这一篇主要解决:

text 复制代码
1. 红黑树 iterator 怎么走
2. begin/end 怎么表示
3. ++ 和 -- 怎么实现
4. const_iterator 怎么实现
5. 为什么 set 不能修改元素
6. 为什么 map 只能修改 value
7. insert 为什么返回 pair<iterator,bool>
8. operator[] 为什么依赖 insert

一、红黑树迭代器本质上是什么?

1. 先回忆 list 的 iterator

对于链表,我们可能写过:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct ListIterator
{
    Node* _node;
};

它本质上就是:

用一个对象封装结点指针,然后通过运算符重载让这个对象表现得像指针。

例如:

cpp 复制代码
operator*
operator->
operator++
operator--
operator==
operator!=

红黑树 iterator 的整体思路其实完全一样。


2. RBTreeIterator

可以设计:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
    typedef RBTreeNode<T> Node;

    typedef RBTreeIterator<
        T,
        Ref,
        Ptr
    > Self;

    Node* _node;
    Node* _root;
};

其中:

text 复制代码
_node

表示当前迭代器指向哪个结点。

而:

text 复制代码
_root

主要用于:

cpp 复制代码
--end()

这种特殊情况。


二、为什么红黑树迭代器要按照中序遍历?

红黑树本质上仍然是:

text 复制代码
Binary Search Tree

因此:

text 复制代码
左子树 key
<
当前 key
<
右子树 key

所以执行:

text 复制代码
中序遍历

即:

text 复制代码
左子树
  ↓
根
  ↓
右子树

得到的就是:

text 复制代码
升序序列

例如:

text 复制代码
        18
       /  \
     10    30
       \   / \
       15 25 40
             / \
            35 50

中序遍历:

text 复制代码
10
15
18
25
30
35
40
50

因此:

cpp 复制代码
for (auto e : set)

实际上就是不断寻找:

text 复制代码
中序遍历的下一个结点

三、begin() 应该指向哪里?

既然迭代器按照:

text 复制代码
中序遍历

访问,那么:

cpp 复制代码
begin()

必须指向:

text 复制代码
中序遍历的第一个结点

而一棵 BST 中序遍历的第一个结点就是:

text 复制代码
整棵树最左边的结点

因此:

cpp 复制代码
Iterator Begin()
{
    Node* leftMost = _root;

    while (leftMost
        && leftMost->_left)
    {
        leftMost = leftMost->_left;
    }

    return Iterator(
        leftMost,
        _root
    );
}

例如:

text 复制代码
        18
       /
      10

那么:

cpp 复制代码
begin()

指向:

text 复制代码
10

四、end() 应该怎么表示?

1. 简化实现:nullptr

我们可以规定:

cpp 复制代码
End()

返回:

cpp 复制代码
Iterator(nullptr, _root);

即:

cpp 复制代码
Iterator End()
{
    return Iterator(
        nullptr,
        _root
    );
}

于是:

cpp 复制代码
it == end()

本质就是:

text 复制代码
it._node == nullptr

2. STL 源码中的 header 哨兵结点

经典 SGI-STL 的红黑树实现会额外设计:

text 复制代码
header

哨兵结点。

它通常会维护类似:

text 复制代码
header.parent → root
header.left   → 最左结点
header.right  → 最右结点

这样做以后:

text 复制代码
begin
end
最左
最右

等操作可以更加统一。

我们自己学习实现时,可以暂时使用:

text 复制代码
nullptr 作为 end

结构更简单。

代价就是:

cpp 复制代码
--end()

需要单独特殊处理。


五、iterator++ 到底在找什么?

++it 并不是:

text 复制代码
地址 + 1

而是:

找当前结点在中序遍历中的后继结点。

假设当前:

text 复制代码
it → x

我们要寻找:

text 复制代码
x 的中序后继

只需要分析两种情况。


六、情况一:当前结点有右子树

例如:

text 复制代码
      30
        \
         40
        /
       35

当前:

text 复制代码
it → 30

按照中序:

text 复制代码
30
35
40

所以下一个不是:

text 复制代码
40

而是:

text 复制代码
右子树中最左边的结点

即:

text 复制代码
35

所以:

cpp 复制代码
if (_node->_right)
{
    Node* leftMost =
        _node->_right;

    while (leftMost->_left)
    {
        leftMost =
            leftMost->_left;
    }

    _node = leftMost;
}

可以概括为一句:

有右子树,后继就是右子树的最左结点。


七、情况二:当前结点没有右子树

这个情况稍微难一些。

假设:

text 复制代码
       18
      /
     10
       \
        15

当前:

text 复制代码
it → 15

15 没有右子树。

中序顺序:

text 复制代码
10
15
18

那么:

text 复制代码
15 的下一个

是:

text 复制代码
18

但 18 并不是 15 的父亲。

所以不能简单写:

cpp 复制代码
_node = _node->_parent;

八、向祖先寻找"第一次从左边上来"的位置

从:

text 复制代码
15

向上:

text 复制代码
15 是 10 的右孩子

说明:

text 复制代码
10 这整棵子树已经访问完成

继续往上:

text 复制代码
10 是 18 的左孩子

根据中序:

text 复制代码
左子树
↓
根

说明:

text 复制代码
18

就是下一个结点。

所以算法是:

cpp 复制代码
Node* cur = _node;
Node* parent = cur->_parent;

while (parent
    && cur == parent->_right)
{
    cur = parent;
    parent = cur->_parent;
}

_node = parent;

也就是:

没有右子树时,一直向祖先寻找,直到找到"当前路径是从某个父结点左子树上来的"那个父结点。


九、完整 operator++

因此:

cpp 复制代码
Self& operator++()
{
    if (_node->_right)
    {
        Node* leftMost =
            _node->_right;

        while (leftMost->_left)
        {
            leftMost =
                leftMost->_left;
        }

        _node = leftMost;
    }
    else
    {
        Node* cur = _node;
        Node* parent =
            cur->_parent;

        while (parent
            && cur == parent->_right)
        {
            cur = parent;
            parent =
                cur->_parent;
        }

        _node = parent;
    }

    return *this;
}

整个逻辑只需要记两句话:

text 复制代码
有右:
    找右子树最左

无右:
    向上找第一次从左边上来的祖先

十、为什么最大结点 ++ 会变成 end()?

例如最大结点:

text 复制代码
50

没有右子树。

它可能沿祖先一直:

text 复制代码
50 是 40 的右
40 是 30 的右
30 是 18 的右

最后:

text 复制代码
parent == nullptr

于是:

cpp 复制代码
_node = parent;

也就是:

cpp 复制代码
_node = nullptr;

正好变成:

cpp 复制代码
end()

所以:

cpp 复制代码
++最后一个元素

自然得到:

text 复制代码
end()

十一、operator-- 与 ++ 完全对称

--it 实际上是在寻找:

text 复制代码
中序前驱

因此逻辑和 ++ 几乎镜像。


十二、情况一:当前结点有左子树

例如:

text 复制代码
         30
        /
       20
         \
          25

当前:

text 复制代码
it → 30

中序:

text 复制代码
20
25
30

所以前驱是:

text 复制代码
25

也就是:

text 复制代码
左子树最右边的结点

代码:

cpp 复制代码
Node* rightMost =
    _node->_left;

while (rightMost->_right)
{
    rightMost =
        rightMost->_right;
}

_node = rightMost;

十三、情况二:当前结点没有左子树

此时就要向上寻找:

第一次从父结点右边上来的祖先。

代码:

cpp 复制代码
Node* cur = _node;
Node* parent =
    cur->_parent;

while (parent
    && cur == parent->_left)
{
    cur = parent;
    parent =
        cur->_parent;
}

_node = parent;

所以:

text 复制代码
++:
向上找从左边来的祖先

--:
向上找从右边来的祖先

正好完全对称。


十四、--end() 为什么需要特殊处理?

由于我们的:

cpp 复制代码
end()

是:

text 复制代码
nullptr

那么:

cpp 复制代码
--end()

不能直接访问:

cpp 复制代码
_node->_left

否则:

text 复制代码
空指针解引用

所以需要单独处理。

--end() 在正常迭代语义中应该得到:

text 复制代码
中序遍历最后一个结点

也就是:

text 复制代码
整棵 BST 最右边的结点

因此:

cpp 复制代码
if (_node == nullptr)
{
    Node* rightMost = _root;

    while (rightMost
        && rightMost->_right)
    {
        rightMost =
            rightMost->_right;
    }

    _node = rightMost;
}

这也是为什么我们的迭代器除了 _node 以外,还保存了:

cpp 复制代码
_root

十五、完整 operator--

cpp 复制代码
Self& operator--()
{
    if (_node == nullptr)
    {
        Node* rightMost = _root;

        while (rightMost
            && rightMost->_right)
        {
            rightMost =
                rightMost->_right;
        }

        _node = rightMost;
    }
    else if (_node->_left)
    {
        Node* rightMost =
            _node->_left;

        while (rightMost->_right)
        {
            rightMost =
                rightMost->_right;
        }

        _node = rightMost;
    }
    else
    {
        Node* cur = _node;
        Node* parent =
            cur->_parent;

        while (parent
            && cur == parent->_left)
        {
            cur = parent;
            parent =
                cur->_parent;
        }

        _node = parent;
    }

    return *this;
}

可以压缩记忆为:

text 复制代码
--end()
    → 整棵树最右

有左
    → 左子树最右

无左
    → 向上找第一次从右边上来的祖先

十六、operator* 和 operator->

iterator 必须像指针一样使用。

所以:

cpp 复制代码
Ref operator*()
{
    return _node->_data;
}

以及:

cpp 复制代码
Ptr operator->()
{
    return &_node->_data;
}

1. 为什么 operator-> 返回地址?

例如:

cpp 复制代码
it->second

实际上等价于:

cpp 复制代码
(&(**it))->second

所以:

cpp 复制代码
operator->

应该返回当前元素的:

text 复制代码
指针

十七、iterator 为什么有 Ref 和 Ptr?

这是一个非常经典的泛型技巧。

定义:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct RBTreeIterator;

普通 iterator:

cpp 复制代码
RBTreeIterator<
    T,
    T&,
    T*
>

const_iterator:

cpp 复制代码
RBTreeIterator<
    T,
    const T&,
    const T*
>

这样只用:

text 复制代码
一份代码

就可以实现:

text 复制代码
iterator
const_iterator

十八、RBTree 中定义两种迭代器

cpp 复制代码
typedef RBTreeIterator<
    T,
    T&,
    T*
> Iterator;

typedef RBTreeIterator<
    T,
    const T&,
    const T*
> ConstIterator;

于是:

text 复制代码
Iterator

解引用返回:

cpp 复制代码
T&

而:

text 复制代码
ConstIterator

返回:

cpp 复制代码
const T&

这正是我们以前在:

text 复制代码
list
vector

迭代器中学到的同一种模板复用思想。


十九、Begin 和 End 的 const 版本

普通成员:

cpp 复制代码
Iterator Begin()
{
    Node* leftMost = _root;

    while (leftMost
        && leftMost->_left)
    {
        leftMost =
            leftMost->_left;
    }

    return Iterator(
        leftMost,
        _root
    );
}

const 版本:

cpp 复制代码
ConstIterator Begin() const
{
    Node* leftMost = _root;

    while (leftMost
        && leftMost->_left)
    {
        leftMost =
            leftMost->_left;
    }

    return ConstIterator(
        leftMost,
        _root
    );
}

以及:

cpp 复制代码
Iterator End()
{
    return Iterator(
        nullptr,
        _root
    );
}

ConstIterator End() const
{
    return ConstIterator(
        nullptr,
        _root
    );
}

二十、set 的 iterator 为什么应该是只读的?

对于:

cpp 复制代码
set<int>

元素:

text 复制代码
10
20
30

本身就是:

text 复制代码
key

如果允许:

cpp 复制代码
*it = 100;

就相当于修改红黑树的搜索依据。

例如:

text 复制代码
      20
     /  \
   10    30

如果直接把:

text 复制代码
10 → 100

得到:

text 复制代码
      20
     /  \
   100   30

显然已经不是 BST。

因此:

text 复制代码
set 的元素不能通过 iterator 修改

我们可以让底层保存:

cpp 复制代码
const K

即:

cpp 复制代码
RBTree<
    K,
    const K,
    SetKeyOfT
> _t;

二十一、map 为什么 key 不能修改,但 value 可以?

map 中一个元素:

cpp 复制代码
pair<const K, V>

例如:

cpp 复制代码
("apple", 5)

其中:

text 复制代码
apple

决定这个结点在红黑树中的位置。

所以:

cpp 复制代码
it->first = "banana";

必须禁止。

但是:

text 复制代码
5

并不参与树的排序。

所以:

cpp 复制代码
it->second = 10;

完全合法。

于是 map 保存:

cpp 复制代码
pair<const K, V>

恰好满足:

text 复制代码
first 不能修改
second 可以修改

二十二、为什么 insert 不能只返回 bool?

最开始我们可能写:

cpp 复制代码
bool Insert(const T& data);

插入成功返回:

text 复制代码
true

失败返回:

text 复制代码
false

但是实现:

cpp 复制代码
map::operator[]

以后,这个返回值已经不够用了。

我们不仅想知道:

text 复制代码
是否插入成功

还需要知道:

text 复制代码
对应元素在哪里

因此更合理的返回值是:

cpp 复制代码
pair<Iterator, bool>

二十三、pair<iterator,bool> 分别表示什么?

例如:

cpp 复制代码
pair<Iterator, bool> ret;

其中:

text 复制代码
ret.first
    ↓
目标元素 iterator

ret.second
    ↓
是否新插入成功

假设:

cpp 复制代码
m.insert({"apple", 5});

如果:

text 复制代码
apple 不存在

那么:

text 复制代码
first  → 新插入的 apple
second → true

如果已经存在:

text 复制代码
first  → 原来的 apple
second → false

这正是 map::operator[] 需要的行为。


二十四、Insert 的返回值改造

空树:

cpp 复制代码
if (_root == nullptr)
{
    _root = new Node(data);
    _root->_col = BLACK;

    return make_pair(
        Iterator(_root, _root),
        true
    );
}

发现重复 key:

cpp 复制代码
else
{
    return make_pair(
        Iterator(cur, _root),
        false
    );
}

成功插入以后:

cpp 复制代码
return make_pair(
    Iterator(newnode, _root),
    true
);

注意为什么需要单独保存:

cpp 复制代码
Node* newnode = cur;

因为红黑树后面在调整过程中:

text 复制代码
cur

可能被不断改成:

text 复制代码
grandfather

所以如果最后直接返回:

cpp 复制代码
Iterator(cur, _root)

有可能已经不是:

text 复制代码
真正插入的结点

因此插入后立即:

cpp 复制代码
Node* newnode = cur;

保存它。


二十五、map::operator\[\] 是怎么实现的?

现在终于可以实现:

cpp 复制代码
m[key]

代码非常短:

cpp 复制代码
V& operator[](const K& key)
{
    pair<iterator, bool> ret =
        insert(
            make_pair(key, V())
        );

    return ret.first->second;
}

虽然只有几行,但这里的设计非常巧妙。


二十六、operator\[\] 的第一种情况:key 已经存在

假设:

cpp 复制代码
map<string, int> m;

m.insert({"apple", 5});

然后:

cpp 复制代码
m["apple"];

调用:

cpp 复制代码
insert(
    make_pair(
        "apple",
        int()
    )
);

因为:

text 复制代码
apple 已经存在

Insert 返回:

text 复制代码
iterator → 原来的 ("apple",5)
bool     → false

于是:

cpp 复制代码
ret.first->second

就是:

text 复制代码
5

并不会真正插入新的结点。


二十七、operator\[\] 的第二种情况:key 不存在

如果:

cpp 复制代码
m["banana"];

但是:

text 复制代码
banana

不存在。

此时:

cpp 复制代码
make_pair(
    "banana",
    int()
)

其中:

cpp 复制代码
int()

得到:

text 复制代码
0

所以会插入:

text 复制代码
("banana", 0)

然后返回:

cpp 复制代码
second

的引用。

因此:

cpp 复制代码
m["banana"] = 20;

实际上可以理解为:

text 复制代码
1. 查找 banana

2. 不存在
      ↓
   插入
   ("banana",0)

3. 返回 value 的引用

4. value = 20

最终:

text 复制代码
("banana",20)

二十八、为什么 operator\[\] 可能"偷偷插入元素"?

这是 map 使用时一个非常重要的细节。

下面这句:

cpp 复制代码
cout << m["hello"];

很多初学者会认为:

text 复制代码
只是查询

但如果:

text 复制代码
"hello" 不存在

实际上会先插入:

text 复制代码
("hello", V())

所以:

text 复制代码
operator[]

不是一个纯查找操作。

如果只是想查询:

text 复制代码
是否存在

更适合:

cpp 复制代码
find()

二十九、扩展理解:为什么 const map 不能随便使用 operator\[\]?

这也能从前面的原理直接推出来。

因为:

cpp 复制代码
operator[]

在 key 不存在时可能:

text 复制代码
插入新元素

也就是说它可能:

text 复制代码
修改容器

因此对一个:

cpp 复制代码
const map

来说,这种操作自然不能作为普通只读访问使用。

这个现象从:

text 复制代码
operator[] = 查找 + 必要时插入

就很好理解了。


三十、find 如何实现?

find 依然利用 BST 搜索。

泛型封装以后建议继续使用:

text 复制代码
KeyOfT

而不是重新假设:

text 复制代码
T 一定是 pair

例如:

cpp 复制代码
Iterator Find(const K& key)
{
    KeyOfT kot;

    Node* cur = _root;

    while (cur)
    {
        if (kot(cur->_data) < key)
        {
            cur = cur->_right;
        }
        else if (kot(cur->_data) > key)
        {
            cur = cur->_left;
        }
        else
        {
            return Iterator(
                cur,
                _root
            );
        }
    }

    return End();
}

这一点尤其值得注意。

既然 RBTree 已经泛型化成:

cpp 复制代码
RBTree<K,T,KeyOfT>

那么后面的:

text 复制代码
Insert
Find
Erase

都应该统一通过:

cpp 复制代码
KeyOfT

提取 key。

不要在某个函数中又重新写死:

cpp 复制代码
_data.first

否则:

text 复制代码
map 可以工作
set 可能就无法复用

三十一、完整 myset 封装

cpp 复制代码
#pragma once

#include "RBTree.h"

namespace bit
{
    template<class K>
    class set
    {
        struct SetKeyOfT
        {
            const K& operator()(
                const K& key)
            {
                return key;
            }
        };

        typedef RBTree<
            K,
            const K,
            SetKeyOfT
        > Tree;

    public:

        typedef typename Tree::Iterator
            iterator;

        typedef typename Tree::ConstIterator
            const_iterator;

        iterator begin()
        {
            return _t.Begin();
        }

        iterator end()
        {
            return _t.End();
        }

        const_iterator begin() const
        {
            return _t.Begin();
        }

        const_iterator end() const
        {
            return _t.End();
        }

        pair<iterator, bool>
        insert(const K& key)
        {
            return _t.Insert(key);
        }

        iterator find(const K& key)
        {
            return _t.Find(key);
        }

    private:

        Tree _t;
    };
}

三十二、测试 myset

cpp 复制代码
void test_set()
{
    bit::set<int> s;

    int a[] =
    {
        4, 2, 6, 1, 3,
        5, 15, 7, 16, 14
    };

    for (auto e : a)
    {
        s.insert(e);
    }

    for (auto e : s)
    {
        cout << e << " ";
    }

    cout << endl;
}

理论上输出:

text 复制代码
1 2 3 4 5 6 7 14 15 16

注意:

输出顺序不是插入顺序,而是红黑树的中序遍历顺序。

因此:

text 复制代码
set 天然有序

三十三、完整 mymap 封装

cpp 复制代码
#pragma once

#include "RBTree.h"

namespace bit
{
    template<class K, class V>
    class map
    {
        typedef pair<const K, V>
            ValueType;

        struct MapKeyOfT
        {
            const K& operator()(
                const ValueType& kv)
            {
                return kv.first;
            }
        };

        typedef RBTree<
            K,
            ValueType,
            MapKeyOfT
        > Tree;

    public:

        typedef typename Tree::Iterator
            iterator;

        typedef typename Tree::ConstIterator
            const_iterator;

        iterator begin()
        {
            return _t.Begin();
        }

        iterator end()
        {
            return _t.End();
        }

        const_iterator begin() const
        {
            return _t.Begin();
        }

        const_iterator end() const
        {
            return _t.End();
        }

        pair<iterator, bool>
        insert(const ValueType& kv)
        {
            return _t.Insert(kv);
        }

        iterator find(const K& key)
        {
            return _t.Find(key);
        }

        V& operator[](const K& key)
        {
            pair<iterator, bool> ret =
                insert(
                    make_pair(
                        key,
                        V()
                    )
                );

            return ret.first->second;
        }

    private:

        Tree _t;
    };
}

三十四、测试 mymap

cpp 复制代码
void test_map()
{
    bit::map<string, string> dict;

    dict.insert(
        {"sort", "排序"}
    );

    dict.insert(
        {"left", "左边"}
    );

    dict.insert(
        {"right", "右边"}
    );

    dict["left"] =
        "左边,剩余";

    dict["insert"] =
        "插入";

    dict["string"];

    for (auto it = dict.begin();
         it != dict.end();
         ++it)
    {
        cout
            << it->first
            << ":"
            << it->second
            << endl;
    }
}

注意:

cpp 复制代码
it->second += "x";

可以。

但是:

cpp 复制代码
it->first += "x";

应该无法通过编译。

原因就是:

cpp 复制代码
pair<const K, V>

中的:

text 复制代码
first

const K


三十五、整个封装过程其实是一条完整的演化路线

现在回过头,可以看到我们的代码不是一次性设计出来的。

而是逐步演化:

text 复制代码
第一阶段

RBTree<K,V>
结点写死 pair<K,V>

        ↓

第二阶段

RBTree<K,T>
T 决定结点存什么

        ↓

第三阶段

RBTree<K,T,KeyOfT>
通过仿函数获得 key

        ↓

第四阶段

支持 Iterator
Begin
End

        ↓

第五阶段

Iterator<T,Ref,Ptr>
复用 iterator / const_iterator

        ↓

第六阶段

set:
const K

map:
pair<const K,V>

解决 key 不允许修改

        ↓

第七阶段

Insert
返回 pair<iterator,bool>

        ↓

第八阶段

map::operator[]

三十六、时间复杂度分析

由于底层仍然是:

text 复制代码
红黑树

高度保持:

text 复制代码
O(logN)

因此:

1. insert

寻找插入位置:

text 复制代码
O(logN)

红黑树调整:

text 复制代码
O(logN)

所以整体:

text 复制代码
O(logN)

2. find

只沿一条搜索路径:

text 复制代码
O(logN)

3. operator\[\]

本质执行:

text 复制代码
insert

所以:

text 复制代码
O(logN)

4. begin

我们当前实现需要一路寻找:

text 复制代码
最左结点

因此:

text 复制代码
O(logN)

如果像经典 STL 红黑树一样额外使用:

text 复制代码
header

直接维护最左结点,那么 begin() 可以进一步简化。


三十七、常见错误总结

1. ++ 直接移动到父结点

错误:

cpp 复制代码
_node = _node->_parent;

因为中序后继并不一定是父亲。

正确判断:

text 复制代码
有右子树
    → 右子树最左

无右子树
    → 向祖先寻找

2. 有右子树时直接进入右孩子

错误:

cpp 复制代码
_node = _node->_right;

因为真正的下一个结点应该是:

text 复制代码
右子树中最左结点

3. 忘记特殊处理 --end()

我们使用:

text 复制代码
nullptr

作为 end()

因此:

cpp 复制代码
--end()

必须单独定位:

text 复制代码
整棵树最右结点

4. set iterator 返回 K&

如果允许:

cpp 复制代码
*it = ...

可能破坏红黑树结构。

所以:

text 复制代码
set 元素必须只读

5. map 使用 pair<K,V>

如果写:

cpp 复制代码
pair<K,V>

则:

text 复制代码
first 可以修改

这是不合理的。

应该:

cpp 复制代码
pair<const K,V>

6. Insert 只返回 bool

这样无法方便实现:

cpp 复制代码
operator[]

更好的接口是:

cpp 复制代码
pair<iterator,bool>

7. operator\[\] 直接写一套搜索代码

这样会重复:

text 复制代码
查找
插入

逻辑。

更好的方式:

cpp 复制代码
insert(make_pair(key, V()))

直接复用 insert


8. 泛型化后 Find 又写死 pair

例如:

cpp 复制代码
cur->_data.first

这种写法会重新把:

text 复制代码
RBTree

绑定到 map。

应该统一:

cpp 复制代码
KeyOfT kot;

kot(cur->_data)

这才能保持:

text 复制代码
set/map 共用一棵红黑树

的设计目标。


相关推荐
m0_547486661 小时前
《面向对象与Java程序设计》全套PPT课件2026
java·开发语言
旖旎夜光1 小时前
LeetCode 30:串联所有单词的子串(滑动窗口) —— 题解
数据结构·c++·算法·leetcode·滑动窗口
jason成都1 小时前
ignav-next|Java 层封装 GNSS/INS 组合导航框架,隔离 RTKLIB 与 INS 内核,双模块独立演进、最小侵入
java·开发语言
抓不住时间的沙1 小时前
butterfly主题美化,打造属于自己的个性博客
java·开发语言·前端·javascript·node.js·github
MarkHD2 小时前
Stable Diffusion入门第19-20天:保存与分享你的工作流——第一阶段收官,从“能用”到“可复用”
java·开发语言·stable diffusion
云水初2 小时前
【agent篇】RAG 知识库构建避坑指南
开发语言·python·学习·agent·rag
小小龙学IT2 小时前
oneTBB 开源并行编程库深度解析:从工作窃取调度器到 flow graph 实战
c++·开源
2019一路前行2 小时前
Python 函数、循环语句
开发语言·python
CarIise3 小时前
C语言基础课程笔记:硬件原理、数据类型、结构体与格式化输入输出详解
c语言·开发语言·笔记