std::map 可以在迭代器处有效地拆分为两个 std::maps 吗?

Posted

技术标签:

【中文标题】std::map 可以在迭代器处有效地拆分为两个 std::maps 吗?【英文标题】:Can a std::map be efficiently split into two std::maps at an iterator? 【发布时间】:2021-03-02 23:40:03 【问题描述】:

假设我有一个包含数据的std::map<int, std::string> myMap

1. Red
2. Blue
3. Green
5. Fuchsia
6. Mauve
9. Gamboge
10. Vermillion

还有一个指向元素的std::map<int, std::string>::iterator it

5. Fuchsia

我想做一些类似的事情(编造这个)

std::map<int, std::string> myHead = eject(myMap, myMap.begin(), it);

这将导致myMap 包含

5. Fuchsia
6. Mauve
9. Gamboge
10. Vermillion

myHead包含

1. Red
2. Blue
3. Green

我可以通过做类似的事情来做到这一点

std::map<int, std::string> myHead;
myHead.insert(myMap.begin(), it);
myMap.erase(myMap.begin(), it);

但这至少在某些情况下似乎不是最理想的,例如如果我选择一个点,我只是分裂一个子树。 (我承认我并没有真正考虑过这里算法复杂性的实际细节,但是如果我们想象一个值类型复制起来非常昂贵的情况,那么很明显,上述方法通常不是最优的.)

问题: 有没有办法让std::map 以最佳方式执行此操作,或者我是否必须编写自己的二叉搜索树来访问内部结构要做到这一点?

【问题讨论】:

@ildjarn:我知道这些,但我看不到使用它们来完成我在这里描述的内容的方法。 extract 在单个节点上运行(不带其下方的节点),因此似乎任何基于此的解决方案都必须至少为 O(m),其中 m 是被拆分的片段中的元素数.而且我在这里根本看不到merge 的用途。 【参考方案1】:

如果我们说的是渐近复杂性,对于大多数自平衡树类型,您可以在 O(log n) 时间内执行此操作,使用通俗地称为 splitjoin 的两个操作。这里有一个广泛的Wikipedia article。

您无法使用std::map 获得这种复杂性,您需要推出自己的或第三方的自平衡树实现。如果您需要经常进行此操作,这是非常值得的。使用标准库可以获得的最佳效果是 O(n),它可能会慢很多数量级。

您可以在 C++11 中的 O(n) 中这样做:

template<class K, class T, class C, class A>
std::map<K, T, C, A> eject(
    std::map<K, T, C, A>& my_map,
    std::map<K, T, C, A>::iterator begin,
    std::map<K, T, C, A>::iterator end,
) 
    std::map<K, T, C, A> result;
    while (begin != end) 
        auto next = std::next(begin);
        // C++11
        result.insert(result.end(), std::move(*begin));
        my_map.erase(begin);
        // C++17 (avoids move and destruct)
        // result.insert(result.end(), my_map.extract(begin));
        begin = next;
    
    return result;
        

【讨论】:

谢谢,这很有帮助。是否有实现此功能的知名第三方 BST? @DanielMcLaury 我不知道,抱歉。 不,std 库可以将地图拆分为 O(较小地图的大小),而不是 n lg n。 只需在末尾按顺序提取和插入即可。两者都是摊销的常数时间操作,在较小的容器节点上。 @Yakk-AdamNevraumont 很公平,我将编辑O(n) 的答案,尽管这仍然会慢很多数量级。【参考方案2】:

可以使用move iterators,如下

int main()
    auto my_map = std::map<int, std::string>
        1, "Read"s,
        2, "Blue"s,
        3, "Green"s,
        5, "Fuchsia"s,
        6, "Mauve"s,
         9, "Gamboge"s ,
        10, "Vermillion"s
    ;
    auto it = my_map.begin();
    std::advance(it, 3);
    auto head_map = std::map
        std::make_move_iterator(my_map.begin()),
        std::make_move_iterator(it)
    ;
    auto tail_map = std::map
        std::make_move_iterator(it),
        std::make_move_iterator(my_map.end())
    ;
    std::cout << "The Head\n";
    for (auto [key, value]: head_map)
        std::cout << key << ":" << value << " ";
    
    std::cout << "\n\nThe Tail\n";
    for (auto [key, value]: tail_map)
        std::cout << key << ":" << value << " ";
    

Demo

【讨论】:

所以,如果我理解正确的话,这解决了与移动值类型相关的任何潜在成本的问题,但是当我们调用构造函数来构建 tail_map 时,我们只是插入了尾顺序? @DanielMcLaury 不。我认为 begin 和 it 迭代器仅分配给新地图的 begin 和 end 迭代器。 @DanielMcLaury 是的。你是对的,我认为你需要实施你的。【参考方案3】:

有提取,但它在节点而不是节点范围上运行。

而且您可以有效地组合地图。

但没有有效(比 O(n) 快)基于范围的提取。

【讨论】:

以上是关于std::map 可以在迭代器处有效地拆分为两个 std::maps 吗?的主要内容,如果未能解决你的问题,请参考以下文章

为啥允许 std::unordered_map::rehash() 使迭代器无效?

如何仅针对键的子集有效地比较 C++ 中的两个字符串映射

如何将可迭代拆分为两个具有交替元素的列表

std::map的insert和下标[]操作区别

在不迭代的情况下在两个映射中找到共同值

std::map 上可能的线程不安全操作