Patrzyłem na kontenery STL i próbowałem zorientować się, jakie one są naprawdę (tj. Zastosowana struktura danych), a deque mnie zatrzymał: na początku pomyślałem, że to podwójnie połączona lista, która umożliwi wstawianie i usuwanie z obu końców w stały czas, ale niepokoi mnie obietnica złożona przez operatora [], która ma być wykonana w stałym czasie. Na połączonej liście arbitralny dostęp powinien być O (n), prawda?
A jeśli jest to tablica dynamiczna, jak może dodawać elementy w stałym czasie? Należy wspomnieć, że może nastąpić realokacja i że O (1) jest zamortyzowanym kosztem, jak w przypadku wektora .
Zastanawiam się więc, jaka jest ta struktura, która umożliwia swobodny dostęp w stałym czasie, a jednocześnie nigdy nie musi być przenoszona w nowe, większe miejsce.
deque
oznacza podwójnie zakończoną kolejkę , chociaż oczywiście rygorystyczny wymóg dostępu O (1) do środkowych elementów jest szczególny dla C ++Odpowiedzi:
Deque jest nieco rekurencyjnie zdefiniowany: wewnętrznie utrzymuje podwójną kolejkę fragmentów o stałym rozmiarze. Każdy fragment jest wektorem, a sama kolejka („mapa” na poniższej grafice) fragmentów jest również wektorem.
Jest tam świetna analiza charakterystyk i jak to porównać do
vector
nad na CodeProject .Standardowa implementacja biblioteki GCC wewnętrznie używa
T**
do reprezentowania mapy. Każdy blok danychT*
ma przydzielony określony rozmiar__deque_buf_size
(zależny odsizeof(T)
).źródło
Wyobraź to sobie jako wektor wektorów. Tylko, że nie są standardowymi
std::vector
.Wektor zewnętrzny zawiera wskaźniki do wektorów wewnętrznych. Kiedy jego pojemność zostanie zmieniona poprzez realokację, zamiast przydzielać całą pustą przestrzeń do końca, jak to
std::vector
robi, dzieli pustą przestrzeń na równe części na początku i na końcu wektora. To pozwalapush_front
ipush_back
na tym wektorze występować zarówno w zamortyzowanym czasie O (1).Zachowanie wektora wewnętrznego musi się zmieniać w zależności od tego, czy znajduje się z przodu, czy z tyłu
deque
. Z tyłu może zachowywać się normalnie,std::vector
gdy rośnie na końcu ipush_back
występuje w czasie O (1). Z przodu musi robić coś przeciwnego, rosnąc na początku z każdympush_front
. W praktyce można to łatwo osiągnąć, dodając wskaźnik do elementu przedniego i kierunek wzrostu wraz z rozmiarem. Dzięki tej prostej modyfikacjipush_front
można również ustawić czas O (1).Dostęp do dowolnego elementu wymaga przesunięcia i podziału na odpowiedni indeks wektora zewnętrznego, który występuje w O (1), i indeksowania do wektora wewnętrznego, który jest również O (1). Zakłada się, że wszystkie wektory wewnętrzne mają stały rozmiar, z wyjątkiem tych na początku lub na końcu
deque
.źródło
Pojemnik, który może rosnąć w dowolnym kierunku.
Deque jest zazwyczaj realizowane jako
vector
ovectors
(wykaz wektorów nie może dać stały czas dostępu losowego). Podczas gdy rozmiar wektorów wtórnych zależy od implementacji, powszechnym algorytmem jest stosowanie stałego rozmiaru w bajtach.źródło
array
o niczym anivector
o niczym, co mogłoby obiecać zamortyzowaneO(1)
push_front. Przynajmniej wewnętrzna z tych dwóch struktur musi mieć zdolnośćO(1)
push_front, której nie gwarantuje aniarray
ani anivector
.vector
tego nie robi, ale jest to wystarczająco prosta modyfikacja, aby to zrobić.(To jest odpowiedź, którą podałem w innym wątku . Zasadniczo twierdzę, że nawet dość naiwne implementacje, używając jednego
vector
, są zgodne z wymogami „ciągłego niezamortyzowanego pchnięcia {przód, tył}”. Możesz być zaskoczony i myślę, że jest to niemożliwe, ale znalazłem inne istotne cytaty w standardzie, które definiują kontekst w zaskakujący sposób. Proszę o wyrozumiałość; jeśli popełniłem błąd w tej odpowiedzi, bardzo pomocne byłoby określenie, które rzeczy Powiedziałem poprawnie i gdzie moja logika się zepsuła).W tej odpowiedzi nie próbuję zidentyfikować dobrej implementacji, staram się jedynie pomóc w interpretacji wymagań dotyczących złożoności standardu C ++. Cytuję z N3242 , co według Wikipedii jest najnowszym dokumentem normalizacyjnym C ++ 11. (Wygląda na to, że jest zorganizowany inaczej niż w ostatecznym standardzie, dlatego nie podam dokładnych numerów stron. Oczywiście reguły te mogły ulec zmianie w ostatecznym standardzie, ale nie sądzę, żeby tak się stało).
A
deque<T>
może być poprawnie zaimplementowany za pomocąvector<T*>
. Wszystkie elementy są kopiowane na stos, a wskaźniki są przechowywane w wektorze. (Więcej o wektorze później).Dlaczego
T*
zamiastT
? Ponieważ standard tego wymaga(mój nacisk). The
T*
Pomaga zaspokoić to. Pomaga nam to również spełnić:Teraz trochę (kontrowersyjny) bit. Po co używać a
vector
do przechowywaniaT*
? Daje nam losowy dostęp, co jest dobrym początkiem. Zapomnijmy na chwilę o złożoności wektora i starannie do tego budujmy:Standard mówi o „liczbie operacji na zawartych obiektach”. Dla
deque::push_front
to wyraźnie 1 bo dokładnie jedenT
obiekt jest zbudowany i zero istniejącychT
obiektów są odczytywane lub zeskanowane w jakikolwiek sposób. Ta liczba, 1, jest wyraźnie stała i jest niezależna od liczby obiektów znajdujących się obecnie w deque. To pozwala nam powiedzieć, że:'Dla naszych
deque::push_front
liczba operacji na zawartych obiektach (Ts) jest stała i jest niezależna od liczby obiektów już znajdujących się w deque”.Oczywiście liczba operacji na
T*
nie będzie tak dobrze wychowana. Gdy stanie sięvector<T*>
zbyt duży, zostanie przeniesiony i wiele innychT*
nich zostanie skopiowanych. Więc tak, liczba operacji naT*
będzie się bardzo różnić, aleT
nie wpłynie to na liczbę operacji na .Dlaczego zależy nam na tym rozróżnieniu między liczeniem operacji
T
a liczeniem operacjiT*
? Jest tak, ponieważ standard mówi:Dla
deque
, zawarte obiekty sąT
, a nieT*
, co oznacza, że możemy zignorować każdą operację, którą kopiuje (lub ponownie przydziela) aT*
.Nie powiedziałem wiele o tym, jak wektor zachowuje się w deque. Być może interpretowalibyśmy go jako bufor kołowy (wektor zawsze przyjmuje maksimum
capacity()
, a następnie przestawia wszystko do większego bufora, gdy wektor jest pełny. Szczegóły nie mają znaczenia.W ostatnich kilku akapitach przeanalizowaliśmy
deque::push_front
i związek między liczbą obiektów w deque już a liczbą operacji wykonanych przez push_front na zawartychT
obiektach. I stwierdziliśmy, że byli od siebie niezależni. Ponieważ standard wymaga, aby złożoność dotyczyła operacji naT
, więc możemy powiedzieć, że ma ona stałą złożoność.Tak, złożoność Operacji On-T * jest amortyzowana (z powodu
vector
), ale interesuje nas tylko złożoność Operacje On-T i jest ona stała (nie amortyzowana).Złożoność vector :: push_back lub vector :: push_front nie ma znaczenia w tej implementacji; względy te dotyczą operacji,
T*
a zatem są nieistotne. Gdyby norma odnosiła się do „konwencjonalnego” teoretycznego pojęcia złożoności, wówczas nie ograniczyliby się wprost do „liczby operacji na zawartych obiektach”. Czy interpretuję to zdanie?źródło
list
niezależnie od aktualnego rozmiaru listy, jest nierealistyczne ; jeśli lista jest zbyt duża, alokacja będzie powolna lub zakończy się niepowodzeniem. Dlatego, o ile widzę, komitet podjął decyzję o sprecyzowaniu jedynie operacji, które można obiektywnie policzyć i zmierzyć. (PS: Mam inną teorię na inną odpowiedź.)O(n)
, że liczba operacji jest asymptotycznie proporcjonalna do liczby elementów. IE, liczą się metaoperacje. W przeciwnym razie nie ma sensu ograniczać wyszukiwania doO(1)
. Ergo, listy połączone nie kwalifikują się.list
można również zaimplementować a jakovector
wskaźnik (wstawienie w środku spowoduje wywołanie pojedynczej kopii konstruktora, niezależnie od wielkości listy, aO(N)
przetasowanie wskaźników można zignorować, ponieważ nie są operacjami na T).deque
ten sposób i (2) „oszukuje” w ten sposób (nawet jeśli jest to dozwolone przez standard), gdy obliczanie złożoności algorytmicznej nie jest pomocne w pisaniu wydajnych programów .Z przeglądu możesz myśleć
deque
jakodouble-ended queue
Dane w
deque
są przechowywane przez fragmenty wektora o stałym rozmiarze, które sąwskazywany przez
map
(który jest również kawałkiem wektora, ale jego rozmiar może się zmienić)Główny kod części
deque iterator
jest następujący:Główny kod części
deque
jest następujący:Poniżej dam ci podstawowy kod
deque
, głównie o trzech częściach:iterator
Jak zbudować
deque
1. iterator (
__deque_iterator
)Głównym problemem iteratora jest to, że gdy ++, - iterator może przejść do innej porcji (jeśli jest wskaźnikiem do krawędzi porcji). Na przykład, istnieją trzy kawałki danych:
chunk 1
,chunk 2
,chunk 3
.Te
pointer1
wskaźniki do pocz? Tkuchunk 2
, gdy operator--pointer
będzie to wskaźnik do końcachunk 1
, tak jak dopointer2
.Poniżej podam główną funkcję
__deque_iterator
:Po pierwsze, przejdź do dowolnego fragmentu:
Zauważ, że
chunk_size()
funkcja, która oblicza wielkość porcji, możesz pomyśleć, że zwraca 8 dla uproszczenia tutaj.operator*
pobierz dane do porcjioperator++, --
// przedrostek formy przyrostu
iterator pomiń n kroków / dostęp losowy2. Jak zbudować
deque
wspólna funkcja
deque
Załóżmy, że
i_deque
ma 20 elementów int,0~19
których wielkość porcji wynosi 8, a teraz push_back 3 elementy (0, 1, 2) doi_deque
:To wewnętrzna struktura, jak poniżej:
Następnie push_back ponownie, wywoła przydzielanie nowej porcji:
Jeśli my
push_front
, przydzielimy nową porcję przed poprzedniąstart
Zauważ, że gdy
push_back
element w deque, wszystkie mapy i fragmenty są wypełnione, spowoduje to przydzielenie nowej mapy i dostosowanie fragmentów, ale powyższy kod może być wystarczający do zrozumieniadeque
.źródło
Czytałem „Struktury danych i algorytmy w C ++” Adama Drozdka i uznałem to za przydatne. HTH.
W środku widać tablicę wskaźników do danych (fragmenty po prawej), a także można zauważyć, że tablica w środku zmienia się dynamicznie.
Obraz jest wart tysiąca słów.
źródło
deque
część i jest całkiem dobra.Chociaż standard nie nakazuje żadnej konkretnej implementacji (tylko losowy dostęp w stałym czasie), deque jest zwykle implementowany jako zbiór ciągłych „stron” pamięci. Nowe strony są przydzielane w razie potrzeby, ale nadal masz dostęp losowy. W przeciwieństwie do tego
std::vector
, nie obiecuje się, że dane są przechowywane w sposób ciągły, ale podobnie jak wektor, wstawki w środku wymagają dużo przeniesienia.źródło
insert
wymaga dużo relokacji w jaki sposób eksperyment 4 tutaj pokazać zdumiewające różnicę międzyvector::insert()
adeque::insert()
?