web-dev-qa-db-fra.com

La liste est-elle meilleure que le vecteur lorsque nous devons stocker "les n derniers éléments"?

Il y a beaucoup de questions qui suggèrent que l'on devrait toujours utiliser un vecteur, mais il me semble qu'une liste serait meilleure pour le scénario, où nous devons stocker "les n derniers éléments"

Par exemple, disons que nous devons stocker les 5 derniers éléments vus: Itération 0:

3,24,51,62,37,

Ensuite, à chaque itération, l'élément à l'index 0 est supprimé et le nouvel élément est ajouté à la fin:

Itération 1:

24,51,62,37,8

Itération 2:

51,62,37,8,12

Il semble que pour ce cas d'utilisation, pour un vecteur, la complexité sera O (n), car nous aurions à copier n éléments, mais dans une liste, cela devrait être O (1), car nous coupons toujours le tête, et en ajoutant à la queue chaque itération.

Ma compréhension est-elle correcte? Est-ce le comportement réel d'une liste std ::?

43
Kaizer Sozay

Ni. Votre collection a une taille fixe et std::array est suffisant.

La structure de données que vous implémentez est appelée tampon en anneau. Pour l'implémenter, vous créez un tableau et gardez une trace du décalage du premier élément actuel.

Lorsque vous ajoutez un élément qui pousserait un élément hors du tampon - c'est-à-dire lorsque vous supprimez le premier élément - vous augmentez le décalage.

Pour récupérer des éléments dans le tampon, vous ajoutez l'index et l'offset et prenez le module de celui-ci et la longueur du tampon.

95
Taemyr

std :: deque est une bien meilleure option. Ou si vous aviez comparé std :: deque et trouvé ses performances inadéquates pour votre utilisation spécifique, vous pourriez implémenter un tampon circulaire dans un tableau de taille fixe, stockant l'index du début du tampon. Lors du remplacement d'un élément dans le tampon, vous écraseriez l'élément à l'index de démarrage, puis définiriez l'index de démarrage à sa valeur précédente plus un module de la taille du tampon.

La traversée de liste est très lente, car les éléments de liste peuvent être dispersés dans la mémoire, et le décalage vectoriel est en fait étonnamment rapide, car la mémoire se déplace sur un seul bloc de mémoire est assez rapide même s'il s'agit d'un grand bloc.

La conférence Taming The Performance Beast de la conférence Meeting C++ 2015 pourrait vous intéresser.

34
David Scarlett

Si vous pouvez utiliser Boost, essayez boost :: circular_buffer :

Boost Circular Buffer

C'est une sorte de séquence similaire à std::list ou std::deque. Il prend en charge les itérateurs d'accès aléatoire, les opérations d'insertion et d'effacement à temps constant au début ou à la fin de la mémoire tampon et l'interopérabilité avec les algorithmes std.

Il offre un stockage de capacité fixe: lorsque le tampon est rempli, de nouvelles données sont écrites en commençant au début du tampon et en remplaçant l'ancien

// 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

Le circular_buffer stocke ses éléments dans une région contiguë de la mémoire, ce qui permet ensuite l'insertion, la suppression et l'accès aléatoire rapides à temps constant des éléments.


PS ... ou implémentez le tampon circulaire directement comme suggéré par Taemyr .

Overload Journal # 50 - Aug 2002 a Nice introduction (par Pete Goodliffe) pour écrire un tampon circulaire robuste de type STL.

25
manlio

Le problème est que O(n) ne parle que du comportement asymptotique lorsque n tend vers l'infini. Si n est petit, les facteurs constants impliqués deviennent significatifs. Le résultat est que pour "les 5 derniers entiers items "Je serais stupéfait si le vecteur ne battait pas la liste. Je m'attendrais même à std::vector battre std::deque.

Pour les "500 derniers éléments entiers", je m'attendrais toujours à std::vector pour être plus rapide que std::list - mais std::deque gagnerait probablement maintenant. Pour "les 5 derniers millions d'éléments à copie lente", std:vector serait le plus lent de tous.

Un tampon en anneau basé sur std::array ou std::vector serait probablement encore plus rapide.

Comme (presque) toujours avec des problèmes de performances:

  • encapsuler avec une interface fixe
  • écrire le code le plus simple qui puisse implémenter cette interface
  • si le profilage montre que vous avez un problème, optimisez (ce qui rendra le code plus compliqué).

En pratique, il suffit d'utiliser un std::deque, ou un tampon en anneau prédéfini si vous en avez un, sera suffisant. (Mais cela ne vaut pas la peine d'écrire un tampon en anneau à moins que le profilage ne le stipule.)

5
Martin Bonner

Si vous avez besoin de stocker les derniers éléments N-, vous faites logiquement une sorte de file d'attente ou un tampon circulaire, std :: stack et std :: deque = sont des implémentations des files d'attente LIFO et FIFO .

Vous pouvez utiliser boost :: circular_buffer ou implémenter manuellement un tampon circulaire simple:

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;
};

Exemple d'utilisation pour le tampon circulaire de 5 éléments 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] << ' ';
}

Notez que lorsque vous n'avez que 5 éléments, la meilleure solution est celle qui est rapide à mettre en œuvre et qui fonctionne correctement.

3
Pavel

Voici un tampon circulaire minimal. Je poste principalement cela ici pour obtenir une tonne de commentaires et d'idées d'amélioration.

Mise en œuvre minimale

#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; }
};

Utilisation

#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;
}

Compiler avec

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

Démo

Sur Coliru: essayez-le en ligne

3
YSC

Voici les débuts d'une classe de modèle de retrait de file d'attente basée sur un tampon en anneau que j'ai écrite il y a un certain temps, principalement pour expérimenter l'utilisation de std::allocator (donc pas nécessite que T soit constructible par défaut). Notez qu'il n'a actuellement pas d'itérateurs, ni insert/remove, des constructeurs de copie/déplacement, etc.

#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

Oui. La complexité temporelle du vecteur std :: pour supprimer les éléments de la fin est linéaire. std :: deque peut être un bon choix pour ce que vous faites car il offre une insertion et une suppression à temps constant au début ainsi qu'à la fin de la liste et également de meilleures performances que std :: list

La source:

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

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

2
codelyzer

Dites brièvement le std::vector est préférable pour une taille de mémoire inchangée. Dans votre cas, si vous déplacez toutes les données vers l'avant ou ajoutez de nouvelles données dans un vecteur, cela doit être un gaspillage. Comme @David l'a dit, le std::deque est une bonne option, car vous feriez pop_head et Push_back par exemple. liste bidirectionnelle.

de la référence cplus cplus à propos de la liste

Par rapport aux autres conteneurs de séquence standard de base (tableau, vecteur et deque), les listes fonctionnent généralement mieux pour insérer, extraire et déplacer des éléments dans n'importe quelle position dans le conteneur pour lequel un itérateur a déjà été obtenu, et donc aussi dans des algorithmes qui en font un usage intensif, comme les algorithmes de tri.

Le principal inconvénient des listes et des forward_lists par rapport à ces autres conteneurs de séquence est qu'ils n'ont pas d'accès direct aux éléments par leur position; Par exemple, pour accéder au sixième élément d'une liste, il faut itérer d'une position connue (comme le début ou la fin) à cette position, ce qui prend un temps linéaire dans la distance entre ceux-ci. Ils consomment également de la mémoire supplémentaire pour conserver les informations de liaison associées à chaque élément (ce qui peut être un facteur important pour les grandes listes d'éléments de petite taille).

à propos de deque

Pour les opérations qui impliquent l'insertion ou la suppression fréquentes d'éléments à des positions autres que le début ou la fin, les deques fonctionnent moins bien et ont des itérateurs et des références moins cohérents que les listes et les listes de transfert.

vétérinaire

Par conséquent, par rapport aux baies, les vecteurs consomment plus de mémoire en échange de la capacité de gérer le stockage et de se développer dynamiquement de manière efficace.

Par rapport aux autres conteneurs de séquence dynamique (deques, lists et forward_lists), les vecteurs sont très efficaces pour accéder à ses éléments (tout comme les tableaux) et relativement efficaces pour ajouter ou supprimer des éléments de sa fin. Pour les opérations qui impliquent l'insertion ou la suppression d'éléments à des positions autres que la fin, elles fonctionnent moins bien que les autres, et ont des itérateurs et des références moins cohérents que les listes et les listes_en avant.

1
Shihe Zhang