Ich habe kürzlich untersucht, wie verschiedene Datenstrukturen in Python implementiert sind, um meinen Code effizienter zu gestalten. Bei der Untersuchung der Funktionsweise von Listen und Deques stellte ich fest, dass ich Vorteile erzielen kann, wenn ich die Zeit von O (n) in Listen auf O (1) in Deques verschieben und verschieben möchte (Listen werden als Arrays mit fester Länge implementiert) jedes Mal, wenn etwas vorne eingefügt wird, vollständig kopiert werden usw.). Was ich anscheinend nicht finden kann, sind die Besonderheiten der Implementierung einer Deque und die Besonderheiten ihrer Nachteile gegenüber Listen. Kann mich jemand über diese beiden Fragen aufklären?
85
.append()
und.pop()
amortisiert werden (Neuzuweisung und Kopieren erfolgen, jedoch sehr selten und nur bis Sie die maximale Größe des Stapels erreichen jemals haben).deque
definitiv der richtige Weg.deque
s in CPython auch nicht wirklich um die Thread-Sicherheit. Sie profitieren nur davon, dass die GIL ihre Operationen atomar macht (und tatsächlichappend
undpop
am Ende von alist
den gleichen Schutz hat). In der Praxis sind , wenn Sie nur einen Stapel mit, beidelist
unddeque
effektiv identische Leistung in CPython haben; Die Blockzuweisungen sind häufiger mitdeque
(aber nicht einfach verknüpfte Listen häufig; Sie würden nur jedes Mal zuweisen / freigeben, wenn Sie eine 64-Mitglieder-Grenze in der CPython-Implementierung überschritten haben), aber das Fehlen großer intermittierender Kopien gleicht dies aus.Auschecken
collections.deque
. Aus den Dokumenten:Wie bereits erwähnt, führt die Verwendung von pop (0) oder insert (0, v) zu hohen Strafen bei Listenobjekten. Sie können keine Slice / Index-Operationen für a verwenden
deque
, aber Sie könnenpopleft
/ verwendenappendleft
, für die Operationendeque
optimiert sind. Hier ist ein einfacher Benchmark, um dies zu demonstrieren:import time from collections import deque num = 100000 def append(c): for i in range(num): c.append(i) def appendleft(c): if isinstance(c, deque): for i in range(num): c.appendleft(i) else: for i in range(num): c.insert(0, i) def pop(c): for i in range(num): c.pop() def popleft(c): if isinstance(c, deque): for i in range(num): c.popleft() else: for i in range(num): c.pop(0) for container in [deque, list]: for operation in [append, appendleft, pop, popleft]: c = container(range(num)) start = time.time() operation(c) elapsed = time.time() - start print "Completed %s/%s in %.2f seconds: %.1f ops/sec" % (container.__name__, operation.__name__, elapsed, num / elapsed)
Ergebnisse auf meiner Maschine:
Completed deque/append in 0.02 seconds: 5582877.2 ops/sec Completed deque/appendleft in 0.02 seconds: 6406549.7 ops/sec Completed deque/pop in 0.01 seconds: 7146417.7 ops/sec Completed deque/popleft in 0.01 seconds: 7271174.0 ops/sec Completed list/append in 0.01 seconds: 6761407.6 ops/sec Completed list/appendleft in 16.55 seconds: 6042.7 ops/sec Completed list/pop in 0.02 seconds: 4394057.9 ops/sec Completed list/popleft in 3.23 seconds: 30983.3 ops/sec
quelle
list
Anhänge etwas schneller sind alsdeque
Anhänge.deque
genau wie ein Index in ein indizieren könnenlist
.list
pop
s langsamer alsdeque
die s (wahrscheinlich aufgrund derlist
höheren Kosten für die zeitweise Größenänderung, wenn sie verkleinert werden, wobeideque
nur Blöcke zurück in die freie Liste oder den kleinen Objektpool freigegeben werden), also bei der Auswahl einer Datenstruktur für ein Stapel (auch bekannt als LIFO - Warteschlange), die leer zu voll zu leer Leistung sieht etwas besserdeque
(Mittelwert 6365K ops / sec fürappend
/pop
, vs.list
‚s 5578K ops / sec). Ich vermute,deque
dass es in der realen Welt etwas besser laufen würde, dadeque
die freie Liste bedeutet, dass das erste Wachstum teurer ist als das Wachstum nach dem Schrumpfen.deque
wird nichtfree
bis zu 16 Blöcke (modulweit, nicht prodeque
) enthalten, sondern sie in einem billigen Array verfügbarer Blöcke zur Wiederverwendung ablegen. Wenn also eindeque
zum ersten Mal wächst , muss es immer neue Blöcke herausziehenmalloc
(wasappend
teurer wird), aber wenn es sich ein bisschen ständig ausdehnt, dann ein bisschen schrumpft und hin und her, wird es normalerweise nichtmalloc
/free
at beinhalten alles solange die Länge ungefähr in einem Bereich von 1024 Elementen bleibt (16 Blöcke auf der freien Liste, 64 Slots pro Block).Der Dokumentationseintrag für
deque
Objekte enthält vermutlich das meiste, was Sie wissen müssen. Bemerkenswerte Zitate:Aber...
Ich müsste mir die Quelle ansehen, um festzustellen, ob es sich bei der Implementierung um eine verknüpfte Liste oder um etwas anderes handelt, aber für mich klingt es so, als hätte a
deque
ungefähr die gleichen Eigenschaften wie eine doppelt verknüpfte Liste.quelle
Neben all den anderen hilfreichen Antworten finden Sie hier einige weitere Informationen zum Vergleich der Zeitkomplexität (Big-Oh) verschiedener Operationen in Python-Listen, Deques, Mengen und Wörterbüchern. Dies sollte bei der Auswahl der richtigen Datenstruktur für ein bestimmtes Problem hilfreich sein.
quelle
Ich bin mir zwar nicht ganz sicher, wie Python es implementiert hat, aber hier habe ich eine Implementierung von Queues geschrieben, die nur Arrays verwendet. Es hat die gleiche Komplexität wie Pythons Warteschlangen.
class ArrayQueue: """ Implements a queue data structure """ def __init__(self, capacity): """ Initialize the queue """ self.data = [None] * capacity self.size = 0 self.front = 0 def __len__(self): """ return the length of the queue """ return self.size def isEmpty(self): """ return True if the queue is Empty """ return self.data == 0 def printQueue(self): """ Prints the queue """ print self.data def first(self): """ Return the first element of the queue """ if self.isEmpty(): raise Empty("Queue is empty") else: return self.data[0] def enqueue(self, e): """ Enqueues the element e in the queue """ if self.size == len(self.data): self.resize(2 * len(self.data)) avail = (self.front + self.size) % len(self.data) self.data[avail] = e self.size += 1 def resize(self, num): """ Resize the queue """ old = self.data self.data = [None] * num walk = self.front for k in range(self.size): self.data[k] = old[walk] walk = (1+walk)%len(old) self.front = 0 def dequeue(self): """ Removes and returns an element from the queue """ if self.isEmpty(): raise Empty("Queue is empty") answer = self.data[self.front] self.data[self.front] = None self.front = (self.front + 1) % len(self.data) self.size -= 1 return answer class Empty(Exception): """ Implements a new exception to be used when stacks are empty """ pass
Und hier können Sie es mit etwas Code testen:
def main(): """ Tests the queue """ Q = ArrayQueue(5) for i in range(10): Q.enqueue(i) Q.printQueue() for i in range(10): Q.dequeue() Q.printQueue() if __name__ == '__main__': main()
Es funktioniert nicht so schnell wie die C-Implementierung, verwendet jedoch dieselbe Logik.
quelle