본문 바로가기
[CS] - Data Structure

# 2. 데크 (Deque)

by Bebsae 2021. 12. 2.

저번 포스트에서는 나서스의 스택에 스택과 큐에 대해 알아보았다. 이번 포스트에서는 큐의 연장선상 개념(?)인 데크에 대해서 알아보겠다. 데크는 풀네임 영어로 Double Ended Queue를 의미한다. 즉, 양방향으로 끝이 존재하는 큐를 뜻한다. 이번에도 역시 그림을 그려보자.

 

데크

 

큐와 모양 자체는 똑같다! 하지만, 큐는 한뱡으로 들어가고 나갈 수 없는 방면에 데크는 양끝에 데이터가 들어오고 나갈 수 있는 특징이 있다. 그렇다면 데크를 왜 사용하는지 알아보자. 이는 파이썬의 리스트 자료형과 비교하면 알 수 있다.

 

리스트의 insert

 

우리가 리스트에 insert하는 과정을 그림으로 그리면 다음과 같다. 먼저 insert를 하기 위한 공간 하나를 할당해주어야 한다. 그 다음 기존에 존재하던 자료의 갯수(N개, 위 그림에서는 2개)만큼 데이터들을 한칸씩 전부 뒤로 N번 밀어줘야한다. 극단적으로 생각했을 때, 리스트의 길이가 1억이라면 1억번을 뒤로 한칸씩 밀어줘야(Shift 연산)한다. 이는 시간복잡도가 O(N)이 소요된다. (N번의 연산이 필요하므로)

 

deque의 push_front

 

반면에 데크는 뒤로 밀어주는 과정이 필요없이 그냥 앞에다가 새로운 데이터를 넣으면 끝이다. 이로써 시간복잡도는 O(1)이 소요된다. (단 한번의 연산으로 끝나므로)

 

이제 코드로 직접 데크를 경험해보자. 필자는 데크를 collections 라이브러리를 통해서 사용하는 방법과 직접 구현하는 방법 두 가지를 모두 소개할 것이다.

 

[collections 라이브러리를 사용한 deque]

# 01. collections 라이브러리를 사용하여 데크를 사용하는 방법 & 리스트와의 속도 비교
import time
import datetime

from collections import deque


def timestamp(obj, iter_=200):
    print(type(obj))
    start = time.time()
    for i in range(iter_):
        if isinstance(obj, deque):
            obj.appendleft(i)
        elif isinstance(obj, list):
            obj.insert(0, i)
    print(datetime.timedelta(time.time() - start), '\n')


dq = deque([i for i in range(100)])
li = list([i for i in range(100)])
timestamp(dq)  # 0:00:02.410126 
timestamp(li)  # 0:00:06.385803

collections 내장 라이브러리에 구현되어 있는 데크를 사용한 코드이다. timestamp 메소드는 인자로 받은 obj가 데크 타입일 경우 appendleft, 리스트 타입을 경우 insert를 반복적으로 수행하고 걸리는 시간을 출력한다. 초만 보아도 압도적으로 데크가 속도가 빠르다!

 

[직접 구현한 데크]

# 02. 데크 직접 구현 (연결, 노드 사용)
class Node:
    def __init__(self, data):
        # 해당 노드의 데이터와 앞, 뒤 노드
        self.data = data
        self.front = None
        self.behind = None


class Deque:
    def __init__(self, data):
        # 맨 앞과 맨 뒤의 노드와 현재 탐색중인 위치의 노드
        self.front = None
        self.last = None
        self.current = None

        # 입력값이 리스트로 들어온 경우
        if isinstance(data, list):

            # for loop 안에서의 기준 노드
            curr_node = None

            for i in range(len(data)-1):

                if i == 0:
                    # 첫 노드와 두 번째 노드를 생성한다.
                    self.front = self.current = Node(data[i])
                    self.last = next_node = Node(data[i+1])
                    # 첫 노드와 두 번째 노드를 양방향으로 연결한다.
                    self.front.behind = next_node
                    self.last.front = self.front
                    # 다음 노드를 for loop 안에서의 기준 노드로 설정한다.
                    curr_node = next_node

                else:
                    # 새로운 노드로 생성하고 self.last 로 설정한다.
                    self.last = next_node = Node(data[i+1])
                    # 기준 노드와 새로운 노드를 양방향으로 연결한다.
                    curr_node.behind = next_node
                    self.last.front = curr_node
                    # 기준 노드를 새로 생성한 노드로 설정한다.
                    curr_node = next_node

        # 입력값이 정수로 들어온 경우
        elif isinstance(data, int):
            self.front = self.current = Node(data)

    def peek_all(self) -> List[Node]:
        """
        데크의 모든 노드를 리스트로 반환한다. (순차탐색)
        :return:
        """
        # 맨 앞의 노드를 탐색을 시작할 노드로 설정한다. (존재하지 않으면 끝낸다.)
        retrieve_node = self.front
        if retrieve_node is None:
            return None
        # 다음 노드가 존재하지 않을 때까지 nodes 리스트에다 (Node 인스턴스들을) 추가한다.
        nodes = list([retrieve_node])
        while retrieve_node.behind is not None:
            nodes.append(retrieve_node.behind)
            retrieve_node = retrieve_node.behind
        return nodes

    def is_empty(self):
        """
        데크가 비었는지 검사한다.
        :return:
        """
        return True if not len(self.peek_all()) else False

    def push_front(self, data):
        """
        데크의 맨 앞에 새로운 노드를 생성한다.
        :param data:
        :return:
        """
        new_node = Node(data)
        new_node.behind = self.front
        self.front.front = new_node
        self.front = new_node

    def push_behind(self, data):
        """
        데크의 맨 뒤에 새로운 노드를 생성한다.
        :param data:
        :return:
        """
        new_node = Node(data)
        new_node.front = self.last
        self.last.behind = new_node
        self.last = new_node

    def pop_front(self):
        """
        데크의 맨 앞에 노드를 제거한다.
        :return:
        """
        assert self.front is not None, '지울 값이 존재하지 않습니다.'
        remove_node = self.front
        self.front = self.front.behind
        self.front.front = None
        return remove_node

    def pop_last(self):
        """
        데크의 맨 뒤에 노드를 제거한다.
        :return:
        """
        assert self.last is not None, '지울 값이 존재하지 않습니다.'
        remove_node = self.last
        self.last = self.last.front
        self.last.behind = None
        return remove_node

    def peek_front(self):
        """
        데크의 맨 앞의 노드를 반환한다.
        :return:
        """
        print(self.front, self.front.data)
        return self.front

    def peek_last(self):
        """
        데크의 맨 뒤의 노드를 반환한다.
        :return:
        """
        print(self.last, self.last.data)
        return self.last

    def peek_current(self):
        """
        데크의 현재 탐색중인 노드를 반환한다.
        :return:
        """
        print(self.current, self.current.data)
        return self.current

    def peek_next(self):
        """
        데크의 현재 탐색중 노드의 다음 노드를 반환한다.인 (현재 위치를 다음 위치로 변경)
        :return:
        """
        assert self.current.behind is not None, '더 이상 값이 존재하지 않습니다.'
        self.current = self.current.behind
        return self.peek_current()

    def peek_prev(self):
        """
        데크의 현재 탐색중인 노드의 이전 노드를 반환한다. (현재 위치를 이전 위치로 변경)
        :return:
        """
        assert self.current.front is not None, '더 이상 값이 존재하지 않습니다.'
        self.current = self.current.front
        return self.peek_current()


custom_dq = Deque([1, 2, 3])
custom_dq.peek_front()  # 1
custom_dq.peek_last()  # 3
custom_dq.peek_current()  # 1

custom_dq.peek_next()  # 2
custom_dq.peek_next()  # 3
# custom_dq.peek_next()  # Assertion error

custom_dq.peek_prev()  # 2
custom_dq.peek_prev()  # 1
# custom_dq.peek_prev()  # Assertion error

custom_dq.push_front(10)  # [10, 1, 2, 3]
custom_dq.peek_front()  # 10

custom_dq.push_behind(20)  # [10, 1, 2, 3, 20]
custom_dq.peek_last()  # 20

nodes = custom_dq.peek_all()
[print(node.data) for node in nodes]

위 코드는 필자가 양방향 연결 리스트 형태를 통해 직접 구현한 코드이다. 생각보다 많이 길어졌다. (코드가 긴 관계로 설명을 주석으로 대체했다.) 하지만, 기본으로 구현해야하는 메소드인 push_front(), push_last(), pop_front(), pop_last() 이외에도 정상적으로 데크가 생성되었는지 검증하기 위한 메소드들을 추가로 구현했다. 

 

이 부분은 직접 구현하기 어렵다면 코드를 복사해서 직접 하나하나 디버거를 돌려가며 분석해도 좋다. 그리고 필자의 코드 없이도 스스로 구현할 수 있을 때까지 반복 연습을 하기를 바란다.

댓글