ChatGPT解决这个技术问题 Extra ChatGPT

连接两个向量的最佳方法是什么?

我正在使用多线程并希望合并结果。例如:

std::vector<int> A;
std::vector<int> B;
std::vector<int> AB;

我希望 AB 必须按顺序获得 A 的内容和 B 的内容。做这样的事情最有效的方法是什么?

如果在使用大型容器时寻找效率,使用列表可能更有效,您可以使用多个指针操作将一个拼接到另一个。但是列表有空间开销(考虑使用单链表)。
这回答了你的问题了吗? Concatenating two std::vectors

K
Kirill V. Lyadvinsky
AB.reserve( A.size() + B.size() ); // preallocate memory
AB.insert( AB.end(), A.begin(), A.end() );
AB.insert( AB.end(), B.begin(), B.end() );

谢谢!不会想到保留。
它应该复制每个元素,所以它是 O(n)
不确定是否要问一个新问题,但是在考虑移动语义时可以改进这个答案吗?有什么方法可以期望/指示编译器进行一次内存移动而不是循环遍历所有元素?
@boycy 不。 push_back 一个元素是摊销的常数时间。推回 n 个元素是 O(n)
@Konrad我没有暗示其他,但感谢您的澄清。请注意,插入操作的复杂性永远不会根据插入的元素数量给出 - 这总是会给出 O(n) - 但根据容器中已经存在的元素数量,因为这提供了衡量其可伸缩性的指标.
R
Robbie Morrison

这正是成员函数 std::vector::insert 的用途

std::vector<int> AB = A;
AB.insert(AB.end(), B.begin(), B.end());

@Nick:与什么相比慢?
也许它会检查元素的每个插入是否有足够的空间?预先使用储备会加快速度。
@Nick:如果每个现代 stdlib 实现都专门针对随机访问迭代器 insert 并预先保留,我不会感到惊讶。
@Gman:这是一个公平的观点,因为我们知道源也是一个向量(其中迭代器 distance 具有 O(1) 复杂度)。尽管如此,当您通常可以通过提前计划做得更好时,需要注意 insert 的性能保证。
@RvdK 检查空间只是几个指令:加载容量、比较大小、条件跳转;在大多数情况下,所有这些成本都可以忽略不计。由于 size < capacity 在大多数情况下,分支预测可能会导致非重新分配分支的指令位于指令流水线中,从而最大限度地减少分支引起的延迟,但迭代次数较少。这假设一个良好的向量实现,加上 CPU 指令流水线 & [good] 分支预测,但对于现代工具链和台式机来说,这些都是非常可靠的假设。虽然不知道智能手机..
b
bradgonesurfing

取决于您是否真的需要在物理上连接两个向量,或者为了迭代而想要给出连接的外观。 boost::join 函数

http://www.boost.org/doc/libs/1_43_0/libs/range/doc/html/range/reference/utilities/join.html

会给你这个。

std::vector<int> v0;
v0.push_back(1);
v0.push_back(2);
v0.push_back(3);

std::vector<int> v1;
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
...

BOOST_FOREACH(const int & i, boost::join(v0, v1)){
    cout << i << endl;
}

应该给你

1
2
3
4
5
6

注意 boost::join 不会将两个向量复制到新容器中,而是生成一对覆盖两个容器跨度的迭代器(范围)。会有一些性能开销,但可能比首先将所有数据复制到新容器要少。


好主意。想了一会儿,我意识到这个目标也可以在不使用 boost 库的情况下实现。我已经发布了一个答案,解释了如何。
R
Ronald Souza

在 Bradgonesurfing 的答案的方向上,很多时候人们并不真正需要连接两个向量 (O(n)),而只是像连接它们一样使用它们 (O(1))。如果这是您的情况,则无需 Boost 库即可完成。

诀窍是创建一个向量代理:一个包装类,它操作对两个向量的引用,在外部被视为单个连续的。

用法

std::vector<int> A{ 1, 2, 3, 4, 5};
std::vector<int> B{ 10, 20, 30 };

VecProxy<int> AB(A, B);  // ----> O(1). No copies performed.

for (size_t i = 0; i < AB.size(); ++i)
    std::cout << AB[i] << " ";  // 1 2 3 4 5 10 20 30

执行

template <class T>
class VecProxy {
private:
    std::vector<T>& v1, v2;
public:
    VecProxy(std::vector<T>& ref1, std::vector<T>& ref2) : v1(ref1), v2(ref2) {}
    const T& operator[](const size_t& i) const;
    const size_t size() const;
};

template <class T>
const T& VecProxy<T>::operator[](const size_t& i) const{
    return (i < v1.size()) ? v1[i] : v2[i - v1.size()];
};

template <class T>
const size_t VecProxy<T>::size() const { return v1.size() + v2.size(); };

主要好处

创建它是 O(1)(恒定时间),并且额外的内存分配最少。

需要考虑的一些事情

只有当你真的知道在处理参考文献时你在做什么时,你才应该这样做。该解决方案旨在针对所提出问题的特定目的,并且效果很好。如果您不确定引用的工作方式,在任何其他上下文中使用它可能会导致意外行为。

在此示例中,AB 不提供非常量访问运算符 ([ ])。随意包含它,但请记住:由于 AB 包含引用,因此为其分配值也会影响 A 和/或 B 中的原始元素。无论这是否是一个理想的功能,这是一个特定于应用程序的问题,应该仔细考虑。

直接对 A 或 B 进行的任何更改(如赋值、排序等)也将“修改”AB。这不一定是坏事(实际上,它可能非常方便:AB 永远不需要显式更新以保持自身与 A 和 B 同步),但这肯定是一种必须注意的行为。重要的例外:将 A 和/或 B 的大小调整到更大可能会导致它们在内存中重新分配(为了需要连续空间),这反过来会使 AB 无效。

因为每次访问一个元素之前都会有一个测试(即“i < v1.size()”),VecProxy 访问时间虽然恒定,但也比向量慢一点。

这种方法可以推广到 n 个向量。我没试过,但应该没什么大不了的。


正是我需要的,非常感谢
a
aloisdg

基于Kiril V. Lyadvinsky answer,我制作了一个新版本。这个片段使用模板和重载。有了它,您可以编写 vector3 = vector1 + vector2vector4 += vector3。希望它可以提供帮助。

template <typename T>
std::vector<T> operator+(const std::vector<T> &A, const std::vector<T> &B)
{
    std::vector<T> AB;
    AB.reserve(A.size() + B.size());                // preallocate memory
    AB.insert(AB.end(), A.begin(), A.end());        // add A;
    AB.insert(AB.end(), B.begin(), B.end());        // add B;
    return AB;
}

template <typename T>
std::vector<T> &operator+=(std::vector<T> &A, const std::vector<T> &B)
{
    A.reserve(A.size() + B.size());                // preallocate memory without erase original data
    A.insert(A.end(), B.begin(), B.end());         // add B;
    return A;                                        // here A could be named AB
}

您的意思是将每个向量的元素相互添加吗?或者你的意思是追加?现在很清楚,但在接下来的 5 年里..?如果含义不明确,则不应重载运算符。
@SR我的意思是连接。我在 3 年前写了这个答案。我仍然知道这意味着什么。那里没问题。如果 C++ 可以提供自己的重载,那就更好了。 (是的 :: 被采取了;)
一般来说,绝对不清楚 v1 + v2 不代表加法。
@Apollys well
替代方法是像在 F# 中一样使用 @
E
Erik Campobadal

另一种尚未提及的简单变体:

copy(A.begin(),A.end(),std::back_inserter(AB));
copy(B.begin(),B.end(),std::back_inserter(AB));

并使用合并算法:

#include <algorithm>
#include <vector>
#include <iterator>
#include <iostream>
#include <sstream>
#include <string>

template<template<typename, typename...> class Container, class T>
std::string toString(const Container<T>& v)
{
    std::stringstream ss;
    std::copy(v.begin(), v.end(), std::ostream_iterator<T>(ss, ""));
    return ss.str();
};


int main()
{
    std::vector<int> A(10);
    std::vector<int> B(5);  //zero filled
    std::vector<int> AB(15);

    std::for_each(A.begin(), A.end(),
            [](int& f)->void
            {
                f = rand() % 100;
            });

    std::cout << "before merge: " << toString(A) << "\n";
    std::cout << "before merge: " << toString(B) << "\n";
    merge(B.begin(),B.end(), begin(A), end(A), AB.begin(), [](int&,int&)->bool {});
    std::cout << "after merge:  " << toString(AB) << "\n";

    return 1;
}


u
user3360767

所有解决方案都是正确的,但我发现编写一个函数来实现它更容易。像这样:

template <class T1, class T2>
void ContainerInsert(T1 t1, T2 t2)
{
    t1->insert(t1->end(), t2->begin(), t2->end());
}

这样你就可以避免像这样的临时放置:

ContainerInsert(vec, GetSomeVector());

t
tsnorri

对于这个用例,如果您事先知道每个线程产生的结果数,您可以预先分配 AB 并将 std::span 传递给每个线程。这样就不需要进行串联。例子:

std::vector<int> AB(total_number_of_results, 0);
std::size_t chunk_length = …;
std::size_t chunk2_start = chunk_length;
std::size_t chunk3_start = 2 * chunk_length; // If needed
…
// Pass these to the worker threads.
std::span<int> A(AB.data(), chunk_length);
std::span<int> B(AB.data() + chunk2_start, chunk_length);
…

S
Sudharsan

我的回答是基于 Mr.Ronald Souza 的原始解决方案。除了他原来的解决方案,我还写了一个支持迭代器的向量代理!

对不了解原始解决方案上下文的人的简短描述:joined_vector 模板类(即向量代理)将两个向量的两个引用作为构造函数参数,然后将它们视为一个连续向量。我的实现还支持前向迭代器。

用法:

int main()
{
    std::vector<int> a1;
    std::vector<int> a2;

    joined_vector<std::vector<int>> jv(a1,a2);

    for (int i = 0; i < 5; i++)
        a1.push_back(i);
    for (int i = 5; i <=10; i++)
        a2.push_back(i);

    for (auto e : jv)
        std::cout << e<<"\n";
    for (int i = 0; i < jv.size(); i++)
        std::cout << jv[i] << "\n";
    return 0;
}

执行:

template<typename _vec>
class joined_vector
{
    _vec& m_vec1;
    _vec& m_vec2;

public:

    struct Iterator
    {
        typedef typename _vec::iterator::value_type type_value;
        typedef typename _vec::iterator::value_type* pointer;
        typedef typename _vec::iterator::value_type& reference;
        typedef std::forward_iterator_tag iterator_category;
        typedef std::ptrdiff_t difference_type;
        _vec* m_vec1;
        _vec* m_vec2;
        Iterator(pointer ptr) :m_ptr(ptr)
        {

        }
        Iterator operator++()
        {
            if (m_vec1->size() > 0 && m_ptr == &(*m_vec1)[m_vec1->size() - 1] && m_vec2->size() != 0)
                m_ptr = &(*m_vec2)[0];
            else
                ++m_ptr;
            return m_ptr;
        }
        Iterator operator++(int)
        {
            pointer curr = m_ptr;
            if (m_vec1->size() > 0 && m_ptr == &(*m_vec1)[m_vec1->size() - 1] && m_vec2->size() != 0)
                m_ptr = &(*m_vec2)[0];
            else
                ++m_ptr;
            return curr;
        }
        reference operator *()
        {
            return *m_ptr;
        }
        pointer operator ->()
        {
            return m_ptr;
        }

        friend bool operator == (Iterator& itr1, Iterator& itr2)
        {
            return itr1.m_ptr == itr2.m_ptr;
        }
        friend bool operator != (Iterator& itr1, Iterator& itr2)
        {
            return itr1.m_ptr != itr2.m_ptr;
        }
    private:
        pointer m_ptr;
    };

    joined_vector(_vec& vec1, _vec& vec2) :m_vec1(vec1), m_vec2(vec2)
    {

    }
    Iterator begin()
    {
        //checkes if m_vec1 is empty and gets the first elemet's address,
        //if it's empty then it get's the first address of the second vector m_vec2
        //if both of them are empty then nullptr is returned as the first pointer
        Iterator itr_beg((m_vec1.size() != 0) ? &m_vec1[0] : ((m_vec2.size() != 0) ? &m_vec2[0] : nullptr));
        itr_beg.m_vec1 = &m_vec1;
        itr_beg.m_vec2 = &m_vec2;
        return itr_beg;
    }
    Iterator end()
    {
        //check if m_vec2 is empty and get the last address of that vector
        //if the second vector is empty then the m_vec1's vector/the first vector's last element's address is taken
        //if both of them are empty then a null pointer is returned as the end pointer
        typename _vec::value_type* p = ((m_vec2.size() != 0) ? &m_vec2[m_vec2.size() - 1] : ((m_vec1.size()) != 0 ? &m_vec1[m_vec1.size() - 1] : nullptr));
        Iterator itr_beg(p != nullptr ? p + 1 : nullptr);
        itr_beg.m_vec1 = &m_vec1;
        itr_beg.m_vec2 = &m_vec2;
        return itr_beg;
    }
    typename _vec::value_type& operator [](int i)
    {
        if (i < m_vec1.size())
            return m_vec1[i];
        else
            return m_vec2[i - m_vec1.size()];
    }
    size_t size()
    {
        return m_vec1.size() + m_vec2.size();
    }

};

1
1201ProgramAlarm

如果您的向量已排序*,请查看 <algorithm> 中的 set_union

set_union(A.begin(), A.end(), B.begin(), B.end(), AB.begin());

链接中有一个更详尽的示例。


此外,它与直接附加不同 - 输出范围内的元素是唯一的,这可能不是 OP 想要的(它们甚至可能无法比较)。这当然不是最有效的方法。
F
Faker

template<typename T>
vector<T> CombineVectors(vector<T> &&a, vector<T> &&b) {
  vector<T> ab = move(a);
  ab.insert(
      ab.end(), make_move_iterator(b.begin()), make_move_iterator(b.end()));
  return ab;
}


template<typename T>
vector<T> CombineVectors(const vector<T> &a, const vector<T> &b) {
  vector<T> ab = a;
  ab.insert(ab.end(), b.begin(), b.end());
  return ab;
}