C++ 컨테이너 어댑터: stack, queue 및 priority_queue의 이해와 구현

1. stack의 특징과 활용

C++ STL의 stack은 LIFO(Last-In-First-Out, 후입선출) 원칙을 따르는 컨테이너 어댑터입니다. 데이터의 삽입과 삭제가 한쪽 끝(Top)에서만 이루어지는 구조를 가집니다.

1.1 stack의 주요 인터페이스

함수 설명
push(val) 스택의 맨 위에 데이터를 추가
pop() 스택의 맨 위 데이터를 제거
top() 스택의 맨 위 데이터에 대한 참조 반환
empty() 스택이 비어있는지 확인
size() 스택 내 원소 개수 반환

1.2 실전 예제: 최소값을 유지하는 스택(Min Stack)

스택의 기본 기능과 더불어 현재 스택 내의 최소값을 상시 O(1)에 조회할 수 있는 구조입니다.

#include <stack>
#include <algorithm>

class CustomMinStack {
public:
    void push(int val) {
        main_stk.push(val);
        // 최소값 스택이 비었거나 새로운 값이 현재 최소값보다 작거나 같으면 기록
        if (min_stk.empty() || val <= min_stk.top()) {
            min_stk.push(val);
        }
    }

    void pop() {
        if (main_stk.top() == min_stk.top()) {
            min_stk.pop();
        }
        main_stk.pop();
    }

    int top() { return main_stk.top(); }
    int getMin() { return min_stk.top(); }

private:
    std::stack<int> main_stk;
    std::stack<int> min_stk;
};

2. queue의 특징과 활용

queue는 FIFO(First-In-First-Out, 선입선출) 구조를 가지며, 데이터는 뒤(Back)로 들어가고 앞(Front)으로 나오는 형태입니다.

2.1 queue의 주요 인터페이스

함수 설명
push(val) 큐의 끝에 데이터 추가
pop() 큐의 맨 앞 데이터 제거
front() 큐의 첫 번째 원소 참조
back() 큐의 마지막 원소 참조

2.2 queue의 시뮬레이션 구현

큐는 앞부분의 삭제(pop_front)가 빈번하므로 std::liststd::deque를 기반으로 구현하는 것이 효율적입니다.

#include <list>

namespace my_std {
    template<typename T, typename Container = std::list<T>>
    class queue {
    public:
        void push(const T& x) { storage.push_back(x); }
        void pop() { storage.pop_front(); }
        T& front() { return storage.front(); }
        T& back() { return storage.back(); }
        bool empty() const { return storage.empty(); }
        size_t size() const { return storage.size(); }
    private:
        Container storage;
    };
}

3. priority_queue의 이해

priority_queue는 모든 요소 중에서 우선순위가 가장 높은 요소가 항상 맨 앞에 위치하도록 설계된 컨테이너 어댑터입니다. 기본적으로 힙(Heap) 구조를 사용합니다.

3.1 사용자 정의 객체 정렬

기본적으로는 최대 힙(Max Heap)으로 동작하지만, 비교 연산자를 오버로딩하여 최소 힙(Min Heap)으로 변경할 수 있습니다.

#include <iostream>
#include <queue>
#include <vector>

struct Task {
    int id;
    int priority;

    // 우선순위가 높은(값이 큰) 것이 먼저 나오도록 설정
    bool operator<(const Task& other) const {
        return priority < other.priority;
    }
};

void checkPriority() {
    std::priority_queue<Task> pq;
    pq.push({1, 10});
    pq.push({2, 50});
    pq.push({3, 30});

    // priority가 50인 Task 2가 가장 먼저 출력됨
    std::cout << "Top ID: " << pq.top().id << std::endl;
}

3.2 힙 알고리즘을 이용한 직접 구현

우선순위 큐는 내부적으로 push_heappop_heap 원리를 따릅니다.

#include <vector>
#include <algorithm>

template<typename T, typename Container = std::vector<T>, typename Compare = std::less<T>>
class my_priority_queue {
public:
    void push(const T& x) {
        data.push_back(x);
        shiftUp(data.size() - 1);
    }

    void pop() {
        if (data.empty()) return;
        std::swap(data[0], data[data.size() - 1]);
        data.pop_back();
        shiftDown(0);
    }

    const T& top() const { return data[0]; }
    bool empty() const { return data.empty(); }

private:
    Container data;
    Compare comp;

    void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while (child > 0) {
            if (comp(data[parent], data[child])) {
                std::swap(data[child], data[parent]);
                child = parent;
                parent = (child - 1) / 2;
            } else break;
        }
    }

    void shiftDown(int parent) {
        size_t child = parent * 2 + 1;
        while (child < data.size()) {
            if (child + 1 < data.size() && comp(data[child], data[child + 1])) {
                child++;
            }
            if (comp(data[parent], data[child])) {
                std::swap(data[parent], data[child]);
                parent = child;
                child = parent * 2 + 1;
            } else break;
        }
    }
};

4. 컨테이너 어댑터와 deque

STL에서 stackqueue컨테이너 어댑터로 분류됩니다. 이는 독자적인 데이터 구조를 갖기보다는 기존의 vector, list, deque 등을 내부 컨테이너로 활용하여 특정 인터페이스만을 노출하기 때문입니다.

4.1 왜 deque가 기본 컨테이너인가?

std::deque(double-ended queue)는 다음과 같은 이유로 stackqueue의 기본 기반 컨테이너로 선택됩니다.

  • 효율적인 메모리 관리: vector처럼 단일 연속 메모리를 할당하지 않고, 여러 메모리 블록을 관리하므로 확장이 유연합니다.
  • 양단 삽입/삭제 성능: 앞뒤 모든 방향에서 O(1) 성능을 보장하므로 queuepop_front 연산에 최적화되어 있습니다.
  • 중간 삽입의 부재: 어차피 stackqueue는 중간 데이터 접근을 허용하지 않으므로, deque의 단점인 복잡한 반복자 연산이 문제가 되지 않습니다.

4.2 어댑터 구조의 유연성

사용자는 다음과 같이 필요에 따라 기반 컨테이너를 변경할 수 있습니다.

#include <stack>
#include <vector>
#include <list>

// vector를 기반으로 하는 stack
std::stack<int, std::vector<int>> v_stack;

// list를 기반으로 하는 queue
std::queue<int, std::list<int>> l_queue;

태그: cpp STL DataStructure Stack Queue

8월 5일 00:44에 게시됨