Pythonの両端キューはどのように実装され、リストよりも悪いのはいつですか?


85

私は最近、コードをより効率的にするために、Pythonでさまざまなデータ構造がどのように実装されているかを調査するようになりました。リストと両端キューがどのように機能するかを調査したところ、シフトとシフト解除を行うと、リストのO(n)から両端キューのO(1)に時間を短縮したいときにメリットが得られることがわかりました(リストは固定長配列として実装されています。前面に何かを挿入するたびに完全にコピーされるなど...)。私が見つけられないように見えるのは、dequeがどのように実装されているか、およびその欠点とリストの詳細です。誰かがこれらの2つの質問について私に教えてもらえますか?

回答:


74

https://github.com/python/cpython/blob/v3.8.1/Modules/_collectionsmodule.c

Adequeobjectは、二重にリンクされたblockノードのリストで構成されます。

そうです、deque別の答えが示唆しているように、aは(二重に)リンクされたリストです。

詳細:これが意味するのは、Pythonリストは、スライスを含むランダムアクセスおよび固定長の操作にはるかに優れているのに対し、両端キューは、インデックス付け(ただし、興味深いことにスライスではない)を使用して、物事をプッシュおよびポップするのにはるかに役立つということです。可能ですが、リストよりも遅くなります。


3
片方の端(スタック)で追加してポップする必要がある場合は、リストのパフォーマンスが向上し.append().pop()O(1)で償却されることに注意してください(再割り当てとコピーは発生しますが、スタックが最大サイズに達するまでは非常にまれです。これまでに持っている)。

@delnan:しかし、キューが必要な場合dequeは、次のようなものが間違いなく正しい方法です。
JAB 2011

@delnan:どう思いますか?.append()と.pop()は、リストでは償却されたO(1)ですが、両端キューでは実際のO(1)ではなく、コピーは必要ありません。
Eli 2011年

1
@Eli:リストはスレッドセーフを扱っておらず(まあ、それは内部に接続されていません)、多くの賢い人々によって長い間調整されてきました。

3
@delnan:実際、dequeCPythonのsはスレッドセーフも実際には処理しません。彼らは、GILが操作をアトミックにすることで恩恵を受けるだけです(そして実際、appendそしてpop最後からlist同じ保護があります)。あなただけのスタックを使用している場合実際には、両方listdequeCPythonの中に効果的に同じ性能を有します。ブロック割り当てはより頻繁に行われますdeque(ただし、プレーンリンクリストは頻繁ではありません。CPython実装で64メンバーの境界を超えるたびに割り当て/解放されるだけです)が、巨大な断続的なコピーがないことで補います。
ShadowRanger 2015

51

チェックアウトcollections.deque。ドキュメントから:

Dequeは、スレッドセーフでメモリ効率の高い追加と、Dequeの両側からのポップをサポートし、どちらの方向でもほぼ同じO(1)パフォーマンスを実現します。

リストオブジェクトは同様の操作をサポートしますが、高速の固定長操作用に最適化されており、基になるデータ表現のサイズと位置の両方を変更するpop(0)およびinsert(0、v)操作のO(n)メモリ移動コストが発生します。

それが言うように、pop(0)またはinsert(0、v)を使用すると、リストオブジェクトに大きなペナルティが発生します。でスライス/インデックス操作を使用することはできませんが、操作が最適化されている/dequeを使用することはできます。これを実証するための簡単なベンチマークは次のとおりです。popleftappendleftdeque

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)

私のマシンでの結果:

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

3
インデックス作成はできても、両端キューではスライスできないことに気づきました。面白い。
JAB 2011

1
タイミングの+ list1-追加がdeque追加よりもわずかに速いのは興味深いことです。
センダール2011

1
@zeekay:特定のアイテムのインデックスを検索するには、通常、コレクションのアイテムを反復処理する必要があり、とdeque同じようにインデックスを作成できることを考えると、これは非常に奇妙なことですlist
JAB

1
@senderle:もちろん、list popsはdeque'sよりも遅かった(おそらくlist、縮小するときに断続的にサイズ変更するコストが高く、dequeブロックを解放して空きリストまたは小さなオブジェクトプールに戻すだけであるため)。スタック(別名LIFOキュー)、空へのフル・ツー・空のため若干良いパフォーマンスルックスdeque(の平均値6365K ops /秒append/ pop、対listの5578K ops /秒)。私は疑うdequeように、現実の世界でわずかに良いだろうdeque、初めて成長の空きリスト手段が収縮後の成長よりも高価です。
ShadowRanger

1
私のフリーリスト参照を明確にするために:CPythondequeは実際にはfree最大16ブロック(モジュール全体、あたりではないdeque)ではなく、再利用可能なブロックの安価な配列に配置します。したがってdeque、初めて成長するときは、常に新しいブロックをプルする必要がありますmallocappendより高価になります)が、それが常に少し拡大し、次に少し縮小し、前後に移動する場合、通常はmalloc/ freeatを含みません長さがおおよそ1024要素の範囲内にある限り(空きリストの16ブロック、ブロックあたり64スロット)。
ShadowRanger

17

dequeオブジェクトのドキュメントエントリは、あなたが知る必要のあることのほとんどを詳しく説明していると私は思う。注目すべき引用:

Dequeは、スレッドセーフでメモリ効率の高い追加と、Dequeの両側からのポップをサポートし、どちらの方向でもほぼ同じO(1)パフォーマンスを実現します。

だが...

インデックス付きアクセスは両端でO(1)ですが、途中でO(n)に遅くなります。高速ランダムアクセスの場合は、代わりにリストを使用してください。

実装がリンクリストなのか他のものなのかを知るためにソースを調べる必要がありますがdeque、二重リンクリストとほぼ同じ特性を持っているように思えます。


11

他のすべての役に立つの回答に加えて、ここではPythonのリスト、dequeの、セット、および辞書に様々な操作の時間計算量(ビッグ・オー)を比較するいくつかのより多くの情報があります。これは、特定の問題に適切なデータ構造を選択するのに役立ちます。


-3

Pythonがどのように実装したかは正確にはわかりませんが、ここでは、配列のみを使用してキューの実装を作成しました。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

そしてここであなたはいくつかのコードでそれをテストすることができます:

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()

C実装ほど高速には動作しませんが、同じロジックを使用します。


1
車輪の再発明をしないでください!
AbhijitSarkar19年

問題は Pythonの両端キューがどのように実装されているかでした。代替の実装を求めていませんでした。
ジーノメンピン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.