web-dev-qa-db-ja.com

「最後のn個のアイテム」を保存する必要がある場合、リストはベクターよりも優れていますか?

常にベクトルを使用する必要があることを示唆する多くの質問がありますが、「最後のn個のアイテム」を保存する必要があるシナリオにはリストの方が良いと思われます

たとえば、最後の5つのアイテムを保存する必要があるとします:反復0:

3,24,51,62,37,

次に、各反復で、インデックス0のアイテムが削除され、新しいアイテムが最後に追加されます。

反復1:

24,51,62,37,8

反復2:

51,62,37,8,12

このユースケースでは、ベクトルの場合、n個のアイテムをコピーする必要があるため、複雑さはO(n)になりますが、リストでは常にO(1)になります。頭、そして各反復の末尾に追加します。

私の理解は正しいですか?これはstd :: listの実際の動作ですか?

43
Kaizer Sozay

どちらでもない。コレクションのサイズは固定されており、std::array 十分なものです。

実装するデータ構造は、リングバッファーと呼ばれます。実装するには、配列を作成し、現在の最初の要素のオフセットを追跡します。

バッファからアイテムをプッシュする要素を追加するとき、つまり最初の要素を削除するとき、オフセットを増分します。

バッファ内の要素を取得するには、インデックスとオフセットを追加し、このモジュロとバッファの長さを取得します。

95
Taemyr

std :: deque は、はるかに優れたオプションです。または、std :: dequeのベンチマークを行い、そのパフォーマンスが特定の用途に不十分であることがわかった場合は、固定サイズの配列に循環バッファーを実装して、バッファーの先頭のインデックスを保存できます。バッファ内の要素を置換する場合、開始インデックスの要素を上書きし、開始インデックスをその前の値にバッファのサイズの1モジュロを加えた値に設定します。

リスト要素はメモリ全体に散在している可能性があるため、リストのトラバースは非常に遅く、メモリの単一ブロックでのメモリ移動は大きなブロックであっても非常に高速であるため、ベクトルシフトは実際に驚くほど高速です。

Meeting C++ 2015カンファレンスの講演 Taming The Performance Beast が興味深いかもしれません。

34
David Scarlett

Boostを使用できる場合は、 boost :: circular_buffer :を試してください。

Boost Circular Buffer

std::listまたはstd::dequeに似た一種のシーケンスです。ランダムアクセス反復子、バッファの先頭または末尾での一定時間の挿入および消去操作、およびstdアルゴリズムとの相互運用性をサポートしています。

固定容量のストレージを提供します。バッファがいっぱいになると、バッファの先頭から新しいデータが書き込まれ、古いデータが上書きされます

// Create a circular buffer with a capacity for 5 integers.
boost::circular_buffer<int> cb(5);

// Insert elements into the buffer.
cb.Push_back(3);
cb.Push_back(24);
cb.Push_back(51);
cb.Push_back(62);
cb.Push_back(37);

int a = cb[0];  // a == 3
int b = cb[1];  // b == 24
int c = cb[2];  // c == 51

// The buffer is full now, so pushing subsequent
// elements will overwrite the front-most elements.
cb.Push_back(8);   // overwrite 3 with 8
cb.Push_back(12);  // overwrite 24 with 12

// The buffer now contains 51, 62, 37, 8, 12.

// Elements can be popped from either the front or the back.
cb.pop_back();  // 12 is removed
cb.pop_front(); // 51 is removed

circular_bufferは、メモリの連続領域に要素を格納します。これにより、要素の高速constant-time挿入、削除、ランダムアクセスが可能になります。


PS ...または Taemyr が示唆するように、直接 循環バッファー を実装します。

オーバーロードジャーナル#50-2002年8月には、堅牢なSTLのような循環バッファーを記述するための 素敵な紹介 (Pete Goodliffe著)があります。

25
manlio

問題は、O(n)はnが無限に向かう傾向にある漸近的な動作についてのみ語るということです。nが小さい場合、関連する定数因子が重要になります。 「ベクターがリストを破らなかったらif然とするでしょう。std::vectorstd::dequeを破ることさえ期待しています。

「最後の500個の整数項目」については、std::vectorstd::listよりも高速であると期待しますが、std::dequeはおそらく勝つでしょう。 「最後の500万個のコピーが遅いアイテム」の場合、std:vectorが最も遅くなります。

std::arrayまたはstd::vectorに基づくリングバッファーは、おそらくでも高速になります。

(ほぼ)常にパフォーマンスの問題があります:

  • 固定インターフェースでカプセル化する
  • そのインターフェースを実装できる最も単純なコードを書く
  • プロファイリングで問題があることがわかった場合は、最適化してください(コードがより複雑になります)。

実際には、std::dequeを使用するか、事前に構築されたリングバッファーがある場合はそれを使用するだけで十分です。 (ただし、プロファイリングで必要とされない限り、リングバッファーを記述する手間をかける価値はありません。)

5
Martin Bonner

最後のN-- elementsを保存する必要がある場合、論理的には何らかのキューまたは循環バッファー、 std :: stack および std :: deque =は [〜#〜] lifo [〜#〜] および [〜#〜] fifo [〜#〜] キューの実装です。

boost :: circular_buffer を使用するか、単純な循環バッファーを手動で実装できます。

template<int Capcity>
class cbuffer
{
public:
    cbuffer() : sz(0), p(0){}
    void Push_back(int n)
    {
        buf[p++] = n;
        if (sz < Capcity)
            sz++;
        if (p >= Capcity)
            p = 0;
    }
    int size() const
    {
        return sz;
    }
    int operator[](int n) const
    {
        assert(n < sz);
        n = p - sz + n;
        if (n < 0)
            n += Capcity;
        return buf[n];
    }
    int buf[Capcity];
    int sz, p;
};

サンプル使用 5つのint要素の循環バッファーの場合:

int main()
{
    cbuffer<5> buf;

    // insert random 100 numbers
    for (int i = 0; i < 100; ++i)
        buf.Push_back(Rand());

    // output to cout contents of the circular buffer
    for (int i = 0; i < buf.size(); ++i)
        cout << buf[i] << ' ';
}

留意点として、要素が5つしかない場合、最適なソリューションは実装が速く、正しく機能するものであることに注意してください。

3
Pavel

最小限の循環バッファーを次に示します。主にここに投稿して、大量のコメントと改善のアイデアを得る。

最小限の実装

#include <iterator>

template<typename Container>
class CircularBuffer
{
public:
    using iterator   = typename Container::iterator;
    using value_type = typename Container::value_type;
private:
    Container _container;
    iterator  _pos;
public:
    CircularBuffer() : _pos(std::begin(_container)) {}
public:
    value_type& operator*() const { return *_pos; }
    CircularBuffer& operator++() { ++_pos ; if (_pos == std::end(_container)) _pos = std::begin(_container); return *this; }
    CircularBuffer& operator--() { if (_pos == std::begin(_container)) _pos = std::end(_container); --_pos; return *this; }
};

使用法

#include <iostream>
#include <array>

int main()
{
    CircularBuffer<std::array<int,5>> buf;

    *buf = 1; ++buf;
    *buf = 2; ++buf;
    *buf = 3; ++buf;
    *buf = 4; ++buf;
    *buf = 5; ++buf;
    std::cout << *buf << " "; ++buf;
    std::cout << *buf << " "; ++buf;
    std::cout << *buf << " "; ++buf;
    std::cout << *buf << " "; ++buf;
    std::cout << *buf << " "; ++buf;
    std::cout << *buf << " "; ++buf;
    std::cout << *buf << " "; ++buf;
    std::cout << *buf << " "; --buf;
    std::cout << *buf << " "; --buf;
    std::cout << *buf << " "; --buf;
    std::cout << *buf << " "; --buf;
    std::cout << *buf << " "; --buf;
    std::cout << *buf << " "; --buf;

    std::cout << std::endl;
}

でコンパイル

g++ -std=c++17 -O2 -Wall -Wextra -pedantic -Werror

デモ

Coliruの場合:オンラインで試してください

3
YSC

ここに、私が少し前に書いたリングバッファベースのデキューテンプレートクラスの始まりがあります。ほとんどがstd::allocator(つまりnotTがデフォルトで構築可能であることを要求します)。現在、イテレーター、またはinsert/remove、コンストラクターのコピー/移動などがないことに注意してください。

#ifndef RING_DEQUEUE_H
#define RING_DEQUEUE_H

#include <memory>
#include <type_traits>
#include <limits>

template <typename T, size_t N>
class ring_dequeue {
private:
    static_assert(N <= std::numeric_limits<size_t>::max() / 2 &&
                  N <= std::numeric_limits<size_t>::max() / sizeof(T),
                  "size of ring_dequeue is too large");

    using alloc_traits = std::allocator_traits<std::allocator<T>>;

public:
    using value_type = T;
    using reference = T&;
    using const_reference = const T&;
    using difference_type = ssize_t;
    using size_type = size_t;

    ring_dequeue() = default;

    // Disable copy and move constructors for now - if iterators are
    // implemented later, then those could be delegated to the InputIterator
    // constructor below (using the std::move_iterator adaptor for the move
    // constructor case).
    ring_dequeue(const ring_dequeue&) = delete;
    ring_dequeue(ring_dequeue&&) = delete;
    ring_dequeue& operator=(const ring_dequeue&) = delete;
    ring_dequeue& operator=(ring_dequeue&&) = delete;

    template <typename InputIterator>
    ring_dequeue(InputIterator begin, InputIterator end) {
        while (m_tailIndex < N && begin != end) {
            alloc_traits::construct(m_alloc, reinterpret_cast<T*>(m_buf) + m_tailIndex,
                                    *begin);
            ++m_tailIndex;
            ++begin;
        }
        if (begin != end)
            throw std::logic_error("Input range too long");
    }

    ring_dequeue(std::initializer_list<T> il) :
        ring_dequeue(il.begin(), il.end()) { }

    ~ring_dequeue() noexcept(std::is_nothrow_destructible<T>::value) {
        while (m_headIndex < m_tailIndex) {
            alloc_traits::destroy(m_alloc, elemPtr(m_headIndex));
            m_headIndex++;
        }
    }

    size_t size() const {
        return m_tailIndex - m_headIndex;
    }
    size_t max_size() const {
        return N;
    }

    bool empty() const {
        return m_headIndex == m_tailIndex;
    }
    bool full() const {
        return m_headIndex + N == m_tailIndex;
    }

    template <typename... Args>
    void emplace_front(Args&&... args) {
        if (full())
            throw std::logic_error("ring_dequeue full");
        bool wasAtZero = (m_headIndex == 0);
        auto newHeadIndex = wasAtZero ? (N - 1) : (m_headIndex - 1);
        alloc_traits::construct(m_alloc, elemPtr(newHeadIndex),
                                std::forward<Args>(args)...);
        m_headIndex = newHeadIndex;
        if (wasAtZero)
            m_tailIndex += N;
    }
    void Push_front(const T& x) {
        emplace_front(x);
    }
    void Push_front(T&& x) {
        emplace_front(std::move(x));
    }

    template <typename... Args>
    void emplace_back(Args&&... args) {
        if (full())
            throw std::logic_error("ring_dequeue full");
        alloc_traits::construct(m_alloc, elemPtr(m_tailIndex),
                                std::forward<Args>(args)...);
        ++m_tailIndex;
    }
    void Push_back(const T& x) {
        emplace_back(x);
    }
    void Push_back(T&& x) {
        emplace_back(std::move(x));
    }

    T& front() {
        if (empty())
            throw std::logic_error("ring_dequeue empty");
        return *elemPtr(m_headIndex);
    }
    const T& front() const {
        if (empty())
            throw std::logic_error("ring_dequeue empty");
        return *elemPtr(m_headIndex);
    }
    void remove_front() {
        if (empty())
            throw std::logic_error("ring_dequeue empty");
        alloc_traits::destroy(m_alloc, elemPtr(m_headIndex));
        ++m_headIndex;
        if (m_headIndex == N) {
            m_headIndex = 0;
            m_tailIndex -= N;
        }
    }
    T pop_front() {
        T result = std::move(front());
        remove_front();
        return result;
    }

    T& back() {
        if (empty())
            throw std::logic_error("ring_dequeue empty");
        return *elemPtr(m_tailIndex - 1);
    }
    const T& back() const {
        if (empty())
            throw std::logic_error("ring_dequeue empty");
        return *elemPtr(m_tailIndex - 1);
    }
    void remove_back() {
        if (empty())
            throw std::logic_error("ring_dequeue empty");
        alloc_traits::destroy(m_alloc, elemPtr(m_tailIndex - 1));
        --m_tailIndex;
    }
    T pop_back() {
        T result = std::move(back());
        remove_back();
        return result;
    }

private:
    alignas(T) char m_buf[N * sizeof(T)];
    size_t m_headIndex = 0;
    size_t m_tailIndex = 0;
    std::allocator<T> m_alloc;

    const T* elemPtr(size_t index) const {
        if (index >= N)
            index -= N;
        return reinterpret_cast<const T*>(m_buf) + index;
    }
    T* elemPtr(size_t index) {
        if (index >= N)
            index -= N;
        return reinterpret_cast<T*>(m_buf) + index;
    }
};

#endif
2
Daniel Schepler

はい。端から要素を削除するためのstd :: vectorの時間の複雑さは線形です。 std :: dequeは、リストの最初と最後に一定時間の挿入と削除を提供し、std :: listよりもパフォーマンスが良いため、あなたがしていることに適しているかもしれません

ソース:

http://www.sgi.com/tech/stl/Vector.html

http://www.sgi.com/tech/stl/Deque.html

2
codelyzer

簡単に言うと、std::vectorはメモリのサイズを変更しないのに適しています。あなたの場合、すべてのデータを前方に移動するか、ベクトルに新しいデータを追加する場合、それは無駄になります。@ Davidが言ったように、std::dequepop_headPush_backを使用するため、たとえば双方向リスト。

cplus cplus reference リストについて

他の基本標準シーケンスコンテナ(配列、ベクトル、および両端キュー)と比較して、リストは、コンテナ内の任意の位置に要素を挿入、抽出、および移動する際に全般的に実行イテレータが既に取得されているため、ソートアルゴリズムなど、これらを集中的に使用するアルゴリズムでも取得されています。

これらの他のシーケンスコンテナと比較したリストとforward_listの主なdrawbackは、位置によって要素に直接アクセスできないことです。たとえば、リストの6番目の要素にアクセスするには、既知の位置(開始または終了など)からその位置まで反復する必要があります。また、各要素に関連付けられているリンク情報を保持するために余分なメモリを消費します(これは、小さなサイズの要素の大きなリストにとって重要な要素になる可能性があります)。

deque

開始または終了以外の位置で要素を頻繁に挿入または削除する操作の場合、両端キューのパフォーマンスは低下し、反復子と参照の一貫性はリストや前方リストよりも低くなります。

ベクトル

したがって、配列と比較して、ベクトルはストレージを管理し、効率的な方法で動的に成長する能力と引き換えに、より多くのメモリを消費します。

他の動的シーケンスコンテナ(deque、listsおよびforward_lists)と比較して、ベクターはその要素へのアクセスが非常に効率的で(配列と同様)、その要素の要素の追加または削除が比較的効率的です。終了以外の位置で要素を挿入または削除する操作の場合、それらは他の要素よりもパフォーマンスが低下し、listsおよびforward_listsよりも一貫性のない反復子と参照を持ちます。

1
Shihe Zhang