web-dev-qa-db-fra.com

Comment les deques dans Python sont-ils implémentés, et quand sont-ils pires que les listes?

J'ai récemment étudié comment les différentes structures de données sont implémentées dans Python afin de rendre mon code plus efficace. En étudiant le fonctionnement des listes et des deques, j'ai constaté que je pouvais obtenir des avantages lorsque je veulent décaler et rétrograder en réduisant le temps de O(n) dans les listes à O(1) en deques (les listes étant implémentées comme des tableaux de longueur fixe qui doivent être copiés complètement chaque fois que quelque chose est inséré à l'avant, etc ...). Ce que je n'arrive pas à trouver sont les détails de la façon dont un deque est implémenté, et les détails de ses inconvénients par rapport aux listes. Quelqu'un peut-il éclairer moi sur ces deux questions?

61
Eli

https://hg.python.org/cpython/file/3.5/Modules/_collectionsmodule.c

Un dequeobject est composé d'une liste doublement liée de block nœuds.

Donc oui, un deque est une liste chaînée (doublement) comme une autre réponse le suggère.

Élaboration: ce que cela signifie, c'est que les listes Python sont bien meilleures pour les opérations d'accès aléatoire et de longueur fixe, y compris le découpage, tandis que les deques sont beaucoup plus utiles pour pousser et faire sauter les choses des extrémités, avec l'indexation (mais pas le découpage, fait intéressant) étant possible mais plus lente qu'avec les listes.

61
JAB

Check-out collections.deque . De la documentation:

Deques prend en charge les ajouts et les sauts de mémoire de thread-safe, efficaces de chaque côté du deque avec approximativement les mêmes performances O(1) dans les deux sens).

Bien que les objets de liste prennent en charge des opérations similaires, ils sont optimisés pour des opérations rapides de longueur fixe et entraînent O(n) coûts de déplacement de la mémoire pour les opérations pop (0) et insert (0, v) qui modifient les deux la taille et la position de la représentation sous-jacente des données.

Comme il est dit, l'utilisation de pop (0) ou de insert (0, v) entraîne des pénalités importantes avec les objets de liste. Vous ne pouvez pas utiliser d'opérations tranche/index sur un deque, mais vous pouvez utiliser popleft/appendleft, qui sont des opérations deque est optimisé pour. Voici une référence simple pour le démontrer:

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)

Résultats sur ma machine:

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
44
zeekay

L'entrée de documentation pour deque objets énonce la plupart de ce que vous devez savoir, je suppose. Citations notables:

Deques prend en charge les ajouts et les sauts de mémoire de thread-safe, efficaces de chaque côté du deque avec approximativement les mêmes performances O(1) dans les deux sens).

Mais...

L'accès indexé est O(1) aux deux extrémités mais ralentit à O(n) au milieu. Pour un accès aléatoire rapide, utilisez plutôt des listes.

Il faudrait que je jette un œil à la source pour savoir si l'implémentation est une liste chaînée ou autre, mais il me semble qu'une deque a à peu près les mêmes caractéristiques qu'une liste doublement liée.

14
senderle

En plus de toutes les autres réponses utiles, ici est un peu plus d'informations comparant la complexité temporelle (Big-Oh) de diverses opérations sur Python listes, deques, sets, et les dictionnaires. Cela devrait aider à sélectionner la bonne structure de données pour un problème particulier.

9
PythonJin

Alors que je ne sais pas exactement comment Python l'a implémenté, j'ai écrit ici une implémentation de files d'attente utilisant uniquement des tableaux. Elle a la même complexité que les files d'attente de Python.

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

Et ici, vous pouvez le tester avec du code:

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 __== '__main__':
    main()

Cela ne fonctionnera pas aussi rapidement que l'implémentation C, mais il utilise la même logique.

0
TheRevanchist