Подтвердить что ты не робот

Является ли список лучше, чем вектор, когда нам нужно хранить "последние n элементов"?

Есть много вопросов, которые предполагают, что всегда нужно использовать вектор, но мне кажется, что список будет лучше для сценария, где нам нужно сохранить "последние n элементов"

Например, скажем, нам нужно сохранить последние 5 предметов: Итерация 0:

3,24,51,62,37,

Затем на каждой итерации элемент в индексе 0 удаляется, а новый элемент добавляется в конце:

Итерация 1:

24,51,62,37,8

Итерация 2:

51,62,37,8,12

Кажется, что для этого случая использования для вектора сложность будет O (n), так как нам пришлось бы копировать n элементов, но в списке это должно быть O (1), так как мы всегда просто отрубая голову и добавляя к хвосту каждую итерацию.

Правильно ли я понимаю? Это фактическое поведение std:: list?

4b9b3361

Ответ 1

Ни. Ваша коллекция имеет фиксированный размер и std::array достаточно.

Структура данных, которую вы реализуете, называется кольцевым буфером. Для его реализации вы создаете массив и отслеживаете смещение текущего первого элемента.

Когда вы добавляете элемент, который выталкивает элемент из буфера - т.е. когда вы удаляете первый элемент - вы увеличиваете смещение.

Для извлечения элементов в буфере вы добавляете индекс и смещение и принимаете по модулю это и длину буфера.

Ответ 2

std:: deque - гораздо лучший вариант. Или, если у вас есть benchmarked std:: deque и он считает, что его производительность неадекватна для вашего конкретного использования, вы можете реализовать циклический буфер в массиве фиксированного размера, сохраняя индекс начала буфера. При замене элемента в буфере вы должны перезаписать элемент в начальном индексе, а затем установить начальный индекс в его предыдущее значение плюс один по модулю размер буфера.

Обход списка очень медленный, поскольку элементы списка могут быть разбросаны по всей памяти, а векторное смещение на самом деле удивительно быстро, так как память перемещается по одному блоку памяти довольно быстро, даже если это большой блок.

Обсуждение Приручение зверя производительности на конференции Meeting С++ 2015 может вас заинтересовать.

Ответ 3

Если вы можете использовать Boost, попробуйте boost:: circle_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 сохраняет свои элементы в смежной области памяти, что позволяет быстро вставлять, удалять и произвольный доступ к элементам < <времени > .


PS... или внесите круговой буфер непосредственно, как предложено Taemyr.

Журнал перегрузки # 50 - август 2002 года имеет приятное введение (Питом Гудлиффом) для написания надежного кольцевого буфера STL.

Ответ 4

Проблема состоит в том, что O (n) говорит только об асимптотическом поведении, когда n стремится к бесконечности. Если n мало, то постоянные факторы становятся значительными. Результат состоит в том, что для "последних 5 целых элементов" я был бы ошеломлен, если бы вектор не разбил список. Я даже ожидал, что std::vector будет бить std::deque.

Для "последних 500 целых элементов" я бы ожидал, что std::vector будет быстрее, чем std::list, но теперь, вероятно, победит std::deque. Для "последних 5 миллионов элементов с медленным копированием" std:vector будет самым медленным из всех.

Буферный буфер на основе std::array или std::vector, вероятно, будет быстрее, хотя.

Как (почти) всегда с проблемами производительности:

  • инкапсулировать с помощью фиксированного интерфейса
  • напишите простейший код, который может реализовать этот интерфейс
  • Если профилирование показывает, что у вас есть проблема, оптимизируйте (что сделает код более сложным).

На практике просто использовать std::deque или предварительно построенный кольцевой буфер, если он у вас есть, будет достаточно хорошим. (Но не стоит беспокоиться о написании кольцевого буфера, если профилирование не говорит о необходимости.)

Ответ 5

Если вам нужно сохранить последние N -элементы, то логически вы делаете какую-то очередь или круговой буфер, std:: stack и std:: deque являются реализациями LIFO и FIFO.

Вы можете использовать boost:: circle_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 элементов, лучшим решением является тот, который быстро реализуется и работает правильно.

Ответ 6

Вот минимальный круговой буфер. Я в первую очередь публикую это здесь, чтобы получить тонну комментариев и идей улучшения.

Минимальная реализация

#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

Demo

On Coliru: попробуйте онлайн

Ответ 7

Да. Временная сложность std::vector для удаления элементов с конца линейна. std:: deque может быть хорошим выбором для того, что вы делаете, поскольку он предлагает постоянную вставку и удаление времени в начале, а также в конце списка, а также лучшую производительность, чем std:: list

Источник:

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

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

Ответ 8

Вот начало класса шаблона dequeue, основанного на кольцевом буфере, который я написал некоторое время назад, в основном для экспериментов с использованием std::allocator (поэтому для конструктора по умолчанию не требуется T). Обратите внимание, что в настоящее время он не имеет итераторов, или 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

Ответ 9

Вкратце скажем, что std::vector лучше для неизменяемого размера памяти. В вашем случае, если вы перемещаете все данные вперед или добавляете новые данные в вектор, это должно быть ненужным. Как @David сказал, что std::deque является хорошим вариантом, так как вы бы pop_head и push_back например. двухсторонний список.

из cplus cplus reference о списке

По сравнению с другими базовыми стандартными контейнерами последовательности (массив, вектор и deque), списки выполняют в целом лучше при вставке, извлечении и движущихся элементов в любом положении внутри контейнера, для которого итератор уже получен, а следовательно, и в алгоритмах которые интенсивно используют их, например алгоритмы сортировки.

Основной недостаток списков и forward_lists по сравнению с другими контейнеры последовательности - это то, что они не имеют прямого доступа к элементам посредством их положение; Например, чтобы получить доступ к шестому элементу в списке, нужно перебирать из известной позиции (например, начало или конец) в это положение, которое принимает линейное время на расстоянии между эти. Они также потребляют некоторую дополнительную память, чтобы поддерживать связь информация, связанная с каждым элементом (которая может быть важной фактор для больших списков малогабаритных элементов).

about deque

Для операций, которые включают частую вставку или удаление элементов в положениях, отличных от начала или конца, и имеют менее согласованные итераторы и ссылки, чем списки и пересылаемые списки.

vetor

Поэтому, по сравнению с массивами, векторы потребляют больше памяти в обмене за способность управлять хранилищем и динамически расти в эффективном путь.

По сравнению с другими контейнерами динамической последовательности (deques, lists и forward_lists), векторы очень эффективны для доступа к своим элементам (как и массивы) и относительно эффективное добавление или удаление элементов с ее конца. Для операций, связанных с вставкой или удаляя элементы в других местах, кроме конца, они выполняют хуже чем другие, и имеют менее последовательные итераторы и ссылки чем списки и forward_lists.

Ответ 10

Я думаю, что даже использование std:: deque также имеет накладные расходы на элементы копирования в определенном состоянии, потому что std:: deque - это карта массивов, поэтому std:: list - хорошая идея для устранения накладных расходов на копию.

Чтобы увеличить производительность траверса для std:: list, вы можете реализовать пул памяти, чтобы std:: list выделил память из туловища и ее пространственную локальность для кеширования.