카테고리 없음

[Data Structure]Queue와 Stack

eigen96 2023. 4. 28. 18:34
728x90

기본원리

선형 데이터 구조로 데이터를 저장하고 접근하는 방식에 따라 구분함.

Stack

  • 스택은 Last-In-First-Out (LIFO) 원칙을 따르는 데이터 구조

Queue

  • 큐는 First-In-First-Out (FIFO) 원칙을 따르는 데이터 구조

시간복잡도

Stack

  • 전부 O(1)

Queue

  • 전부 O(1)

사용 예시

Stack

  • 함수 호출 관리
    • 함수가 호출되면 함수 관련 record(지역변수, 매개변수, 메타데이터 등)가 Call Stack에 push됨.
    • 함수가 반환되면 record가 스택에서 pop됨
  • DFS(깊이 우선 검색)
    • 그래프 및 트리에 대한 DFS 순회 알고리즘을 구현
    • 노드를 스택에 푸시하고 팝하면 역추적하기 전에 경로를 따라 모든 노드를 방문하여 깊이 우선 방식
  • 실행 취소/다시 실행 기능
    • 변경 기록을 앞뒤로 이동가능하게 해줌.
  • 후위 표기법(Postfix Notation)

Queue

  • 작업 스케줄링
    • 큐는 운영 체제와 같은 멀티태스킹 환경에서 작업을 관리.작업이 순서대로 실행되어 공정성과 예측 가능성을 제공
  • 인쇄 스풀링
    • 인쇄 작업 대기열을 유지하면 작업이 제출된 순서대로 인쇄
  • BFS(너비 우선 탐색)
    • 노드를 Queue에 추가하고 Queue에서 빼면 다음 Level로 이동하기 전에 지정된 수준의 모든 노드를 방문하여 너비 우선 방식으로 그래프 또는 트리를 탐색
  • 버퍼링
    • 데이터 스트리밍, 네트워크 패킷 관리 또는 키보드 입력 처리와 같은 다양한 애플리케이션에서 버퍼를 구현할 때 사용.
    • 들어오는 데이터의 대기열을 유지함으로써 데이터가 수신된 순서대로 처리되도록 하고 처리 지연으로 인한 데이터 손실을 방지

구현 방법

캐시 친화적(cache-friendly) CPU 캐시를 활용하는 알고리즘 또는 데이터 구조의 속성 ex) 자주 or 순차적으로 액세스되는 데이터가 인접한 메모리 블록에 저장될 때 캐시 친화적.

 

Stack(Array 구현시) Stack (Linked List 구현시)
장점
  • 메모리 할당은 연속적이며 캐시 친화적입니다.
  • 연결리스트 에 비해 메모리 오버헤드가 낮습니다.
  • 장점
    • 크기 조정 오버헤드가 없음
    • 푸시 및 팝 작업에 대한 작업 O(1) 
  •  단점
    • 배열이 가득 찬 경우 크기 조정이 필요하므로 최악의 시나리오에서 푸시 작업에 대해 O(n) 시간 복잡성이 발생
    • 배열이 원소 개수에 비해 너무 큰 경우 공간 낭비로 비효율
  • 단점
    • 각 원소의 포인터 저장으로 인한 메모리 오버헤드 발생.
    • 캐시 친화적이지 않은 비연속 메모리 할당

Queue(Array)

Queue(Linked List)

  • 장점
    • 스택을 배열로 구현했을 때와 같은 장점을 가짐.
  • 장점
    • 크기 조정 오버헤드가 없음.
    • enqueue 및 dequeue 작업에 대한 시간 O(1).
  • 단점
    • Queue에서 enqueue작업시 순환 버퍼를 사용하지 않으면 최악의 경우 O(n).
  • 단점
    • 각 원소의 포인터 저장으로 인한 메모리 오버헤드 발생.
    • 캐시 친화적이지 않은 비연속 메모리 할당

Code

Stack (Array)

#include <iostream>

struct Stack {
    int capacity;
    int top;
    int* array;
};

void initialize(Stack& stack, int capacity) {
    stack.capacity = capacity;
    stack.top = -1;
    stack.array = new int[capacity];
}

void deinitialize(Stack& stack) {
    delete[] stack.array;
}

void push(Stack& stack, int value) {
    if (stack.top == stack.capacity - 1) {
        std::cerr << "Stack overflow. Cannot push." << std::endl;
        return;
    }
    stack.array[++stack.top] = value;
}

int pop(Stack& stack) {
    if (stack.top == -1) {
        std::cerr << "Stack underflow. Cannot pop." << std::endl;
        return -1;
    }
    return stack.array[stack.top--];
}

int peek(Stack& stack) {
    if (stack.top == -1) {
        std::cerr << "Stack is empty. Cannot peek." << std::endl;
        return -1;
    }
    return stack.array[stack.top];
}

bool isEmpty(Stack& stack) {
    return stack.top == -1;
}

int main() {
    Stack stack;
    initialize(stack, 5);

    push(stack, 10);
    push(stack, 20);
    push(stack, 30);
    push(stack, 40);

    std::cout << "Top element is: " << peek(stack) << std::endl;
    std::cout << "Popped element is: " << pop(stack) << std::endl;
    std::cout << "Top element is now: " << peek(stack) << std::endl;

    deinitialize(stack);
    return 0;
}

Stack (Linked List)

#include <iostream>

class Stack {
public:
    Stack() : head(nullptr) {}

    ~Stack() {
        while (!isEmpty()) {
            pop();
        }
    }

    void push(int value) {
        Node* newNode = new Node(value);
        newNode->next = head;
        head = newNode;
    }

    int pop() {
        if (isEmpty()) {
            std::cerr << "Stack underflow. Cannot pop." << std::endl;
            return -1;
        }

        int poppedValue = head->data;
        Node* temp = head;
        head = head->next;
        delete temp;
        return poppedValue;
    }

    int peek() {
        if (isEmpty()) {
            std::cerr << "Stack is empty. Cannot peek." << std::endl;
            return -1;
        }
        return head->data;
    }

    bool isEmpty() {
        return head == nullptr;
    }

private:
    struct Node {
        int data;
        Node* next;

        Node(int data) : data(data), next(nullptr) {}
    };

    Node* head;
};

int main() {
    Stack stack;

    stack.push(10);
    stack.push(20);
    stack.push(30);
    stack.push(40);

    std::cout << "Top element is: " << stack.peek() << std::endl;
    std::cout << "Popped element is: " << stack.pop() << std::endl;
    std::cout << "Top element is now: " << stack.peek() << std::endl;

    return 0;
}

Deque란?

  • deque("double-ended queue"의 줄임말)는 양쪽 끝, 즉 전면과 후면에서 요소를 추가하거나 제거할 수 있는 데이터 구조
  • 스택과 대기열의 기능을 모두 포함

Stack Overflow와 Stack Underflow

  • 스택 데이터 구조로 작업할 때 발생하는 이슈

Stack OverFlow

  • 스택 오버플로는 이미 가득 찬 스택에 push 할 때 발생
  • 데이터가 의도하지 않은 위치에 기록될 수 있으므로 메모리 손상 또는 예기치 않은 동작이 발생

Stack UnderFlow

  • 스택 언더플로는 빈 스택에서 Pop하려고 할 때 발생합니다.
  • 또한 프로그램이 존재하지 않거나 사용하지 않는 데이터에 액세스하려고 시도할 수 있으므로 메모리 손상이나 예기치 않은 동작이 발생

해결방법

  • 팝 및 푸시 작업 전에 비어 있거나 가득 찬 조건을 확인
  • 동적 데이터 구조 사용
  • 스택 크기 제한 구현

참조

  • 자료구조와 함께 배우는 알고리즘 입문 (이지스 퍼블리싱 2017)
728x90