본문 바로가기

알고리즘 온라인 저지/알고리즘 | 자료구조

자료구조 덱(deque, dequeue)

자료구조 덱은 Double Ended QUEue의 축약어로, "deck"으로 발음하게 되어있다. 하지만 데크, 디큐 등의 표기나 발음이 존재할 수 있기 때문에 문맥에 맞는 내용을 선택하자. (사실 '디큐'는 값을 꺼내는 디큐잉이랑 혼동되기 때문에 거의 본 적이 없다.)

 

덱의 풀네임인 double ended queue에서 알 수 있듯 기본적으로 큐의 동작을 양방향으로 지원하는 자료구조이다.

우선 큐에 대하여 잘 모른다면 더보기의 큐를 선행 학습하고 돌아오자.

 

 

 

덱은 큐의 기능들을 정 / 역방향 모두 지원하는 자료구조이다. 덱의 동작은 아래와 같다.

큐에 있는 push, pop, back 기능이 push_front, push_back, pop_front, pop_back, front, back 동작으로, 방향을 가진 세부 동작이 된다. 추가로  c++ 기준 덱은 임의의 인덱스로 접근하는 random access를 지원한다.

 


그런데 이렇게 동작하는 자료구조를 어떻게 하면 효율적으로 만들 수 있을까?

 

인덱스 접근을 허용하는 형태 (= array, vector 등 메모리가 연속된 자료구조)를 이용한다면 임의 접근과, push_back, pop_back 등은 효율적이겠지만 앞쪽을 대상으로 한 연산이 발생한 경우 매번 값을 한 칸씩 밀어줘야 하는 오버헤드가 발생한다.

 

만약 연속적인 메모리 할당 후, 중간지점에서 시작한다고 하더라도 앞쪽과, 뒤쪽의 연산이 균형을 이루지 않는다면 결국 한쪽으로 치우치게 될 것이고 그 치우침을 맞춰주거나, 새롭게 공간을 확보하는 행위가 모두 불필요한 오버헤드가 된다.

 

반대로 비 연속적인 메모리 할당을 하는(= 연결 리스트 등) 형태로 구현한다면 확실히 앞뒤 방향의 연산은 효율적으로 지원되지만, 인덱스 접근 행위에 N/2 회의 탐색이 필요할 수 있다.

 

이 모두를 어느 정도 만족시키기 위해 덱은 일정 단위의 연속적 메모리를 할당하는 블록을, 비 연속적인 자료구조로 묶어서 이용한다.

 

이를 직접 구현하기 위해서는 map이라는 자료구조에 대하여 알아야 한다. 우선 맵에 대하여 잘 모른다면 맵을 선행 학습하고 돌아오자.

더보기

자료구조 map 포스팅 추가 예정

 

시작에 앞서, 덱에 1~8의 값을 넣어둔 상태라고 가정하자. 이 덱은 데이터 4개가 하나의 블록을 이루는 형태이다.


값이 예쁘게 들어가있다, 아래의 블록이 담긴 표는 맵이며, 위의 블록은 4칸짜리 배열이라고 생각해도 된다.

같은 블록내의 값들은 메모리가 연속적으로 할당되어 있으며, 각 블록 간에는 메모리의 연관성이 없다. (= 각각 새롭게 동적으로 할당되었다.)

 

위 상태에서 push front 연산으로 값 0을 넣으려 한다, 그렇다면 먼저 현재 가지고 있는 블록 중 가장 작은 블록에 빈 공간이 있는지 확인 후, 빈 공간이 없다면 새로운 블록을 만든다. 이를 블록 0이라고 하자

 


값 0이 블록0의 맨 뒤에 들어간다는 점이 당연하다고 느껴졌으면 좋겠다.
당연하다고 느껴지지 않는다면 왜 값이 블록의 뒤쪽에 들어가야 하는지 생각해보자. 약간의 힌트를 적자면, 이 블록 0은 앞쪽 연산만을 대상으로 만들어진 블록은 아니라는 점과, 인덱스 접근이 가능해야 한다는 점이다.

 


값 -1을 push_front 한다면 위와 같은 상태가 된다.

 


값 9를 push_back 한다면 위와 같은 상태가 된다.

 

 

 

위의 예시를 통해 앞, 뒤 방향 동작을 보였다.

이 상황에서 어떻게 하면 인덱스 접근을 효율적으로 진행할 수 있을까?

 

아래의 이미지에서 "6번째 값에 최대한 빠르게 접근하는" 방법을 생각해보자.

 

 

 

 

 

Df = 맨 앞 블록의 데이터 개수, 여기서는 2.
Db = 한 블록당 데이터 개수, 여기서는 4.
Index = 입력받은 인덱스, 여기서는 6번째 칸 이므로 5. (0부터 시작하는 인덱스)

 

(Index - Df) = 5 - 2 = 3


(Index - Df) / Db = 3 / 4 = 0
(Index - Df) % Db  = 3 % 4 = 3


즉 6번째 값은 맨 앞 블록에서 바로 다음 블록에, 그 안에서는 인덱스 3에 위치한다.

 

인덱스를 바꿔가며, 블록 개수나, 블록당 데이터 개수를 바꿔가며 계산해봐도 위의 계산식을 통해 원하는 데이터가
어느 블록의, 몇번째 값 인지를 바로 알 수 있다.

 

이때 어느 블록의 시작 주소를 map을 통해 얻어낸다면 연결 리스트를 이용한 방법보다 효율적으로 인덱스 접근을 지원할 수 있다.


하지만 위의 방식을 이용해도 임의의 위치에 대한 값 삽입, 삭제는 오버헤드가 발생할 수밖에 없다.

꽉 차지 않은 블록이 중간에 위치하게 되면 인덱스 접근을 위한 공식이 틀어지기 때문이다.

 

이러한 특성을 알아두고, 덱을 사용하면 비효율적인 상황에서 덱을 선택하는 실수를 하지 않아야 한다.