使用标准算法复制每个其他元素(下采样)

3

假设我有一个包含N个元素的std::vector。我想要将其中每第n个元素复制到一个新的向量中,或者对该元素进行平均然后复制它(下采样原始向量)。因此,我想要执行以下操作:

std::vector<double> vec(N);
long n = 4;
std::vector<double> ds(N/n);
for(long i = 0; i < ds.size(); i+=n)
{
    ds[i] = vec[i*n];
}

或者

for(long i = 0; i < ds.size(); i+=n)
{
    double tmp = 0;    
    for(long j = 0; j < n; j++)
    {
        tmp += vec[i*n+j];
    }
    ds[i] = tmp/static_cast<double>(n);
}

有没有一种使用C++标准算法的方法来实现这个功能?比如使用带有二进制函数的std::copy?我有数十亿个元素要这样处理,我希望这样做尽可能快。
PS:我不想使用外部库,比如boost。

什么是“标准算法”? - Ed Heal
@EdHeal std::copy 等。 - OMGtechy
5
好的,请提供需要翻译的内容。 - Sergey Kalinichenko
3
为什么在使用循环可以使代码易于阅读时,你会选择使用标准算法? - Vlad from Moscow
Boost.Range有一些很好的适配器可以使这更加美观。仅使用标准库,它相当丑陋。 - Sebastian Redl
2
@Vlad 因为应用拟合算法(这里强调“拟合”)几乎是按定义更容易检查正确性的。 - Konrad Rudolph
5个回答

4

为了易读性,循环是一个好主意,正如Vlad在评论中指出的那样。但如果你真的想做这样的事情,可以尝试:

int cnt=0,n=3; 
vector<int> u(v.size()/3); 
copy_if (v.begin(), v.end(), u.begin(), 
          [&cnt,&n] (int i)->bool {return ++cnt %n ==0; } ); 

如果您想进行平均处理,则必须将transform()copy_if()结合使用。请注意,这种方法可能会更加糟糕。 编辑:如果您关注性能,则最好使用循环,正如davidhigh在评论中强调的那样:它将避免为每个元素调用lambda函数所带来的开销。如果您需要经常执行此操作,则最好编写自己的通用算法。

你正确地给出了免责声明。只是为了确认一下:上面的代码可能比手写循环慢大约n倍。基本上,它的时间复杂度是O(N),而O(N/n)是可能的。 - davidhigh
@davidhigh,使用lambda函数会比循环慢,因为它需要为每个元素调用一次。 - Christophe

4

你可以根据<algorithm>中的设计原则编写自己的通用算法。

每隔n个元素进行一次复制:

template<class in_it, class out_it>
out_it copy_every_n( in_it b, in_it e, out_it r, size_t n) {
    for (size_t i=distance(b,e)/n; i--; advance (b,n)) 
        *r++ = *b;
    return r;
}

使用示例:

vector<int> v {1,2,3,4,5,6,7,8,9,10};
vector<int> z(v.size()/3); 
copy_every_n(v.begin(), v.end(), z.begin(), 3);     

如果要对n乘n的元素进行平均,可以使用以下方法:

template<class in_it, class out_it>
out_it average_every_n( in_it b, in_it e, out_it r, size_t n) {
    typename out_it::value_type tmp=0;
    for (size_t cnt=0; b!=e; b++)  {
        tmp+=*b;
        if (++cnt==n) {
            cnt=0; 
            *r++=tmp/n;
            tmp=0;
        }
    }
    return r;
}

使用示例:

vector<int> w(v.size()/3); 
average_every_n(v.begin(), v.end(), w.begin(), 3);  

与您最初的循环相比,这种方法不仅适用于向量,还适用于任何提供begin()end()迭代器的容器。并且它避免了我在另一个答案中指出的开销。

实际上这意味着编写自己的循环而不是使用标准算法。:) - Vlad from Moscow
我的秘密希望是它们能够被包含在下一个标准版本中;-) - Christophe
2
我认为标准库中确实没有类似的东西。我同意@Christophe的观点,编写自己的算法(然后使用它)是一个不错的选择。 - Marshall Clow

3

如果仅使用标准库功能和算法,并且不允许使用循环,那么代码可能如下所示。请注意,该代码基于C++ 2014版本。如果您需要编译器只支持C++ 2011版本的代码,则必须进行一些小的更改。

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <iterator>

int main()
{
    const size_t N = 4;
    std::vector<double> src = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9 };
    size_t n = src.size() / N;
    std::vector<double> dst( n );

    std::copy_if( src.begin(), std::next( src.begin(), n * N ), dst.begin(),
                  [i = 0] ( auto ) mutable { return ( i = ( i + 1 ) % N ) == 0; } );

    for ( double x : dst ) std::cout << x << ' ';
    std::cout << std::endl;

    dst.assign( n, 0.0 );

    std::accumulate( src.begin(), std::next( src.begin(), n * N ), dst.begin(),
                     [i = 0] ( auto acc, auto x ) mutable
                     {
                         *acc += x; 
                         if ( ( i = ( i + 1 ) % N ) == 0 )  *acc++ /= N;
                         return acc;
                     } );

    for ( double x : dst ) std::cout << x << ' ';
    std::cout << std::endl;
}    

程序输出为:
4.4 8.8 
2.75 7.15 

在if条件中,这个复合表达式
if ( ( i = ( i + 1 ) % N ) == 0 )  *acc++ /= N;

您可以用更简单的代替它

if ( ++i % N == 0 )  *acc++ /= N;

1

你可能明确表示不想使用Boost,但任何非Boost的解决方案本质上都会实现这种功能,因此我将展示如何在Boost中实现它。最终,我认为编写一个简单的循环更好。

降采样使用 strided

boost::copy(
        input | strided(2),
        std::back_inserter(output));

下采样平均值还使用transformed,但此解决方案非通用,特别依赖于vector是连续的:
boost::copy(
        input | strided(2) | transformed([](auto& x){
                return std::accumulate(&x, &x + 2, 0) / 2.;
            }),
        std::back_inserter(output));

当然,如果输入的长度不是步长长度的整数倍,那么就会出现问题,因此最好做一些类似于以下的处理:
auto downsample_avg = [](auto& input, int n){
    return input | strided(n) | transformed([&,n](auto& x){
        auto begin = &x;
        auto end = begin + std::min<size_t>(n, &input.back() - begin + 1);
        return std::accumulate(begin, end, 0.0) / (end - begin);
    });
};

boost::copy(
    downsample_avg(input, 2),
    std::back_inserter(output));

这是对 Boost 的很好掌握!即使 OP 不喜欢使用它。 - Christophe

0

这个实现怎么样?

#include <iterator>

template<typename InputIt, typename OutputIt>
OutputIt DownSample(InputIt first, InputIt last, OutputIt d_first,
    typename std::iterator_traits<InputIt>::difference_type n) {
  while (first < last) {
    *(d_first++) = *first;
    std::advance(first, n);
  }
  return d_first;
}

你应该在你的代码中添加一些解释,以及它如何比之前发布的其他答案提供了改进。 - Adrian Mole
很容易理解,仅使用stdlib并且可以与容器和原始数组一起使用。 - jiaqi ju

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接