【问题标题】:Partitioning/batch/chunk a container into equal sized pieces using std algorithms使用标准算法将容器分区/批处理/分块成相等大小的块
【发布时间】:2013-01-09 01:27:01
【问题描述】:

我遇到了一种情况,我必须将一组记录批量处理到数据库中。我想知道如何使用 std 算法来实现这一点。

给定 10002 条记录,我想将其划分为 100 条记录的 bin 进行处理,其余为 2 条的 bin。

希望以下代码能更好地说明我想要完成的工作。我对涉及迭代器、lambda 以及任何现代 C++ 乐趣的解决方案持完全开放的态度。

#include <cassert>
#include <vector>
#include <algorithm>

template< typename T >
std::vector< std::vector< T > > chunk( std::vector<T> const& container, size_t chunk_size )
{
  return std::vector< std::vector< T > >();
}

int main()
{
  int i = 0;
  const size_t test_size = 11;
  std::vector<int> container(test_size);
  std::generate_n( std::begin(container), test_size, [&i](){ return ++i; } );

  auto chunks = chunk( container, 3 );

  assert( chunks.size() == 4 && "should be four chunks" );
  assert( chunks[0].size() == 3 && "first several chunks should have the ideal chunk size" );
  assert( chunks.back().size() == 2 && "last chunk should have the remaining 2 elements" );

  return 0;
}

【问题讨论】:

  • 您的限制条件具体是什么? (即我们如何防止这个问题成为一个洗衣清单?)
  • @OliCharlesworth:听起来很直接。给定一个N 元素列表,他希望将其划分为最多Ceiling(N / k) 个bin(子列表)的列表,其中k 是一个bin 中的最大元素数。对于 N % k != 0,这将导致最后一个 bin 包含 N % k 元素。
  • std 算法,最好不要 if/else 也不要计数器。
  • @MikeBantegui,更加雄辩和简洁的陈述。
  • Zac,你为什么要返回一个向量的向量,而不是 ranges 的向量(其中每个范围都表示为一对迭代器(from,to)

标签: c++ boost stl c++11


【解决方案1】:

在并行化的上下文中,这种范围的划分很常见,我发现将 范围 的概念定义为有用的,即一对 (from,to) 是最小的单元在进程的各个例程和线程之间传递。

这比为每个部分复制整个子向量要好,因为它需要的内存空间要少得多。它也比只维护单个结束迭代器更实用,因为每个范围都可以按原样传递给线程——当它是第一个或最后一个部分等时没有特殊情况。

考虑到这一点,以下是我发现运行良好的例程的简化版本,并且都是现代 C++11:

#include <cassert>
#include <vector>
#include <utility>
#include <algorithm>
#include <cstdint>

template <typename It>
std::vector<std::pair<It,It>>
  chunk(It range_from, It range_to, const std::ptrdiff_t num)
{
  /* Aliases, to make the rest of the code more readable. */
  using std::vector;
  using std::pair;
  using std::make_pair;
  using std::distance;
  using diff_t = std::ptrdiff_t;

  /* Total item number and portion size. */
  const diff_t total
  { distance(range_from,range_to) };
  const diff_t portion
  { total / num };

  vector<pair<It,It>> chunks(num);

  It portion_end
  { range_from };

  /* Use the 'generate' algorithm to create portions. */    
  std::generate(begin(chunks),end(chunks),[&portion_end,portion]()
        {
          It portion_start
          { portion_end };

          portion_end += portion;
          return make_pair(portion_start,portion_end);
        });

  /* The last portion's end must always be 'range_to'. */    
  chunks.back().second = range_to;

  return chunks;
}

int main()
{
  using std::distance;

  int i = 0;
  const size_t test_size = 11;
  std::vector<int> container(test_size);
  std::generate_n( std::begin(container), test_size, [&i](){ return ++i; } );

  /* This is how it's used: */    
  auto chunks = chunk(begin(container),end(container),3);

  assert( chunks.size() == 3 && "should be three chunks" );
  assert( distance(chunks[0].first,chunks[0].second) == 3 && "first several chunks should have the ideal chunk size" );
  assert( distance(chunks[2].first,chunks[2].second) == 5 && "last chunk should have 5 elements" );

  return 0;
}

它的工作方式与您建议的略有不同:部分大小始终向下舍入,因此在您的示例中您只得到 3 个部分,最后部分比其他部分略大(而不是略小)。这可以很容易地修改(我认为这并不重要,因为通常部分的数量远小于工作项的总数)。


备注。在我自己使用范围相关模式时,很快发现实际存储整数(每个表示与.begin() 的距离)而不是迭代器通常更好。原因是这些整数和实际迭代器之间的转换是一种快速且无害的操作,无论您需要iterator 还是const_iterator 都可以执行。然而,当您存储迭代器时,您需要一劳永逸地决定是使用 iterator 还是 const_iterator,这可能会很痛苦。

【讨论】:

  • 有趣。感谢分享-我会在接受之前看看是否有其他人进来。仅供参考,range_from 已被捕获且未使用。
  • @Zac 谢谢,已更正。 (我捕获了range_from,因为在我的函数版本中,我实际上存储的是整数,而不是迭代器。我添加了一条注释来解释为什么这很有用。)
  • 虽然你完全按照 OP 的要求做了,但我仍然更喜欢普通循环和 ifs:ideone.com/PzYMTH
  • @MooingDuck,我很好奇你喜欢循环而不是 std::generate 的原因?
  • std::advance 上的随机访问迭代器应该解析为 O(1) 操作,因此更改对于扩展它可以操作的范围范围很有用。好的代码顺便说一句,谢谢。
【解决方案2】:

问题似乎是std::for_each 的变体,其中您要操作的“每个”是您的集合的间隔。因此,您更愿意编写一个 lambda(或函数),它采用两个迭代器来定义每个间隔的开始和结束,并将该 lambda/函数传递给您的算法。

这就是我想出的......

// (Headers omitted)

template < typename Iterator >
void for_each_interval(
    Iterator begin
  , Iterator end
  , size_t interval_size
  , std::function<void( Iterator, Iterator )> operation )
{
  auto to = begin;

  while ( to != end )
  {
    auto from = to;

    auto counter = interval_size;
    while ( counter > 0 && to != end )
    {
      ++to;
      --counter;
    }

    operation( from, to );
  }
}

(我希望std::advance 会处理使用counter 递增to 的内部循环,但不幸的是它盲目地超出了结尾[我很想编写自己的smart_advance 模板来封装这个]。如果这样可行,它将减少大约一半的代码量!)

现在需要一些代码来测试它...

// (Headers omitted)

int main( int argc, char* argv[] )
{
  // Some test data
  int foo[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  std::vector<int> my_data( foo, foo + 10 );
  size_t const interval = 3;

  typedef decltype( my_data.begin() ) iter_t;
  for_each_interval<iter_t>( my_data.begin(), my_data.end(), interval,
    []( iter_t from, iter_t to )
    {
      std::cout << "Interval:";
      std::for_each( from, to,
        [&]( int val )
        {
          std::cout << " " << val;
        } );
      std::cout << std::endl;
    } );
}

这会产生以下输出,我认为它代表了您想要的:

间隔:0 1 2 间隔:3 4 5 间隔:6 7 8 间隔:9

【讨论】:

  • 我喜欢你的不需要分配向量——尽管我可以用它来产生一个。
  • 确实,您的“操作”函数可用于 (1) 累积原始数据(的副本)的向量向量,或 (2) 累积界定区间边界的迭代器,或(3) 对给定的 from/to 范围进行操作,等等。标准算法万岁!
  • 自我批评:我没试过,但我想如果interval_size 传递一个0,我的for_each_interval 函数中有一个无限循环。
【解决方案3】:

实现略有不同,但对迭代器使用范围操作。我也在考虑使用 std::partition 函数的实现。

#include <iostream>
#include <cassert>
#include <vector>
#include <algorithm>

template< typename Iterator >
void sized_partition( Iterator from, Iterator to, std::ptrdiff_t partition_size, std::function<void(Iterator partition_begin, Iterator partition_end)> range_operation )
{
  auto partition_end = from;
  while( partition_end != to )
  {
    while( partition_end != to && std::distance( from, partition_end ) < partition_size )
      ++partition_end;

    range_operation( from, partition_end );
    from = partition_end;
  }
}

int main()
{
  int i = 0;
  const size_t test_size = 11;
  std::vector<int> container(test_size);
  typedef std::vector<int>::iterator int_iterator;
  std::generate_n( std::begin(container), test_size, [&i](){ return ++i; } );

  sized_partition<int_iterator>( container.begin(), container.end(), 3, []( int_iterator start_partition, int_iterator end_partition )
  {
    std::cout << "Begin: ";
    std::copy( start_partition, end_partition, std::ostream_iterator<int>(std::cout, ", ") );
    std::cout << " End" << std::endl;
  });

  return 0;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-10-29
    • 2019-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-28
    • 2012-05-09
    • 1970-01-01
    相关资源
    最近更新 更多