파이썬의 deques는 어떻게 구현되며 언제 목록보다 더 나쁩니 까?


84

최근에 코드를보다 효율적으로 만들기 위해 다양한 데이터 구조가 Python에서 구현되는 방식을 조사했습니다. 목록과 deques의 작동 방식을 조사한 결과, 목록의 O (n)에서 deques의 O (1)로 시간을 단축하거나 이동을 해제하고 싶을 때 이점을 얻을 수 있음을 발견했습니다 (목록은 다음과 같은 고정 길이 배열로 구현 됨). 전면에 무언가를 삽입 할 때마다 완전히 복사됩니다. 내가 찾을 수없는 것은 deque가 어떻게 구현되는지에 대한 세부 사항과 그 단점과 목록의 세부 사항입니다. 누군가이 두 가지 질문에 대해 가르쳐 줄 수 있습니까?

답변:


73

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

A dequeobject는 이중으로 연결된 block노드 목록으로 구성 됩니다.

그래서 예, deque다른 답변에서 알 수 있듯이 a 는 (의심스러운) 연결된 목록입니다.

정교함 : 이것이 의미하는 것은 Python 목록이 슬라이싱을 포함한 임의 액세스 및 고정 길이 작업에 훨씬 더 나은 반면, 데크는 인덱싱 (하지만 슬라이싱은 아님)을 사용하여 끝 부분을 밀어 내고 터뜨리는 데 훨씬 더 유용하다는 것입니다. 가능하지만 목록보다 느립니다.


3
방금 추가 할 필요 한쪽 끝 (스택)에 팝업 경우,리스트로 더 잘 수행해야 함을 참고 .append()하고 .pop()있습니다 상각 O (1) (재 할당 및 복사가 발생하지만, 아주 드물게 만 때까지 최대. 크기를 스택에 도달하지가 않습니다 적).

@delnan :하지만 대기열을 원한다면 다음과 같은 deque것이 확실히 올바른 방법입니다.
JAB 2011

@delnan : 어떻게 알아? .append () 및 .pop ()은 목록에서 O (1) 상각되지만 데크에서 실제 O (1)가 아니며 사본이 필요 하지 않습니다.
엘리

1
@Eli : 목록은 스레드 안전성을 다루지 않으며 (글쎄요, 내부에 연결되어 있지 않습니다) 오랫동안 많은 현명한 사람들에 의해 조정되었습니다.

3
@delnan : 사실, dequeCPython의 s는 실제로 스레드 안전성을 처리하지 않습니다. 그들은 단지 GIL이 그들의 작업을 원자 적으로 만드는 것으로부터 이익을 얻습니다 (실제로, append그리고 popa의 끝 list에서 동일한 보호를가집니다). 당신은 그냥 스택을 사용하는 경우 실제로, 모두 listdeque의 CPython에서 효율적으로 동일한 성능을 가지고; 블록 할당은 더 자주 발생 deque하지만 (평범한 연결 목록은 자주 발생하지 않습니다. CPython 구현에서 64 개의 멤버 경계를 넘을 때마다 할당 / 해제하게됩니다), 간헐적 인 거대한 복사본의 부족이이를 보완합니다.
ShadowRanger 2015

51

확인하십시오 collections.deque. 문서에서 :

Deques는 어느 방향 으로든 거의 동일한 O (1) 성능으로 deque의 양쪽에서 스레드로부터 안전한 메모리 효율적인 추가 및 팝을 지원합니다.

목록 객체는 유사한 작업을 지원하지만 빠른 고정 길이 작업에 최적화되어 있으며 기본 데이터 표현의 크기와 위치를 모두 변경하는 pop (0) 및 insert (0, v) 작업에 대해 O (n) 메모리 이동 비용이 발생합니다. .

말했듯이 pop (0) 또는 insert (0, v)를 사용하면 목록 객체에 큰 불이익이 발생합니다. 에서 슬라이스 / 인덱스 작업을 deque사용할 수 없지만 작업 이 최적화 된 popleft/ 를 사용할 수 있습니다 . 다음은이를 입증하는 간단한 벤치 마크입니다.appendleftdeque

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
허, 방금 인덱싱을 할 수 있지만 deques로 슬라이싱을 할 수 없다는 것을 알았습니다. 흥미 롭군.
JAB

1
타이밍에 + list1-추가하는 것이 추가보다 약간 빠르다는 점이 흥미 롭습니다 deque.
senderle

1
@zeekay : 특정 항목의 인덱스를 검색하려면 일반적으로 컬렉션 항목을 반복해야 deque하고 list.
JAB

1
@senderle : 물론, list pop들보다 느린 있었다 deque의 (가능성 인해 list'간헐적가 어디 축소로 크기 조정의 높은 비용 deque단지 블록 무료 목록이나 작은 물체 풀에 다시 해제되는)에 대한 데이터 구조를 선택할 때, 스택 (일명 LIFO 큐), 빈 - 투 - 풀에 비어 성능 외모 약간 더 나은 deque(대한 평균 6365K 작전 / 초 append/ pop, 대 list의 5578K 작전 / 초). 의 자유 목록은 처음으로 성장하는 것이 축소 후 성장하는 것보다 더 비싸다는 것을 의미하기 deque때문에 현실 세계에서 약간 더 잘할 것이라고 생각 deque합니다.
ShadowRanger

1
내 freelist 참조를 명확히하기 위해 : CPython deque은 실제로 free최대 16 개 블록 (모듈 단위가 아닌 모듈 전체 deque)이 아니라 재사용을 위해 값싼 블록 배열에 넣습니다. 성장 그래서 deque처음으로, 항상에서 새로운 블록을 끌어가 malloc(하고 append끊임없이 앞뒤로 후, 조금 확대 조금 축소 및 않다면 더 비싼)하지만, 보통 포함되지 않습니다 malloc/ free에서 길이가 대략 1024 개의 요소 범위 (사용 가능한 목록의 16 개 블록, 블록 당 64 개의 슬롯) 내에있는 한 모두 가능합니다.
ShadowRanger

16

deque객체에 대한 문서 항목은 알아야 할 대부분의 내용을 설명합니다. 주목할만한 인용구 :

Deques는 어느 방향 으로든 거의 동일한 O (1) 성능으로 deque의 양쪽에서 스레드로부터 안전한 메모리 효율적인 추가 및 팝을 지원합니다.

그러나...

인덱스 액세스는 양쪽 끝에서 O (1)이지만 중간에서 O (n)으로 느려집니다. 빠른 임의 액세스를 위해 대신 목록을 사용하십시오.

구현이 연결 목록인지 아니면 다른 것인지 확인하기 위해 소스를 살펴보아야하지만, 마치 deque이중 연결 목록과 거의 동일한 특성이있는 것처럼 들립니다 .


10

다른 모든 도움이 답변 외에도, 여기 파이썬 목록, deques, 세트 및 사전에 다양한 작업의 시간 복잡도 (빅 - 오)를 비교 좀 더 정보입니다. 이는 특정 문제에 적합한 데이터 구조를 선택하는 데 도움이됩니다.


-3

파이썬이 어떻게 구현했는지 정확히 모르겠지만 여기에서는 배열 만 사용하여 큐 구현을 작성했습니다. 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
바퀴를 재발 명하지 마십시오!
Abhijit Sarkar

문제는 파이썬의 deque가 어떻게 구현 되는지 였습니다 . 대체 구현을 요구하지 않았습니다.
Gino Mempin
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.