Các deques trong Python được triển khai như thế nào và khi nào chúng tệ hơn danh sách?


84

Gần đây tôi đã bắt đầu điều tra cách các cấu trúc dữ liệu khác nhau được triển khai trong Python để làm cho mã của tôi hiệu quả hơn. Khi điều tra cách thức hoạt động của danh sách và deques, tôi nhận thấy rằng tôi có thể nhận được lợi ích khi tôi muốn chuyển và dọn dẹp việc giảm thời gian từ O (n) trong danh sách sang O (1) trong deques (danh sách đang được triển khai dưới dạng mảng có độ dài cố định có được sao chép hoàn toàn mỗi khi có thứ gì đó được chèn ở phía trước, v.v ...). Những gì tôi dường như không thể tìm thấy là các chi tiết cụ thể về cách một deque được triển khai và các chi tiết cụ thể về nhược điểm của nó so với danh sách. Ai đó có thể khai sáng cho tôi về hai câu hỏi này?

Câu trả lời:


73

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

A dequeobjectbao gồm một danh sách các blocknút được liên kết kép .

Vì vậy, có, a dequelà một danh sách được liên kết (gấp đôi-) như một câu trả lời khác gợi ý.

Xây dựng: Điều này có nghĩa là danh sách Python tốt hơn nhiều cho các hoạt động truy cập ngẫu nhiên và có độ dài cố định, bao gồm cả cắt, trong khi deques hữu ích hơn nhiều để đẩy và đưa mọi thứ ra khỏi đầu, với việc lập chỉ mục (nhưng không phải là cắt, thú vị là) có thể nhưng chậm hơn so với danh sách.


3
Lưu ý rằng nếu bạn chỉ cần thêm và bật ở một đầu (ngăn xếp), các danh sách sẽ hoạt động tốt hơn .append().pop()được khấu hao O (1) (việc phân bổ lại và sao chép xảy ra, nhưng rất hiếm và chỉ cho đến khi bạn đạt đến kích thước tối đa thì ngăn xếp sẽ đã từng có).

@delnan: Nhưng nếu bạn muốn có một hàng đợi, thì những thứ như thế dequechắc chắn là cách thích hợp để đi.
JAB

@delnan: Bạn thấy thế nào? .append () và .pop () được khấu hao O (1) trên danh sách, nhưng chúng không phải là O (1) thực tế trên deques và các bản sao không bao giờ cần thiết.
Eli

1
@Eli: Các danh sách không liên quan đến vấn đề an toàn luồng (tốt, nó không được nối vào bên trong của chúng) và đã được nhiều người thông minh điều chỉnh trong một thời gian dài.

3
@delnan: Trên thực tế, deques trong CPython cũng không thực sự xử lý an toàn luồng; họ chỉ được hưởng lợi từ GIL làm cho các hoạt động của họ trở thành nguyên tử (và trên thực tế, appendpoptừ cuối của a listcó cùng các biện pháp bảo vệ). Trong thực tế, nếu bạn chỉ sử dụng một ngăn xếp, cả hai listdequeđều có hiệu suất giống hệt nhau trong CPython; phân bổ khối thường xuyên hơn với deque(nhưng không phải danh sách liên kết đơn giản thường xuyên; bạn chỉ kết thúc phân bổ / giải phóng mỗi khi bạn vượt qua ranh giới 64 thành viên trong triển khai CPython), nhưng việc thiếu các bản sao gián đoạn khổng lồ sẽ bù đắp.
ShadowRanger

51

Kiểm tra collections.deque. Từ các tài liệu:

Deques hỗ trợ an toàn luồng, gắn và bật bộ nhớ hiệu quả từ hai bên của deque với hiệu suất xấp xỉ O (1) ở cả hai hướng.

Mặc dù các đối tượng danh sách hỗ trợ các hoạt động tương tự, chúng được tối ưu hóa cho các hoạt động có độ dài cố định nhanh và phát sinh chi phí di chuyển bộ nhớ O (n) cho các hoạt động pop (0) và insert (0, v) thay đổi cả kích thước và vị trí của biểu diễn dữ liệu cơ bản .

Đúng như nó nói, việc sử dụng pop (0) hoặc insert (0, v) phải chịu những hình phạt lớn với các đối tượng danh sách. Bạn không thể sử dụng các thao tác lát / chỉ mục trên a deque, nhưng bạn có thể sử dụng popleft/ appendleft, là các thao tác dequeđược tối ưu hóa cho. Đây là một tiêu chuẩn đơn giản để chứng minh điều này:

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)

Kết quả trên máy của tôi:

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
Huh, chỉ cần nhận thấy rằng bạn không thể thực hiện cắt với deques mặc dù bạn có thể lập chỉ mục. Hấp dẫn.
JAB

1
+1 cho thời gian - điều thú vị listlà phần bổ sung nhanh hơn phần dequephụ một chút .
gửi

1
@zeekay: Điều đó khá kỳ lạ, khi xét đến việc tìm kiếm chỉ mục của một mục cụ thể thường sẽ yêu cầu lặp lại các mục của bộ sưu tập và bạn có thể lập chỉ mục thành một mục dequegiống như cách bạn làm list.
JAB

1
@senderle: Tất nhiên, list pops chậm hơn deque(có thể là do listchi phí thay đổi kích thước liên tục cao hơn khi nó thu nhỏ, nơi dequechỉ giải phóng các khối trở lại danh sách miễn phí hoặc nhóm đối tượng nhỏ), vì vậy khi chọn cấu trúc dữ liệu cho một ngăn xếp (hay còn gọi là hàng đợi LIFO), hiệu suất từ ​​rỗng đến đầy để trống có vẻ tốt hơn một chút deque(trung bình 6365K ops / giây cho append/ pop, so với list5578K ops / giây). Tôi nghi ngờ dequesẽ làm tốt hơn một chút trong thế giới thực, vì người làm dequenghề tự do có nghĩa là phát triển lần đầu tiên sẽ đắt hơn phát triển sau khi thu hẹp.
ShadowRanger

1
Để làm rõ tài liệu tham khảo cho người viết tự do của tôi: CPython dequesẽ không thực sự freelên đến 16 khối (toàn mô-đun, không phải cho mỗi khối deque), thay vào đó đặt chúng vào một loạt các khối có sẵn rẻ tiền để sử dụng lại. Vì vậy, khi phát triển một dequelần đầu tiên, nó luôn phải kéo các khối mới từ malloc(làm cho appendđắt hơn), nhưng nếu nó liên tục mở rộng một chút, sau đó thu nhỏ lại một chút, và lùi lại, nó thường sẽ không liên quan đến malloc/ freeat tất cả miễn là độ dài vẫn gần như nằm trong phạm vi 1024 phần tử (16 khối trong danh sách miễn phí, 64 vị trí mỗi khối).
ShadowRanger

16

Tôi nghi ngờ là mục nhập tài liệu cho dequecác đối tượng giải thích hầu hết những gì bạn cần biết. Trích dẫn đáng chú ý:

Deques hỗ trợ an toàn luồng, gắn và bật bộ nhớ hiệu quả từ hai bên của deque với hiệu suất xấp xỉ O (1) ở cả hai hướng.

Nhưng...

Truy cập được lập chỉ mục là O (1) ở cả hai đầu nhưng chậm lại là O (n) ở giữa. Để truy cập ngẫu nhiên nhanh chóng, hãy sử dụng danh sách để thay thế.

Tôi phải xem qua nguồn để biết liệu việc triển khai có phải là một danh sách được liên kết hay một cái gì đó khác hay không, nhưng tôi nghe có vẻ như a dequecó các đặc điểm gần giống như một danh sách được liên kết kép.


10

Ngoài tất cả các câu trả lời hữu ích khác, đây là một số thông tin khác so sánh độ phức tạp về thời gian (Big-Oh) của các hoạt động khác nhau trên danh sách Python, deques, bộ và từ điển. Điều này sẽ giúp lựa chọn cấu trúc dữ liệu phù hợp cho một vấn đề cụ thể.


-3

Trong khi, tôi không chắc chính xác cách Python đã triển khai nó như thế nào, ở đây tôi đã viết một cách triển khai Hàng đợi chỉ sử dụng mảng. Nó có độ phức tạp tương tự như Hàng đợi của 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

Và ở đây bạn có thể kiểm tra nó bằng một số mã:

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

Nó sẽ không hoạt động nhanh như triển khai C, nhưng nó sử dụng cùng một logic.


1
Đừng. Phá vỡ.the.wheel!
Abhijit Sarkar

Câu hỏi đặt ra là cách triển khai deque của Python. Nó không yêu cầu một triển khai thay thế.
Gino Mempin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.