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 ::?
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.
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.
Si vous pouvez utiliser Boost, essayez 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.
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:
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.)
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.
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
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
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:
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.
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.