ホームページ >バックエンド開発 >Python チュートリアル >Python での Deque: 効率的なキューとスタックの実装
Python の Deque は、低レベルで高度に最適化された両端キューであり、コンピューティングにおいて最も一般的なリストベースのデータ型である、エレガントで効率的な Python のキューとスタックの実装に役立ちます。
この記事では、Yun Duo 氏があなたと一緒に次のことを学びます:
Python の右端に要素を追加する操作要素のリストとポップは一般に非常に効率的です。時間計算量を Big O で表現すると、O(1) であると言えます。新しい要素を受け入れるために基になるリストを増やすために Python がメモリを再割り当てする必要がある場合、これらの操作が遅くなり、時間計算量が O(n) になる可能性があります。
さらに、Python リストの左端に要素を追加したりポップしたりする操作も非常に非効率であり、時間計算量は O(n) です。
リストは .append() および .pop() 操作を提供するため、スタックおよびキューとして使用できます。リストの左端と右端での追加およびポップ操作のパフォーマンスの問題は、アプリケーション全体のパフォーマンスに大きな影響を与えます。
Python の両端キューは、Python 2.4 の時点でコレクション モジュールに追加された最初のデータ型でした。このデータ型は、Python リストの .append() および .pop() の効率性の問題を克服するために特別に設計されています。
Deques は、スタックとキューを一般化して設計されたシーケンスのようなデータ型で、データ構造の両端でメモリ効率が高く、高速な追加およびポップ操作をサポートします。
両端キューは二重リンク リストとして実装されているため、両端キュー オブジェクトの両端での追加およびポップ操作は安定しており、同様に効率的です。さらに、デキューでの追加およびポップ操作もスレッドセーフでメモリ効率が高くなります。これらの機能により、deque は Python でカスタム スタックとキューを作成する場合に特に便利になります。
最後に表示された要素リストを保存する必要がある場合は、deque の最大長が制限されている可能性があるため、deque を使用することもできます。これを行うと、両端キューがいっぱいになると、もう一方の端に新しい要素を追加するときに、一方の端の要素が自動的に破棄されます。
以下は、deque の主な機能の概要です。
deque インスタンスの作成は比較的簡単です。コレクションから deque をインポートし、オプションの反復子を引数として呼び出してください。
>>> from collections import deque >>> # 创建一个空的 deque >>> deque() deque([]) >>> # 使用不同的迭代器来创建 deque >>> deque((1, 2, 3, 4)) deque([1, 2, 3, 4]) >>> deque([1, 2, 3, 4]) deque([1, 2, 3, 4]) >>> deque(range(1, 5)) deque([1, 2, 3, 4]) >>> deque("abcd") deque(['a', 'b', 'c', 'd']) >>> numbers = {"one": 1, "two": 2, "three": 3, "four": 4} >>> deque(numbers.keys()) deque(['one', 'two', 'three', 'four']) >>> deque(numbers.values()) deque([1, 2, 3, 4]) >>> deque(numbers.items()) deque([('one', 1), ('two', 2), ('three', 3), ('four', 4)])
パラメータとして iterable を指定せずに両端キューをインスタンス化すると、空の両端キューが取得されます。 iterable が提供されて入力されると、両端キューはそのデータを使用して新しいインスタンスを初期化します。初期化は、deque.append() を使用して左から右に進みます。
Deque イニシャライザには、次の 2 つのオプションのパラメータが必要です。
前に述べたように、反復可能オブジェクトを指定しない場合は、空の両端キューが取得されます。 maxlen の値を指定すると、両端キューには maxlen までの項目のみが保存されます。
最後に、コレクションなどの順序付けされていない反復可能オブジェクトを使用して両端キューを初期化することもできます。このような場合、最終デキュー内の要素の順序は事前に定義されていません。
Deque と List の最も重要な違いは、前者はシーケンスの両端で効率的な追加とポップ操作を実行できることです。 Deque クラスは、シーケンスの左端を直接操作するための特殊な .popleft() メソッドと .appendleft() メソッドを実装します。
>>> from collections import deque >>> numbers = deque([1, 2, 3, 4]) >>> numbers.popleft() 1 >>> numbers.popleft() 2 >>> numbers deque([3, 4]) >>> numbers.appendleft(2) >>> numbers.appendleft(1) >>> numbers deque([1, 2, 3, 4])
ここでは、.popleft() と .appendleft() を使用して、それぞれ数値の左端の値をポップアップおよび増加させます。これらのメソッドは deque 用に設計されており、list にはそのようなメソッドはありません。
Deque は、シーケンスの右端を操作するための list のような .append() および .pop() メソッドも提供します。ただし、.pop() の動作は異なります。
>>> from collections import deque >>> numbers = deque([1, 2, 3, 4]) >>> numbers.pop() 4 >>> numbers.pop(0) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: pop() takes no arguments (1 given)
ここで、.pop() は両端キュー コンテナーの最後の要素を削除して返します。このメソッドはパラメータとしてインデックスを受け入れないため、両端キューから任意の項目を削除するために使用することはできません。これを使用できるのは、右端の項目を削除して戻す場合のみです。
私たちは、deque は二重リンクリストであると考えています。したがって、特定の両端キュー コンテナー内の各項目は、シーケンス内の前後の項目への参照 (ポインター) を保持します。
二重リンク リストを使用すると、両端からの要素の追加とポップの操作が簡単かつ効率的になります。更新する必要があるのはポインターだけであるため、2 つの操作のパフォーマンスは同等であり、どちらも O(1) です。また、新しい項目を受け入れるためにメモリを再割り当てしたり、既存の項目を移動したりする必要がないため、パフォーマンスの点でも予測可能です。
从常规 Python 列表的左端追加和弹出元素需要移动所有元素,这最终是一个 O(n) 操作。此外,将元素添加到列表的右端通常需要Python重新分配内存,并将当前项复制到新的内存位置,之后,它可以添加新项。这个过程需要更长的时间来完成,并且追加操作从 O(1)传递到 O(n)。
考虑以下关于在序列左端添加项的性能测试,deque vs list。
# time_append.py from collections import deque from time import perf_counter TIMES = 10_000 a_list = [] a_deque = deque() def average_time(func, times): total = 0.0 for i in range(times): start = perf_counter() func(i) total += (perf_counter() - start) * 1e9 return total / times list_time = average_time(lambda i: a_list.insert(0, i), TIMES) deque_time = average_time(lambda i: a_deque.appendleft(i), TIMES) gain = list_time / deque_time print(f"list.insert(){list_time:.6} ns") print(f"deque.appendleft() {deque_time:.6} ns({gain:.6}x faster)")
在这个脚本中,average_time() 计算了执行一个给定次数的函数(func)的平均时间。如果我们在命令行中运行该脚本,那么我们会得到以下输出。
$ python time_append.py list.insert()3735.08 ns deque.appendleft() 238.889 ns(15.6352x faster)
在这个例子中,deque 上的 .appendleft() 要比 list 上的 .insert() 快几倍。注意 deque.appendleft() 执行时间是常量O(1)。但列表左端的 list.insert() 执行时间取决于要处理的项的数量O(n)。
在这个例子中,如果增加 TIMES 的值,那么 list.insert() 会有更高的时间测量值,而 deque.appendleft() 会得到稳定(常数)的结果。如果对 deque 和 list 的 pop 操作进行类似的性能测试,那么可以展开下面的练习块。
Exercise:测试 deque.popleft() 与 list.pop(0) 的性能
可以将上面的脚本修改为时间deque.popleft()与list.pop(0)操作并估计它们的性能。
Solution:测试 deque.popleft() 与 list.pop(0) 的性能
# time_pop.py from collections import deque from time import perf_counter TIMES = 10_000 a_list = [1] * TIMES a_deque = deque(a_list) def average_time(func, times): total = 0.0 for _ in range(times): start = perf_counter() func() total += (perf_counter() - start) * 1e9 return total / times list_time = average_time(lambda: a_list.pop(0), TIMES) deque_time = average_time(lambda: a_deque.popleft(), TIMES) gain = list_time / deque_time print(f"list.pop(0) {list_time:.6} ns") print(f"deque.popleft() {deque_time:.6} ns({gain:.6}x faster)")
list.pop(0) 2002.08 ns deque.popleft() 326.454 ns(6.13282x faster) 同样,它deque比list从底层序列的左端删除元素要快。 尝试更改TIMES的值,看看会发生什么
Deque 数据类型的设计是为了保证在序列的两端进行有效的追加和弹出操作。它是处理需要在 Python 中实现队列和堆栈数据结构的问题的理想选择。
Python 的 deque 返回可变的序列,其工作方式与列表相当类似。除了可以有效地从其末端追加和弹出元素外,deque 还提供了一组类似列表的方法和其他类似序列的操作,以处理任意位置的元素。下面是其中的一些。
选项 |
描述 |
.insert(i, value) |
在索引为i的deque容器中插入一个名为value的元素。 |
.remove (value) |
删除第一个出现的 value ,如果 value 不存在则引发ValueError |
a_deque[i] |
从一个deque容器中检索索引为 i 的项。 |
del a_deque[i] |
インデックス i の項目を deque コンテナから削除します。 |
我们可以使用这些方法和技术来处理 deque 对象内部任何位置的元素。下面是如何做到这一点的。
>>> from collections import deque >>> letters = deque("abde") >>> letters.insert(2, "c") >>> letters deque(['a', 'b', 'c', 'd', 'e']) >>> letters.remove("d") >>> letters deque(['a', 'b', 'c', 'e']) >>> letters[1] 'b' >>> del letters[2] >>> letters deque(['a', 'b', 'e'])
在这里,首先将"c"插入到位置 2的letters中。然后使用 .remove() 从deque容器中移除"d"。Deque 还允许索引来访问元素,在这里使用它来访问索引1处的b。最后,你可以使用 del 关键字从 deque 中删除任何存在的项。请注意, .remove() 允许按值删除项,而del则按索引删除项。
尽管 deque 对象支持索引,但它们不支持切片,即不能像常规列表一样使用切片语法, [start:stop:step] 从现有的 deque 中提取:
>>> from collections import deque >>> numbers = deque([1, 2, 3, 4, 5]) >>> numbers[1:3] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: sequence index must be integer, not 'slice'
Deque支持索引,却不支持分片。通常来说在一个链表上执行切片非常低效。
虽然 deque 与 list 非常相似,但 list 是基于数组的,而 deque 是基于双链表的。
Deque 基于双链表,在访问、插入和删除任意元素都是无效操作。如果需要执行这些操作,则解释器必须在deque中进行迭代,直到找到想要的元素。因而他们的时间复杂度是O(n)而不是O(1)。
下面演示了在处理任意元素时 deques 和 list 的行为。
# time_random_access.py from collections import deque from time import perf_counter TIMES = 10_000 a_list = [1] * TIMES a_deque = deque(a_list) def average_time(func, times): total = 0.0 for _ in range(times): start = perf_counter() func() total += (perf_counter() - start) * 1e6 return total / times def time_it(sequence): middle = len(sequence) // 2 sequence.insert(middle, "middle") sequence[middle] sequence.remove("middle") del sequence[middle] list_time = average_time(lambda: time_it(a_list), TIMES) deque_time = average_time(lambda: time_it(a_deque), TIMES) gain = deque_time / list_time print(f"list{list_time:.6} μs ({gain:.6}x faster)") print(f"deque {deque_time:.6} μs")
这个脚本对插入、删除和访问一个 deque 和一个 list 中间的元素进行计时。如果运行这个脚本,得到如下所示的输出:
$ python time_random_access.py list63.8658 μs (1.44517x faster) deque 92.2968 μs
Deque并不像列表那样是随机访问的数据结构。因此,从 deque 的中间访问元素的效率要比在列表上做同样的事情低。这说明 deque 并不总是比列表更有效率。
Python 的 deque 对序列两端的操作进行了优化,所以它们在这方面一直比 list 好。另一方面,列表更适合于随机访问和固定长度的操作。下面是 deque 和 list 在性能上的一些区别。
运作 |
|
|
通过索引访问任意的元素 |
O(n) |
O(1) |
在左端弹出和追加元素 |
O(1) |
O(n) |
要素を右端にポップして追加します |
O (1) |
##O(1) 再配布 |
O(n) | O(n) |
Method |
Support |
|
|
|
|
|
定期的な反復 |
|
逆反復 |
|
文字列表現 |
理想情况下,.__repr__()返回一个字符串,代表一个有效的 Python 表达式。可以用这个表达式以相同的值重新创建这个对象。
然而,在上面的例子中,目的是使用方法的返回值在 interactive shell 上优雅地显示对象。可以通过接受初始化可迭代对象作为.__init__() 的参数并从中构建实例,从而从这个特定的字符串表示形式构建 Queue 实例。
有了这些补充,Queue 类就完成了。要在我们的代码中使用这个类,我们可以做如下事情。
>>> from custom_queue import Queue >>> numbers = Queue() >>> numbers Queue([]) >>> # Enqueue items >>> for number in range(1, 5): ... numbers.enqueue(number) ... >>> numbers Queue([1, 2, 3, 4]) >>> # Support len() >>> len(numbers) 4 >>> # Support membership tests >>> 2 in numbers True >>> 10 in numbers False >>> # Normal iteration >>> for number in numbers: ... print(f"Number: {number}") ... 1 2 3 4
队列和堆栈是编程中常用的 抽象数据类型。它们通常需要在底层数据结构的两端进行有效的 pop 和 append 操作。Python 的 collections 模块提供了一种叫做 deque 的数据类型,它是专门为两端的快速和节省内存的追加和弹出操作而设计的。
有了deque,我们可以用优雅、高效和Pythonic的方式在低层次上编写我们自己的队列和堆栈。
总结下本文所学内容:
以上がPython での Deque: 効率的なキューとスタックの実装の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。