자료구조는 데이터를 효율적으로 구성, 관리, 저장하기 위한 구조적 틀이다. 컴퓨터 과학의 핵심 기초를 이루며, 문제 해결을 위한 알고리즘 설계의 토대가 된다. 기본적인 자료구조로는 배열, 연결 리스트, 스택, 큐 등이 있으며, 각각 고유한 데이터 조직 방식과 연산 규칙을 가진다.
이들 기본 자료구조는 데이터에 접근하고 조작하는 방식에 따라 선형과 비선형으로 구분된다. 배열과 연결 리스트, 스택, 큐는 모두 데이터 요소가 순차적으로 나열되는 선형 자료구조에 속한다. 각 구조는 특정한 접근 패턴(예: 후입선출, 선입선출)이나 물리적 저장 방식(예: 연속 메모리, 노드 연결)에 따라 다른 성능 특성을 보인다.
올바른 자료구조의 선택은 프로그램의 성능과 효율성을 결정하는 핵심 요소이다. 예를 들어, 빈번한 삽입과 삭제가 필요한 경우 연결 리스트가 유리할 수 있지만, 빠른 임의 접근이 필요하면 배열이 더 적합하다. 스택은 함수 호출 관리나 역폴란드 표기법 계산에, 큐는 작업 스케줄링이나 너비 우선 탐색에 주로 활용된다.
이 문서에서는 네 가지 기본적인 선형 자료구조의 원리, 구현, 장단점, 그리고 실제 활용 방법을 상세히 다룬다. 각 구조의 이해는 더 복잡한 트리나 그래프와 같은 고급 자료구조를 학습하는 데 필수적인 기초가 된다.
배열은 동일한 자료형의 데이터 요소들이 연속된 메모리 공간에 순차적으로 저장된 자료구조이다. 각 요소는 인덱스라는 정수 값으로 식별되며, 일반적으로 0부터 시작한다. 배열의 크기는 선언 시에 정해지는 경우가 많으며, 이는 정적 배열의 특징이다. 일부 프로그래밍 언어는 실행 중에 크기를 조절할 수 있는 동적 배열을 제공하기도 한다.
배열의 가장 큰 장점은 임의 접근이 가능하다는 점이다. 인덱스를 알고 있는 특정 요소에 접근하는 시간 복잡도는 O(1)로 상수 시간에 이루어진다. 또한 데이터들이 물리적으로 연속되어 저장되기 때문에 공간 지역성이 좋아 캐시 효율이 높다. 반면, 배열의 크기를 미리 정해야 하며, 중간에 요소를 삽입하거나 삭제할 때는 다른 요소들을 이동시켜야 하므로 비효율적일 수 있다.
배열의 주요 연산에 대한 시간 복잡도는 다음과 같다.
연산 | 시간 복잡도 | 설명 |
|---|---|---|
접근(Access) | O(1) | 인덱스를 통한 직접 접근 |
탐색(Search) | O(n) | 순차적으로 요소를 찾아야 함 |
삽입(Insertion) | O(n) | 중간 삽입 시 요소 이동 필요 |
삭제(Deletion) | O(n) | 중간 삭제 시 요소 이동 필요 |
배열은 구현의 기초가 되는 자료구조로 널리 사용된다. 고정된 크기의 데이터 집합을 다루거나, 행렬 및 다차원 데이터 표현, 다른 복잡한 자료구조(예: 힙, 해시 테이블)의 내부 구현에 활용된다. 또한 버퍼, 루프를 통한 일괄 처리, 이미지의 픽셀 데이터 저장 등 다양한 실전 응용 사례가 존재한다.
배열은 동일한 자료형의 데이터 요소들이 메모리 공간에 연속적으로 저장되는 선형 자료구조이다. 각 요소는 인덱스라는 정수 값을 통해 직접 접근할 수 있다.
배열의 주요 장점은 임의 접근이 가능하다는 점이다. 인덱스를 알고 있다면 해당 위치의 요소에 상수 시간, 즉 O(1)의 시간 복잡도로 접근할 수 있다. 이는 데이터 검색 속도가 매우 빠르다는 것을 의미한다. 또한, 연속된 메모리 공간을 사용하기 때문에 캐시 지역성이 좋아 CPU 캐시 히트율이 높아질 수 있다.
반면, 배열에는 몇 가지 단점이 존재한다. 가장 큰 단점은 크기가 고정되어 있다는 점이다. 배열을 선언할 때 그 크기를 미리 지정해야 하며, 실행 중에 크기를 동적으로 변경하기 어렵다. 크기를 늘리려면 더 큰 새로운 배열을 할당하고 기존 데이터를 모두 복사해야 하는 비용이 발생한다. 또한, 배열 중간에 요소를 삽입하거나 삭제할 경우, 해당 위치 이후의 모든 요소들을 이동시켜야 하므로 시간 복잡도가 O(n)이 된다.
장점 | 단점 |
|---|---|
임의 접근(Random Access) 가능 (O(1)) | 크기가 고정적 (정적 할당) |
캐시 지역성이 좋음 | 중간 삽입/삭제 비용이 큼 (O(n)) |
구현이 간단하고 직관적 | 메모리 사용이 비효율적일 수 있음 (사용하지 않는 공간) |
이러한 특성 때문에 배열은 데이터의 개수가 미리 알려져 있고, 빈번한 검색이 필요하지만 삽입과 삭제가 적은 상황에 적합하다. 예를 들어, 특정 학생의 성적을 학번(인덱스)으로 빠르게 조회해야 하는 경우나 이미지 픽셀 데이터를 저장하는 경우에 유용하게 사용된다.
배열의 주요 연산에 대한 시간 복잡도는 다음과 같다. 접근, 검색, 삽입, 삭제 연산의 효율성이 각각 다르다.
연산 | 평균 시간 복잡도 | 설명 |
|---|---|---|
접근(Access) | O(1) | 인덱스를 알고 있으면 해당 위치의 요소에 즉시 접근할 수 있다. |
검색(Search) | O(n) | 특정 값을 찾기 위해 최악의 경우 모든 요소를 순차적으로 확인해야 한다. |
삽입(Insertion) | O(n) | 배열의 처음이나 중간에 요소를 삽입하면, 이후 요소들을 이동시켜야 할 수 있다. |
삭제(Deletion) | O(n) | 배열의 처음이나 중간에서 요소를 삭제하면, 빈 공간을 채우기 위해 이후 요소들을 이동시켜야 할 수 있다. |
배열 끝에서의 삽입과 삭제는 예외적으로 시간 복잡도 O(1)에 수행될 수 있다. 이는 요소를 이동할 필요가 없기 때문이다. 그러나 배열이 가득 찬 상태에서 끝에 삽입하려면, 더 큰 배열을 새로 할당하고 모든 요소를 복사하는 작업이 필요해 O(n)의 시간이 소요될 수 있다.
배열은 가장 기본적이고 널리 사용되는 선형 자료구조이다. 그 단순함과 효율성 덕분에 다양한 프로그래밍 분야에서 핵심적인 역할을 한다.
가장 일반적인 사용 사례는 동일한 타입의 데이터를 순차적으로 저장하고 인덱스를 통해 빠르게 접근해야 하는 경우이다. 예를 들어, 학생 100명의 점수를 저장하거나 이미지 픽셀 데이터를 표현할 때 배열이 적합하다. 많은 프로그래밍 언어에서 문자열은 문자 배열로 구현된다. 또한, 다른 복잡한 자료구조의 기반이 되기도 하는데, 힙, 해시 테이블의 버킷, 행렬 및 그래프의 인접 행렬 표현 등이 배열을 바탕으로 구축된다.
알고리즘에서도 배열은 빈번히 사용된다. 정렬 알고리즘(예: 버블 정렬, 퀵 정렬, 병합 정렬)은 대부분 배열에 저장된 데이터를 대상으로 작동한다. 탐색 알고리즘인 이진 탐색 역시 정렬된 배열에서 높은 효율을 발휘한다. 동적 프로그래밍에서는 중간 결과를 저장하기 위해 배열(또는 행렬)을 사용하는 경우가 많다.
시스템 수준에서 배열은 메모리 관리의 기본 단위이다. 운영체제의 프로세스 제어 블록 목록이나 파일 디스크립터 테이블과 같은 핵심 자료구조를 구현할 때 배열이 채택된다. 하드웨어와 가까운 저수준 프로그래밍, 예를 들어 임베디드 시스템에서 센서 데이터를 버퍼링하거나 프레임 버퍼를 관리할 때도 배열이 선호된다.
연결 리스트는 자료구조의 일종으로, 데이터 요소를 노드라는 단위로 저장하고, 각 노드가 다음 노드를 가리키는 포인터를 통해 논리적인 순서를 연결하는 선형 구조이다. 배열과 달리 물리적인 메모리 상에서 연속적으로 위치할 필요가 없으며, 런타임에 크기를 동적으로 조절할 수 있다는 특징을 가진다.
주요 구현 형태로는 단일 연결 리스트, 이중 연결 리스트, 원형 연결 리스트가 있다. 단일 연결 리스트는 각 노드가 데이터와 다음 노드를 가리키는 하나의 포인터로 구성되어 한 방향으로만 순회가 가능하다. 이중 연결 리스트는 각 노드에 이전 노드와 다음 노드를 가리키는 두 개의 포인터가 있어 양방향 순회가 가능하며, 특정 노드의 삭제나 삽입이 더 효율적이다. 원형 연결 리스트는 리스트의 마지막 노드가 첫 번째 노드를 가리키는 구조로, 순환적인 데이터 처리가 필요한 경우에 유용하다.
연결 리스트의 장단점과 시간 복잡도는 다음과 같이 정리할 수 있다.
연산 | 시간 복잡도 | 설명 |
|---|---|---|
접근 | O(n) | 특정 인덱스의 요소에 접근하려면 처음부터 순차 탐색해야 한다. |
탐색 | O(n) | 특정 값을 가진 노드를 찾기 위해 선형 탐색이 필요하다. |
삽입 (시작) | O(1) | 리스트의 시작 부분에 삽입하는 것은 상수 시간이 소요된다. |
삽입 (끝) | O(1) 또는 O(n) | 테일 포인터를 유지하면 O(1), 그렇지 않으면 끝까지 탐색해야 하므로 O(n)이다. |
삭제 (시작) | O(1) | 리스트의 첫 번째 노드를 삭제하는 것은 상수 시간이 소요된다. |
삭제 (끝) | O(1) 또는 O(n) |
주요 장점으로는 동적 메모리 할당을 통해 필요할 때마다 노드를 추가하거나 제거할 수 있어 메모리 사용이 유연하다는 점이 있다. 또한, 데이터의 삽입과 삭제가 빈번한 경우, 특히 리스트의 앞부분에서 이루어질 때 배열에 비해 매우 효율적이다. 반면, 단점으로는 각 요소마다 추가적인 포인터 저장 공간이 필요해 메모리 오버헤드가 발생하며, 캐시 지역성이 낮아 순차 접근 시 성능이 배열보다 떨어질 수 있다. 또한 인덱스를 통한 임의 접근이 불가능하다.
단일 연결 리스트는 연결 리스트의 가장 기본적인 형태이다. 각 노드는 저장할 데이터와 다음 노드를 가리키는 하나의 포인터 또는 참조로 구성된다. 이 포인터를 통해 노드들이 일렬로 연결되어 선형적인 구조를 형성한다. 리스트의 시작점을 나타내는 헤드 포인터가 존재하며, 마지막 노드의 포인터는 NULL 값을 가져 리스트의 끝을 표시한다.
주요 연산의 시간 복잡도는 접근 방식에 따라 다르다. 특정 인덱스의 노드에 접근하거나 검색하는 연산은 순차 탐색이 필요하므로 시간 복잡도가 O(n)이다. 반면, 리스트의 시작 부분에서 노드를 삽입하거나 삭제하는 연산은 헤드 포인터만 변경하면 되므로 O(1)의 상수 시간에 수행된다. 리스트의 중간이나 끝에 삽입하려면 원하는 위치까지 순차적으로 이동해야 하므로 O(n)의 시간이 소요된다.
연산 | 시간 복잡도 | 설명 |
|---|---|---|
접근(Access) | O(n) | 인덱스를 통한 직접 접근 불가, 순차 탐색 필요 |
탐색(Search) | O(n) | 데이터 값을 찾기 위해 처음부터 순회 |
헤드 삽입(Insert at head) | O(1) | 새 노드를 생성하고 헤드 포인터를 변경 |
헤드 삭제(Delete at head) | O(1) | 헤드 포인터를 다음 노드로 변경 |
중간/끝 삽입/삭제 | O(n) | 삽입/삭제 위치까지의 선행 노드를 탐색해야 함 |
단일 연결 리스트는 동적 메모리 할당에 유리하며, 데이터의 삽입과 삭제가 빈번하게 일어나는 상황에서 배열보다 효율적이다. 그러나 역방향 순회가 불가능하고, 각 노드가 추가적인 포인터 공간을 필요로 하므로 메모리 오버헤드가 발생한다는 단점도 있다.
이중 연결 리스트는 각 노드가 데이터와 두 개의 포인터를 가지는 연결 리스트의 한 종류이다. 각 노드는 자신의 데이터를 저장하는 data 필드와, 이전 노드를 가리키는 prev 포인터, 다음 노드를 가리키는 next 포인터로 구성된다. 이 구조는 단일 연결 리스트와 달리 양방향으로 탐색이 가능하다는 특징을 가진다.
주요 연산의 시간 복잡도는 다음과 같다. 접근과 탐색은 헤드나 테일에서 시작해 선형 탐색이 필요하므로 O(n)이다. 그러나 리스트의 양 끝에서 노드를 삽입하거나 삭제하는 연산은 O(1)의 시간에 수행된다. 특정 노드의 앞이나 뒤에 새로운 노드를 삽입하는 연산도 해당 노드에 대한 참조가 주어지면 포인터 조작만으로 가능하므로 O(1)이다.
연산 | 시간 복잡도 | 설명 |
|---|---|---|
접근(Access) | O(n) | 인덱스로 특정 위치의 노드를 찾는다. |
탐색(Search) | O(n) | 특정 값을 가진 노드를 찾는다. |
헤드/테일 삽입 | O(1) | 리스트의 시작 또는 끝에 노드를 추가한다. |
헤드/테일 삭제 | O(1) | 리스트의 시작 또는 끝 노드를 제거한다. |
특정 노드 앞/뒤 삽입[1] | O(1) | 포인터 조작만으로 가능하다. |
특정 노드 삭제[2] | O(1) | 포인터 조작만으로 가능하다. |
이중 연결 리스트의 가장 큰 장점은 양방향 탐색이 가능하여 특정 노드의 이전 노드를 즉시 알 수 있다는 점이다. 이는 노드 삭제나 리스트 순회를 더 유연하게 만든다. 반면, 각 노드가 추가적인 포인터를 저장해야 하므로 단일 연결 리스트에 비해 메모리 오버헤드가 더 크다는 단점이 있다. 이 구조는 이전과 다음 노드에 대한 빠른 접근이 모두 필요한 경우, 예를 들어 브라우저 히스토리의 앞으로/뒤로 가기 기능이나 음악 플레이어의 재생 목록 관리 등에 활용된다.
원형 연결 리스트는 단일 연결 리스트나 이중 연결 리스트와 달리, 리스트의 마지막 노드가 다시 첫 번째 노드를 가리키는 구조를 가진다. 즉, 노드들이 원형으로 연결되어 있어 머리(head)와 꼬리(tail)의 구분이 모호해질 수 있다. 일반적으로는 머리 노드 하나만을 포인터로 관리하며, 이 머리 노드의 다음 노드가 리스트의 꼬리가 된다.
주요 특징은 다음과 같다.
순환 구조: 리스트의 끝이 없어, 시작점부터 순회(traversal)를 시작하면 다시 시작점으로 돌아올 수 있다.
단일 원형과 이중 원형: 단일 연결 리스트를 기반으로 하면 단일 원형 연결 리스트, 이중 연결 리스트를 기반으로 하면 이중 원형 연결 리스트가 된다.
삽입/삭제 효율성: 리스트의 시작이나 끝에 노드를 삽입하거나 삭제하는 연산이 상수 시간(O(1))에 가능하다. 특히 머리 포인터만으로도 꼬리에 접근이 용이하다는 장점이 있다.
연산 | 시간 복잡도 | 비고 |
|---|---|---|
머리/꼬리 삽입 | O(1) | |
머리/꼬리 삭제 | O(1) | |
임의 접근 | O(n) | |
특정 값 탐색 | O(n) |
원형 연결 리스트는 연결 리스트의 끝을 다시 처음과 연결함으로써 얻는 순환성을 활용하는 경우에 유용하다. 대표적인 사용 사례로는 운영체제의 프로세스 스케줄링을 위한 라운드 로빈 큐 구현, 멀티플레이어 게임에서 플레이어 순서 관리, 반복적으로 순회해야 하는 데이터 버퍼 등이 있다.
연결 리스트의 장점은 동적 메모리 할당을 통해 크기를 유연하게 조절할 수 있다는 점이다. 요소의 삽입과 삭제가 상대적으로 효율적이며, 특히 리스트의 시작 부분에서의 연산은 시간 복잡도 O(1)로 수행된다. 물리적으로 연속된 메모리 공간을 필요로 하지 않으므로, 메모리 단편화 문제에 덜 민감하다.
반면, 단점으로는 임의 접근이 불가능하다는 점을 들 수 있다. i번째 요소에 접근하려면 헤드 포인터부터 순차적으로 탐색해야 하므로, 접근 시간 복잡도는 O(n)이다. 또한, 각 노드가 데이터와 하나 이상의 포인터를 저장해야 하므로, 데이터 자체를 저장하는 데 필요한 메모리 외에 추가적인 포인터 저장 공간이 필요하다.
주요 연산에 대한 시간 복잡도는 다음과 같이 정리할 수 있다.
연산 | ||
|---|---|---|
접근 (Access) | O(n) | O(n) |
탐색 (Search) | O(n) | O(n) |
전방 삽입 (Insert at head) | O(1) | O(1) |
후방 삽입 (Insert at tail) | O(1)[5] | |
임의 위치 삽입 (Insert at middle) | O(n) (탐색 시간) | O(n) (탐색 시간) |
전방 삭제 (Delete at head) | O(1) | O(1) |
임의 위치 삭제 (Delete at middle) | O(n) (탐색 시간) | O(1)[6] |
이중 연결 리스트는 이전 노드에 대한 포인터를 추가로 저장함으로써 역방향 순회와 특정 노드 삭제를 더 효율적으로 할 수 있지만, 그 대신 더 많은 메모리를 사용한다는 트레이드오프가 존재한다.
스택은 후입선출 원리에 따라 데이터를 관리하는 선형 자료구조이다. 데이터의 삽입과 삭제가 한쪽 끝에서만 일어나며, 이 끝을 스택 포인터 또는 탑이라고 부른다.
주요 연산으로는 푸시와 팝이 있다. 푸시는 스택의 탑에 새로운 요소를 추가하는 연산이고, 팝은 탑에 있는 요소를 제거하고 반환하는 연산이다. 또한 탑의 요소를 제거하지 않고 확인만 하는 피크 연산과 스택이 비어 있는지 확인하는 이스엠티 연산도 자주 사용된다. 이러한 연산의 시간 복잡도는 모두 O(1)이다.
스택은 주로 배열이나 연결 리스트를 이용하여 구현한다. 배열로 구현할 경우 메모리가 연속적으로 할당되어 접근 속도가 빠르지만, 크기가 고정될 수 있다는 단점이 있다. 연결 리스트로 구현할 경우 크기에 제한이 없이 유연하지만, 각 노드에 추가적인 포인터 공간이 필요하고 상대적으로 느릴 수 있다.
스택은 함수 호출과 복귀 주소 관리, 재귀 알고리즘, 괄호 검사, 후위 표기법 계산, 실행 취소 기능, 깊이 우선 탐색 등 다양한 분야에서 활용된다.
스택은 후입선출 원칙에 따라 데이터를 관리하는 선형 자료구조이다. 후입선출은 'Last In, First Out'의 약자로, 가장 나중에 들어온 데이터가 가장 먼저 나가는 방식을 의미한다.
이 원리는 일상생활에서 접할 수 있는 여러 예시로 비유된다. 접시를 쌓아 올렸을 때, 가장 위에 놓인 마지막 접시를 가장 먼저 꺼내는 것과 같다. 또는 프링글스 통에 과자를 쌓아 넣고 꺼낼 때도 가장 위에 있는, 마지막으로 넣은 과자를 먼저 먹게 된다. 이러한 동작 방식을 프로그래밍적으로 정의하면, 데이터의 추가(푸시)는 스택의 최상단(탑)에서만 일어나고, 데이터의 제거(팝) 또한 최상단에서만 일어난다.
후입선출 구조는 특정한 순서로 작업을 실행하거나 되돌려야 하는 상황에 매우 유용하다. 대표적인 활용 예로는 함수 호출 시의 호출 스택, 문서 편집기의 실행 취소 기능, 후위 표기법 수식의 계산, 그리고 깊이 우선 탐색 알고리즘이 있다. 이러한 모든 경우에, 가장 최근에 처리된 항목에 대한 접근과 제거가 빈번하게 요구되며, 스택이 이에 완벽하게 부합한다.
스택은 배열 또는 연결 리스트를 기반으로 구현할 수 있다. 각 구현 방식은 고유한 장단점을 가지며, 사용되는 상황에 따라 선택된다.
배열을 이용한 구현은 연속된 메모리 공간에 데이터를 저장한다. 스택의 최대 크기를 사전에 정의해야 하며, top 인덱스를 통해 가장 최근에 삽입된 요소의 위치를 추적한다. push와 pop 연산은 top 인덱스를 조정하는 방식으로 이루어지기 때문에 시간 복잡도가 O(1)로 매우 효율적이다. 그러나 배열의 크기가 고정되어 있어 스택이 가득 차면 더 이상 요소를 추가할 수 없는 단점이 있다. 이를 해결하기 위해 동적 배열을 사용할 수 있지만, 확장 과정에서 추가적인 비용이 발생한다.
연결 리스트를 이용한 구현은 노드들이 포인터로 연결되는 방식을 사용한다. 각 노드는 데이터와 다음 노드를 가리키는 포인터를 포함한다. 스택의 top은 리스트의 헤드(시작 노드)를 가리키게 된다. 이 방식은 메모리가 허용하는 한 스택의 크기를 동적으로 조절할 수 있어 유연성이 높다. 모든 연산이 리스트의 헤드에서 이루어지므로, 삽입과 삭제 역시 O(1)의 시간 복잡도를 가진다. 다만, 각 노드가 추가적인 포인터 공간을 필요로 하기 때문에 배열에 비해 메모리 오버헤드가 크다는 단점이 있다.
다음 표는 두 구현 방법의 주요 차이점을 정리한 것이다.
특성 | 배열 기반 스택 | 연결 리스트 기반 스택 |
|---|---|---|
메모리 할당 | 정적(또는 동적 배열) | 동적 |
크기 제한 | 최대 크기가 존재할 수 있음 | 메모리 한도까지 유연함 |
메모리 오버헤드 | 낮음 | 각 노드마다 포인터 공간 필요 |
접근 속도 | 빠른 인덱스 접근 | 순차 접근 |
주요 연산 복잡도 | push/pop: O(1) | push/pop: O(1) |
일반적으로 최대 크기를 예측할 수 있고 빠른 접근이 중요한 경우 배열이, 크기가 변동적이고 메모리 효율성보다 유연성이 더 중요한 경우 연결 리스트가 선호된다.
스택의 주요 연산은 Push와 Pop이다. Push는 데이터를 스택의 최상위(Top)에 추가하는 연산이고, Pop은 최상위 데이터를 제거하며 반환하는 연산이다. 이 외에도 최상위 데이터를 제거하지 않고 확인하는 Peek 연산과 스택이 비어 있는지 확인하는 IsEmpty 연산이 일반적으로 사용된다. 스택은 재귀 알고리즘의 호출 기록을 관리하는 콜 스택, 웹 브라우저의 뒤로/앞으로 가기 기능, 문서 편집기의 실행 취소(Undo) 기능, 그리고 후위 표기법 수식 계산 등에 활용된다.
큐의 주요 연산은 Enqueue와 Dequeue이다. Enqueue는 데이터를 큐의 후미(Rear)에 추가하는 연산이고, Dequeue는 데이터를 큐의 전미(Front)에서 제거하며 반환하는 연산이다. Peek(또는 Front) 연산으로 첫 번째 항목을 확인하거나, 큐가 비었는지/가득 찼는지 확인하는 연산도 함께 사용된다. 큐는 순서를 보장해야 하는 다양한 시스템에서 활용된다. 대표적인 예로 프린터의 작업 스케줄링, CPU의 프로세스 스케줄링, 너비 우선 탐색 알고리즘, 그리고 실시간 데이터 처리를 위한 메시지 큐 시스템을 들 수 있다.
연산 | 스택 (Stack) | 큐 (Queue) |
|---|---|---|
데이터 추가 | Push (Top에 추가) | Enqueue (Rear에 추가) |
데이터 제거 | Pop (Top에서 제거) | Dequeue (Front에서 제거) |
데이터 확인 | Peek (Top 확인) | Peek/Front (Front 확인) |
주요 활용 분야 | 실행 취소, 후위 표기법 계산, 재귀/함수 호출 관리 | 작업 스케줄링, BFS 알고리즘, 메시지 버퍼링 |
큐는 선입선출 원칙으로 동작하는 추상 자료형이다. 데이터는 큐의 뒤에서 추가되고, 앞에서 제거된다. 이는 사람이 줄을 서는 것과 유사하여, 먼저 도착한 데이터가 먼저 처리된다.
큐의 주요 연산은 enqueue와 dequeue이다. Enqueue는 데이터를 큐의 뒤에 추가하는 작업이며, dequeue는 큐의 앞에서 데이터를 제거하고 반환하는 작업이다. 이 외에도 큐의 앞 원소를 확인하는 peek 연산과 큐가 비었는지 확인하는 연산이 일반적으로 제공된다. 큐는 배열 또는 연결 리스트를 이용하여 구현할 수 있다.
일반적인 선형 배열로 구현된 큐는 dequeue 연산 시 모든 요소를 앞으로 이동시켜야 하는 비효율성이 있다. 이를 해결하기 위해 원형 큐가 사용된다. 원형 큐는 배열을 원형으로 간주하여, 배열의 끝에 도달하면 다시 처음으로 돌아가 공간을 재활용한다. 이는 배열의 크기가 고정되어 있을 때 효율적인 메모리 사용을 가능하게 한다.
큐의 변형 중 하나인 우선순위 큐는 각 요소에 우선순위를 부여하여, 높은 우선순위를 가진 요소가 먼저 제거된다. 내부적으로는 힙 자료구조를 주로 사용하여 구현된다. 큐는 너비 우선 탐색, 버퍼 관리, 프로세스 스케줄링, 프린터 작업 대기열 등 다양한 컴퓨팅 분야에서 활용된다.
큐는 선입선출 원칙을 따르는 추상적인 자료형이다. 이 원칙은 데이터가 저장된 순서대로 처리됨을 의미하며, 일상 생활의 대기 줄에 비유할 수 있다. 먼저 줄을 선 사람이 먼저 서비스를 받는 것처럼, 큐에 먼저 추가된 데이터가 먼저 제거된다.
이 원리를 구현하기 위해 큐는 두 가지 주요 지점을 관리한다. 데이터가 추가되는 곳은 리어 또는 테일이라고 부르며, 데이터가 제거되는 곳은 프론트 또는 헤드라고 부른다. 인큐 연산은 리어에 새로운 요소를 삽입하고, 디큐 연산은 프론트에 있는 요소를 제거하며 반환한다.
FIFO 구조는 순차적인 처리 흐름이 필요한 다양한 컴퓨팅 상황에서 핵심적으로 적용된다. 대표적인 예로 프로세스 스케줄링, 프린터의 작업 대기열, 네트워크 패킷 버퍼링, 너비 우선 탐색 알고리즘 등이 있다. 이러한 모든 경우에 작업이나 데이터의 도착 순서를 유지하는 것이 중요하다.
일반 큐는 가장 기본적인 형태의 큐로, 선입선출 원칙을 따르는 선형 데이터 구조이다. 주로 배열 또는 연결 리스트를 기반으로 구현된다. 일반 큐는 데이터가 삽입되는 리어와 데이터가 제거되는 프론트라는 두 개의 포인터(또는 인덱스)를 관리한다. 요소가 추가되면 리어가 이동하고, 요소가 제거되면 프론트가 이동한다.
그러나 배열로 구현된 일반 큐는 "가비지 공간" 문제를 겪을 수 있다. 프론트 앞쪽의 공간이 비어 있음에도, 리어가 배열의 끝에 도달하면 더 이상의 삽입이 불가능한 것으로 판단하는 경우가 발생한다[7]. 이 문제를 해결하기 위해 고안된 것이 원형 큐이다.
원형 큐는 배열을 원형으로 간주하여 공간을 효율적으로 재사용한다. 리어가 배열의 마지막 인덱스에 도달하면, 다음 삽입 위치를 배열의 처음(인덱스 0)으로 순환시킨다. 이를 통해 프론트 앞의 빈 공간을 다시 활용할 수 있다. 원형 큐의 구현은 일반 큐와 유사하지만, 포인터의 이동을 계산할 때 모듈로 연산을 사용하는 것이 특징이다.
구분 | 일반 큐 | 원형 큐 |
|---|---|---|
구조 | 선형 구조 | 원형(환형) 구조 |
공간 활용 | 가비지 공간 발생 가능 | 모든 공간을 순환적으로 재사용 |
포인터 이동 | 단순 증가 | 모듈로 연산을 통한 순환 |
포화 상태 판단 | 리어 == 배열 크기 - 1 | (리어 + 1) % 배열 크기 == 프론트 |
장점 | 구현이 직관적 | 메모리 공간을 효율적으로 관리 |
원형 큐는 버퍼, CPU 스케줄링, 스트리밍 데이터 처리 등 연속적인 데이터 흐름을 관리해야 하는 시스템에서 널리 사용된다.
우선순위 큐는 큐의 변형으로, 각 요소가 연관된 우선순위를 가지며, 높은 우선순위를 가진 요소가 낮은 우선순위 요소보다 먼저 제거되는 추상 자료형이다. 일반적인 FIFO 규칙을 따르지 않는다는 점에서 일반 큐와 구별된다. 요소의 추가는 자유롭지만, 제거는 항상 현재 큐에 있는 요소 중 가장 높은(또는 가장 낮은) 우선순위를 가진 요소에서 발생한다.
주요 연산은 일반 큐와 유사하게 요소 추가(enqueue)와 요소 제거(dequeue)를 포함하지만, 내부적으로 우선순위에 따른 정렬 또는 선택 메커니즘이 필요하다. 일반적인 구현 방법으로는 이진 힙, 균형 이진 탐색 트리, 또는 정렬되지 않은 연결 리스트를 사용한다. 이 중 이진 힙을 기반으로 한 구현이 가장 일반적이며, 효율적인 연산 성능을 제공한다[8].
연산 | 설명 | 일반적인 시간 복잡도 (이진 힙 기준) |
|---|---|---|
삽입(Insert) | 새로운 요소를 우선순위에 맞는 위치에 추가한다. | O(log n) |
최대/최소 값 확인(Peek) | 가장 높은(또는 낮은) 우선순위 요소를 제거하지 않고 반환한다. | O(1) |
삭제(Extract-Max/Min) | 가장 높은(또는 낮은) 우선순위 요소를 제거하고 반환한다. | O(log n) |
우선순위 큐는 다양한 실전 응용 분야에서 사용된다. 대표적으로 다익스트라 알고리즘과 같은 그래프 최단 경로 알고리즘, 허프만 코딩과 같은 데이터 압축 알고리즘, 운영체제의 작업 스케줄링, 그리고 이벤트 구동 시뮬레이션에서 널리 활용된다. 또한, 힙 정렬 알고리즘은 우선순위 큐를 사용하여 정렬을 수행하는 대표적인 예시이다.
스택의 주요 연산은 Push와 Pop이다. Push는 데이터를 스택의 최상위(Top)에 추가하는 연산이고, Pop은 최상위 데이터를 제거하며 반환하는 연산이다. 이외에도 최상위 데이터를 제거하지 않고 확인하는 Peek 연산과 스택이 비어 있는지 확인하는 연산이 일반적으로 사용된다. 스택은 재귀 알고리즘의 호출 기록을 관리하는 콜 스택, 웹 브라우저의 뒤로/앞으로 가기 기능, 깊이 우선 탐색 알고리즘, 그리고 수식의 괄호 검사나 후위 표기법 계산 등에 널리 활용된다.
큐의 주요 연산은 Enqueue와 Dequeue이다. Enqueue는 데이터를 큐의 맨 뒤(Rear)에 추가하고, Dequeue는 큐의 맨 앞(Front)에 있는 데이터를 제거하며 반환한다. Peek 연산으로 Front의 데이터를 확인할 수 있다. 큐는 순서를 보장해야 하는 처리 시스템에서 핵심적으로 사용된다. 대표적인 활용 사례로는 프린터의 작업 대기열, CPU 스케줄링, 너비 우선 탐색 알고리즘, 그리고 실시간 데이터 스트림을 처리하는 메시지 큐나 버퍼 등이 있다.
연산 | 스택 (Stack) | 큐 (Queue) |
|---|---|---|
데이터 추가 | Push (Top에 추가) | Enqueue (Rear에 추가) |
데이터 제거/반환 | Pop (Top에서 제거) | Dequeue (Front에서 제거) |
데이터 확인 | Peek (Top 확인) | Peek (Front 확인) |
대표적 활용 | 함수 호출 관리, 괄호 검사, DFS | 작업 스케줄링, 대기열 관리, BFS |
이러한 연산들의 조합은 더 복잡한 자료구조나 알고리즘의 기초를 이룬다. 예를 들어, 스택 두 개를 이용하여 큐를 구현하거나, 우선순위 큐를 이용하여 힙 자료구조를 구성하는 등의 방식으로 확장되어 사용된다.
배열과 연결 리스트는 선형 데이터를 저장하는 기본적인 자료구조이지만, 내부 구현과 특성에서 근본적인 차이를 보인다. 배열은 메모리 상에 연속된 공간을 할당받아 요소를 저장하며, 인덱스를 통한 직접 접근이 가능하다. 이로 인해 특정 위치의 요소를 읽거나 변경하는 작업은 상수 시간 O(1)에 수행된다. 반면, 연결 리스트는 노드들이 포인터로 연결되어 있으며, 메모리상에 불연속적으로 위치할 수 있다. k번째 요소에 접근하려면 처음부터 순차적으로 탐색해야 하므로 접근 시간은 O(n)이다. 배열의 크기는 고정적이어서 확장이 어렵지만, 연결 리스트는 런타임에 노드를 동적으로 추가하거나 제거하기 쉽다.
스택과 큐는 모두 요소의 추가와 삭제가 한쪽 또는 양쪽 끝에서만 발생하는 제한적인 선형 자료구조이다. 핵심 차이는 데이터 처리 순서에 있다. 스택은 LIFO 원리에 따라 가장 나중에 들어온 요소가 가장 먼저 제거된다. 이는 함수 호출 스택, 재귀 알고리즘, 역폴란드 표기법 계산기, 실행 취소 기능 등에 활용된다. 반면, 큐는 FIFO 원리를 따르며, 먼저 들어온 요소가 먼저 나간다. 이는 작업 스케줄링, 프린터 대기열, 너비 우선 탐색, 메시지 큐 등 순서를 보존해야 하는 상황에 적합하다.
메모리 효율성과 성능 측면에서 각 자료구조는 서로 다른 장단점을 지닌다. 배열은 요소 하나당 오버헤드가 없고 참조 지역성이 좋아 캐시 히트율이 높아 빠른 접근 속도를 보인다. 그러나 미리 큰 공간을 할당하면 메모리 낭비가, 작게 할당하면 확장 비용이 발생할 수 있다. 연결 리스트는 필요한 만큼만 메모리를 사용하지만, 각 노드마다 포인터 저장 공간이 추가로 필요하다. 삽입과 삭제는 포인터 조작만으로 O(1)에 가능하지만, 접근이나 탐색에는 선형 시간이 소요된다. 스택과 큐는 이러한 기본 구조 위에 특정 연산 제약을 추가한 추상 데이터 타입으로, 내부 구현이 배열 또는 연결 리스트에 따라 성능 특성이 결정된다.
배열과 연결 리스트는 모두 선형 자료구조로, 데이터 요소를 순차적으로 저장한다. 그러나 내부 구현 방식과 그에 따른 특성은 근본적으로 다르다. 배열은 메모리 상에 연속된 공간을 할당받아 요소를 저장하는 반면, 연결 리스트는 각 요소(노드)가 데이터와 다음 노드를 가리키는 포인터로 구성되어 불연속적인 메모리 공간에 흩어져 저장된다.
이러한 구조적 차이는 각 자료구조의 장단점을 결정한다. 배열은 인덱스를 통한 임의 접근이 가능하여 특정 위치의 데이터 접근 속도가 O(1)로 매우 빠르다. 그러나 배열의 크기는 선언 시 고정되며, 중간에 요소를 삽입하거나 삭제할 경우 많은 요소를 이동시켜야 하므로 시간 복잡도가 O(n)이다. 반면, 연결 리스트는 요소의 추가와 삭제가 해당 위치의 포인터만 변경하면 되므로 O(1)의 시간이 소요된다. 그러나 특정 요소에 접근하려면 처음부터 순차적으로 탐색해야 하므로 접근 시간 복잡도는 O(n)이다.
다음 표는 두 자료구조의 주요 연산에 대한 시간 복잡도를 비교한다.
연산 | 배열 | 연결 리스트 |
|---|---|---|
접근 (Access) | O(1) | O(n) |
탐색 (Search) | O(n) | O(n) |
삽입 (Insertion) | 끝: O(1), 중간/처음: O(n) | 처음/끝: O(1), 중간: O(n)[9] |
삭제 (Deletion) | 끝: O(1), 중간/처음: O(n) | 처음/끝: O(1), 중간: O(n)[10] |
선택 기준은 사용 사례의 요구사항에 따라 달라진다. 데이터 접근(읽기)이 빈번하고 크기가 예측 가능한 경우 배열이 유리하다. 반면, 데이터의 삽입과 삭제가 빈번하게 발생하거나 크기가 동적으로 변하는 경우 연결 리스트가 더 효율적이다. 또한, 메모리 관리 측면에서 배열은 미리 큰 공간을 할당받아 낭비가 발생할 수 있지만, 연결 리스트는 필요할 때마다 노드를 동적으로 할당하여 메모리를 효율적으로 사용한다.
스택과 큐는 모두 선형 자료구조이지만, 데이터의 삽입과 삭제 순서를 규정하는 원리가 근본적으로 다르다. 스택은 LIFO 원칙을 따르는 반면, 큐는 FIFO 원칙을 따른다. 이 차이는 각 자료구조의 추상적 개념과 물리적 구현 모두에 영향을 미친다.
주요 연산의 측면에서도 차이가 명확하다. 스택의 핵심 연산은 push(삽입)와 pop(삭제)이며, 둘 다 스택 포인터가 가리키는 스택의 꼭대기에서만 일어난다. 반면 큐의 핵심 연산은 enqueue(삽입)와 dequeue(삭제)로, 삽입은 큐의 뒤에서, 삭제는 큐의 앞에서 수행된다. 이로 인해 스택은 최근에 추가된 데이터에 대한 접근이 빠르지만, 큐는 저장된 순서를 정확히 유지해야 하는 상황에 적합하다.
특성 | 스택 (LIFO) | 큐 (FIFO) |
|---|---|---|
삽입/삭제 위치 | 한쪽 끝(Top) | |
주요 연산 |
|
|
대표적 활용 예 | 함수 호출 관리*재귀 호출, 되돌리기 기능, 깊이 우선 탐색 | 작업 스케줄링*프린터 대기열, 메시지 큐, 너비 우선 탐색 |
주요 변형 | 단순 스택 |
이러한 구조적 차이는 알고리즘 설계에 직접적인 영향을 준다. 예를 들어, 깊이 우선 탐색 알고리즘은 탐색 경로를 되돌아가야 할 필요가 있어 스택을 사용하는 반면, 너비 우선 탐색 알고리즘은 동일한 깊이의 노드를 순차적으로 방문해야 하므로 큐를 사용한다. 시스템 수준에서는 스택이 재귀 호출과 실행 콘텍스트 관리에, 큐는 프로세스 스케줄링이나 데이터 버퍼링에 주로 활용된다.
배열은 연속된 메모리 공간에 데이터를 저장하기 때문에, 캐시 지역성이 우수하여 데이터 접근 속도가 매우 빠르다. 그러나 배열의 크기를 미리 정해야 하며, 크기 변경이 어려워 메모리 사용이 비효율적일 수 있다. 특히 크기를 초과하면 더 큰 배열을 새로 할당하고 데이터를 복사해야 하는 오버헤드가 발생한다.
반면 연결 리스트는 각 노드가 데이터와 다음 노드를 가리키는 포인터로 구성되어, 물리적으로 흩어져 있는 메모리 공간을 사용할 수 있다. 이로 인해 크기 변경이 자유로워 메모리 할당이 더 유연하지만, 각 노드마다 추가적인 포인터 저장 공간이 필요하고, 데이터 접근 시 포인터를 따라 이동해야 하므로 캐시 효율이 낮아 상대적으로 느리다.
자료구조 | 메모리 효율성 | 접근 성능 | 삽입/삭제 성능 | 특징 |
|---|---|---|---|---|
연속 할당으로 오버헤드 적음. 크기 고정 시 낭비 가능 | O(1)의 빠른 임의 접근 | 중간 삽입/삭제는 O(n), 끝에서는 O(1) | 캐시 친화적, 크기 조정 비용 큼 | |
동적 할당으로 유연하나, 노드당 포인터 오버헤드 있음 | 순차 접근만 가능하여 O(n) | O(1) (위치를 알고 있을 경우) | 동적 크기 조정 용이, 캐시 효율 낮음 | |
스택 (배열 기반) | 배열과 동일 | O(1) | O(1) (top에서만) | LIFO, 빠른 연산 |
스택 (연결 리스트 기반) | 연결 리스트와 동일 | O(1) (top만 접근) | O(1) | LIFO, 동적 크기 |
큐 (배열 기반) | 배열과 동일. 원형 큐는 공간 재활용 가능 | O(1) | O(1) (front/rear에서만) | FIFO, 배열의 빈 공간 관리 필요 |
큐 (연결 리스트 기반) | 연결 리스트와 동일 | O(1) (front만 접근) | O(1) | FIFO, 동적 크기 |
성능 측면에서, 빈번한 검색이나 인덱스 기반 접근이 필요한 경우 배열이 압도적으로 유리하다. 반면 데이터의 빈번한 삽입과 삭제, 특히 크기가 예측 불가능한 경우 연결 리스트 기반 구조가 더 나은 메모리 효율성을 보인다. 스택과 큐는 주로 끝단에서만 연산이 이루어지므로, 둘 다 배열이나 연결 리스트로 구현했을 때 핵심 연산의 시간 복잡도는 O(1)로 동일하다. 그러나 구현 방식에 따라 메모리 사용 패턴과 확장성에서 차이가 발생한다.
알고리즘 문제 해결에서 배열은 동적 프로그래밍의 메모이제이션 테이블이나 투 포인터 기법의 기본 구조로 자주 사용된다. 스택은 괄호 검사, 후위 표기법 계산, 깊이 우선 탐색의 보조 자료구조로 활용되며, 큐는 너비 우선 탐색이나 캐시 구현에 필수적이다. 연결 리스트는 LRU 캐시처럼 빈번한 삽입과 삭제가 필요한 문제를 해결하는 데 적합하다.
시스템 설계에서 스택은 함수 호출의 콜 스택, 되돌리기 기능, 식 평가에 사용된다. 큐는 메시지 큐, 작업 스케줄링, 프린터 스풀링과 같은 순차적 처리 시스템의 핵심이다. 원형 큐는 고정된 크기의 버퍼 관리, 예를 들어 스트리밍 데이터 처리에 효율적이다. 연결 리스트는 파일 시스템의 블록 관리나 해시 테이블의 체이닝 충돌 해결법으로 응용된다.
다양한 자료구조의 조합은 더 복잡한 시스템을 구성한다. 예를 들어, 우선순위 큐는 힙으로 구현되어 작업 스케줄러나 다익스트라 알고리즘에 사용된다. 배열과 해시 맵을 결합하면 빠른 랜덤 접근과 키 기반 검색이 모두 가능한 자료구조를 만들 수 있다. 데이터베이스의 인덱스는 B-트리나 해시 인덱스 등 특화된 자료구조를 기반으로 구축된다.
스택은 후입선출 구조를 활용하여 괄호 검사, 후위 표기법 계산, 깊이 우선 탐색의 재귀 호출을 대체하는 데 사용된다. 예를 들어, 올바른 괄호 문자열 판별 문제에서는 여는 괄호를 스택에 넣고, 닫는 괄호가 나올 때마다 스택의 최상단을 확인하여 짝이 맞는지 검사한다. 재귀 알고리즘을 스택을 이용한 반복문으로 변환하면 함수 호출 오버헤드를 줄일 수 있다.
큐는 선입선출 구조로, 너비 우선 탐색에서 현재 노드의 인접 노드를 방문할 때 필수적으로 사용된다. 또한 슬라이딩 윈도우 최대값 찾기와 같은 문제에서는 덱을 활용하여 효율적으로 해결할 수 있다. 원형 큐는 고정된 크기의 버퍼를 효율적으로 관리할 때 유용하다.
배열은 랜덤 액세스가 가능하여 이진 탐색이나 두 포인터 기법과 같은 알고리즘에 적합하다. 특정 구간의 합을 빠르게 구해야 하는 문제에서는 누적 합 배열을 미리 계산해 두는 전략이 자주 사용된다. 동적 프로그래밍에서도 중간 결과를 저장하는 데 배열이 주로 쓰인다.
자료구조 | 대표적인 활용 알고리즘 문제 유형 |
|---|---|
괄호 검사, 후위 표기법 계산, 히스토그램에서 가장 큰 직사각형, 일일 온도 | |
너비 우선 탐색, 슬라이딩 윈도우 최대값, 프린터 큐, 회전하는 큐 | |
두 수의 합, 구간 합 쿼리, 빗물 트래핑, 정렬된 배열의 이진 탐색 | |
두 정렬 리스트 병합, 역순 연결 리스트, LRU 캐시 구현[11] |
연결 리스트는 주로 다른 자료구조를 구현하는 데 사용되지만, LRU 캐시 설계 문제처럼 노드의 삽입과 삭제가 빈번한 시나리오에서 직접 활용되기도 한다. 각 자료구조의 특성을 이해하고 문제의 제약 조건(예: 데이터 크기, 연산 빈도)을 분석하여 적절한 것을 선택하는 것이 알고리즘 문제 해결의 핵심이다.
자료구조는 시스템 설계의 근간을 이루며, 각 구조의 고유한 특성은 특정 문제 영역에 최적화된 해결책을 제공한다. 배열은 메모리 상에 연속적으로 데이터를 저장하기 때문에 CPU 캐시 지역성의 이점을 극대화하여 빠른 접근 속도를 보인다. 이 특성은 처리 속도가 중요한 데이터베이스의 인덱스 구조나 대규모 배치 처리 시스템에서 데이터 블록을 효율적으로 관리하는 데 활용된다. 반면, 크기가 동적으로 변하는 데이터 집합을 관리할 때는 연결 리스트가 유리하다. 예를 들어, 운영체제의 프로세스 관리 테이블이나 파일 시스템의 디스크 블록 연결 정보 관리에서 널리 사용된다.
스택의 LIFO 원리는 시스템의 제어 흐름 관리에 핵심적이다. 대표적으로 함수 호출 시의 호출 스택은 복귀 주소와 지역 변수를 저장하여 실행 컨텍스트를 관리한다. 또한 웹 브라우저의 뒤로 가기 기능이나 문서 편집기의 실행 취소/다시 실행 기능은 스택을 통해 이전 상태를 순차적으로 저장하고 복구하는 방식으로 구현된다. 구문 분석이나 역폴란드 표기법 계산기 역시 스택을 기반으로 작동하는 대표적인 예시이다.
큐의 FIFO 원리는 순차적이고 공정한 처리 순서가 요구되는 시스템에서 필수적이다. 프린터의 작업 대기열, CPU 스케줄링의 라운드 로빈 큐, 네트워크 패킷의 버퍼링은 모두 큐를 통해 자원에 대한 요청을 순서대로 처리한다. 특히 메시지 큐는 마이크로서비스 아키텍처나 분산 시스템에서 서비스 간의 비동기 통신과 데이터 버퍼링, 부하 분산을 구현하는 데 광범위하게 사용된다. 우선순위 큐는 힙 자료구조로 구현되어 네트워크 트래픽 관리에서의 품질 보장이나 시뮬레이션 시스템에서의 이벤트 스케줄링에 적용된다.
이러한 자료구조들은 종종 복합적으로 사용되어 복잡한 시스템을 구성한다. 해시 테이블은 내부적으로 배열과 연결 리스트(또는 트리)를 결합하여 사용하며, 그래프 인접 리스트 표현에는 배열과 연결 리스트가 함께 쓰인다. 시스템 설계자는 처리할 데이터의 특성(크기 변동성, 접근 패턴, 순서 요구사항)과 성능 목표(시간 복잡도, 공간 복잡도)를 정확히 분석하여 가장 적합한 자료구조를 선택하거나 조합해야 한다.