【问题标题】:How do I iterate equal values with the standard library?如何使用标准库迭代相等的值?
【发布时间】:2019-11-13 12:45:28
【问题描述】:

假设我有一个东西的向量:

std::vector<Foo> v;

此向量已排序,因此相等的元素彼此相邻。

获取表示具有相等元素的范围的所有迭代器对的最佳方法是什么(使用标准库)?

while (v-is-not-processed) {
    iterator b = <begin-of-next-range-of-equal-elements>;
    iterator e = <end-of-next-range-of-equal-elements>;

    for (iterator i=b; i!=e; ++i) {
        // Do something with i
    }
}

我想知道如何在上面的代码中获取be 的值。

例如,如果v 包含这些数字:

 index 0 1 2 3 4 5 6 7 8 9
 value 2 2 2 4 6 6 7 7 7 8

然后我想让be 指向循环中的元素:

 iteration  b  e
 1st        0  3
 2nd        3  4
 3rd        4  6
 4th        6  9
 5th        9 10

有没有一种优雅的方法可以用标准库解决这个问题?

【问题讨论】:

  • 应该注意,问题中的代码并不是一个很好的例子来说明它是如何有用的。除了限制内部循环之外,e 没有做任何事情,并且内部循环也可以通过测试元素 i 中的新值来限制。因此,寻找e 所花费的任何努力都是徒劳的(除非计算用于对v 进行排序的“值”非常昂贵,以至于对e 进行二分搜索比在我们进行时测试每个元素要便宜)。
  • @EricPostpischil:确实如此。但是即使我们什么都不用e,这个公式很方便,更难出错。另一种方法(检查值的变化)更繁琐(因为我们需要特别处理最后一个范围 - 或者你知道一些技巧来避免它吗?)。
  • @geza 最后一个范围不需要特殊处理。

标签: c++ algorithm c++17 c++-standard-library iterator-range


【解决方案1】:

这基本上是 Range v3 的 group_by: group_by(v, std::equal_to{})。它在 C++17 标准库中不存在,但我们可以编写自己的粗略等价物:

template <typename FwdIter, typename BinaryPred, typename ForEach>
void for_each_equal_range(FwdIter first, FwdIter last, BinaryPred is_equal, ForEach f) {
    while (first != last) {
        auto next_unequal = std::find_if_not(std::next(first), last,
            [&] (auto const& element) { return is_equal(*first, element); });

        f(first, next_unequal);
        first = next_unequal;
    }
}

用法:

for_each_equal_range(v.begin(), v.end(), std::equal_to{}, [&] (auto first, auto last) {
    for (; first != last; ++first) {
        // Do something with each element.
    }
});

【讨论】:

  • 如果你知道这些相等元素的子范围很大,你可以通过某种二分搜索来探索。
  • 我最喜欢这个解决方案,因为它的用法是最清晰的。
【解决方案2】:

您可以使用std::upper_bound 将迭代器获取到“下一个”值。由于std::upper_bound 将迭代器返回到大于所提供值的第一个元素,如果您提供当前元素的值,它将为您提供一个超过当前值末尾的迭代器。这会给你一个像

这样的循环
iterator it = v.begin();
while (it != v.end()) {
    iterator b = it;
    iterator e = std::upper_bound(it, v.end(), *it);

    for (iterator i=b; i!=e; ++i) {
        // do something with i
    }
    it = e; // need this so the loop starts on the next value
}

【讨论】:

  • 唯一的问题是std::upper_bound 做的有点多,因为它必须通过二分搜索找到元素。但就我而言,这是不需要的。
  • 如果相等元素的子范围很大,这是有道理的。如果它们很小,那么在整个范围内进行二分搜索就会浪费精力,因为线性搜索可以更快地找到下一个元素(并且具有更好的缓存局部性)。
  • @geza 如果您想进行线性遍历,那么您可以将std::upper_bound(it, v.end(), *it); 替换为std::find_if(it, v.end(), [=](auto e) { return *it != e; });。根据数据,这肯定会更快。
  • +1,谢谢,但我接受了贾斯汀的解决方案,因为它在用法上更清楚一点(我的意思是,算法有一个名字,所以更容易理解,代码是什么确实 - 但当然,您的变体也可以通过这种方式进行修改,您的解决方案基本相同)。
【解决方案3】:

您正在寻找std::equal_range

返回一个范围,其中包含与 范围[第一,最后)

类似下面的东西应该可以工作。

auto it = v.begin();
while (it != v.end())
{
    auto [b, e] = std::equal_range(it, v.end(), *it);
    for (; b != e; ++b) { /* do something in the range[b, e) */ }
    it = e;             // need for the beginning of next std::equal_range
}

备注:尽管这将是一种直观的方法,std::equal_range 获得了它的 firstsecond 迭代器(即be) 在 std::lower_boundstd::upper_bound 的帮助下,这使得这个 approche slightly inefficient。因为,对于 OP 的情况,first 迭代器可以很容易地访问,只需要为 second 迭代器调用 std::upper_bound(如 @NathanOliver 所示的答案)。

【讨论】:

  • 当我们知道它只是 it 时,这会做一些额外的工作来找到范围的下限,但在这一点上,我们将与 NathanOliver 的答案相同 (std::upper_bound而不是std::equal_range)。
  • @贾斯汀同意。也许有一点优势:由于结构化绑定的可能性,打字更少
  • +1,但我接受了 Justin 的解决方案,因为虽然这是最短的版本(并且无需添加名称也易于理解),但存在 std::equal_range 完成不必要工作的小问题。
【解决方案4】:

如果您的相等值范围很短,那么std::adjacent_find 会很好用:

for (auto it = v.begin(); it != v.end();) {
    auto next = std::adjacent_find(it, v.end(), std::not_equal_to<Foo>());
    for(; it != next; ++it) {

    }
}

如果您愿意,也可以用 lambda 替换 std::not_equal_to

【讨论】:

  • 使用std::adjacent_find找到边界的好技巧!
【解决方案5】:

但是即使我们不使用 e 来表示任何东西,这个公式也很方便,而且更难出错。另一种方法(检查值的变化)更繁琐(因为我们需要特别处理最后一个范围[...])

取决于你如何理解'特别处理最后一个范围'

auto begin = v.begin();
// we might need some initialization for whatever on *begin...
for(Iterator i = begin + 1; ; ++i)
{
    if(i == v.end() || *i != *begin)
    {
        // handle range single element of range [begin, ???);
        if(i == v.end())
            break;
        begin = i;
        // re-initialize next range
    }
}

对最后一个范围没有特殊处理——只是,可能需要两次初始化代码...

嵌套循环方法:

auto begin = v.begin();
for(;;)
{
    // initialize first/next range using *begin
    for(Iterator i = begin + 1; ; ++i)
    {
        if(i == v.end() || *i != *begin)
        {
            // handle range single element of range [begin, ???);
            if(i == v.end())
                goto LOOP_EXIT;
            begin = i;
            break;
        }
    }
}
LOOP_EXIT:
// go on
// if nothing left to do in function, we might prefer returning over going to...

更优雅?承认,我自己也很怀疑……不过,这两种方法都避免在同一范围内迭代两次(首先是为了找到终点,然后是实际的迭代)。如果我们从以下位置创建自己的库函数:

template <typename Iterator, typename RangeInitializer, typename ElementHandler>
void iterateOverEqualRanges
(
    Iterator begin, Iterator end,
    RangeInitializer ri, ElementHandler eh
)
{
    // the one of the two approaches you like better
    // or your own variation of...
}

然后我们可以像这样使用它:

std::vector<...> v;
iterateOverEqualRanges
(
    v.begin(), v.end(),
    [] (auto begin) { /* ... */ },
    [] (auto current) { /* ... */ }
);

最后,它看起来与 e 相似。 G。 std::for_each,不是吗?

【讨论】:

  • 感谢您的解决方案,我喜欢它不需要对元素进行两次迭代。通过“特别处理最后一个范围”,我的意思是我们需要以某种方式检查它。即使通过两次i == v.end()
【解决方案6】:
for(auto b=v.begin(), i=b, e=v.end(); i!=e; b=i) {
    // initialise the 'Do something' code for another range
    for(; i!=e && *i==*b; ++i) {
        // Do something with i
    }
}

【讨论】:

    猜你喜欢
    • 2019-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-08
    • 2014-09-17
    • 2012-03-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多