[C++, 자료구조] 큐(Queue)

2025. 12. 22. 16:44·C++/자료구조
728x90

■ 큐(Queue)

[큐의 예시]

앞서 스택(Stack)은 먼저 들어온 것이 나중에 나가는 “선입후출(FILO)” 구조를 지녔다. 그리고 이걸 “쌓여있는 접시”, “프링글스 통 안의 감자칩”에 비유했었다,

 

반대로 큐(Queue)는 “줄을 서 있는 사람들”처럼 먼저 들어온 것이 먼저 나가는 “선입선출(FIFO)”의 구조를 지닌다.

  • 큐의 개념은 일상생활에서도 쉽게 찾을 수 있다. 고무호스, 터널, 극장표 예매처 등은 모두 큐의 동작 방식과 유사한 구조이다.

▶ 큐의 추상 자료형

스택과 마찬가지로 큐의 추상 자료형도 정형화된 편이다.

  • enqueue : 큐에 데이터를 넣는 연산.
  • dequeue : 큐에서 데이터를 빼는 연산.
정의 의미
void InitQueue(Queue* pq) 큐의 초기화를 진행한다.
bool IsEmpty(Queue* pq) 큐가 빈 경우 true, 비어있지 않은 경우 false를 반환한다.
void Enqueue(Queue* pq, Data data) 큐에 데이터를 저장한다.
Data Dequeue(Queue* pq) 저장순서가 가장 앞선 데이터를 삭제하고 반환한다.
Data Peek(Queue* pq) 저장순서가 가장 앞선 데이터를 반환하되 삭제하지 않는다.

 

스택에서 데이터를 넣고 빼는 연산을 가리켜 각각 push, pop이라 하는 것처럼, 큐에서 데이터를 넣고 빼는 연산을 각각 enqueue, dequeue라고 한다.

 

■ 큐의 설계

1. 배열 기반 구현

스택과 큐의 가장 큰 차이는 데이터를 꺼내는 위치, 즉 앞에서 꺼내느냐 뒤에서 꺼내느냐의 차이이다.

그래서 얼핏 보면, 앞서 만든 스택 자료구조에서 단순히 꺼내는 위치만 바꾸면 큐를 만들 수 있을 것처럼 보일 수 있지만, 실제로는 그렇지 않다.

[Enqueue 과정]

F(Fornt)가 가리키는 것이 큐의 머리이고, R(Rear)이 가리키는 것이 큐의 꼬리이다.

  • Enqueue를 하게 되면 R이 다음 칸을 가리키고 그 칸에 데이터가 저장된다.

Dequeue : f가 가리키는 데이터 반환 후, 데이터를 빈자리로 이동

[잘못된 Dequeue 방식]

Dequeue 연산을 할 때, 반환할 데이터를 항상 배열의 맨 앞(Index : 0)에 위치시켜 보자. 이 방식의 경우 아래와 같은 문제점이 생긴다.

  • 항상 배열의 첫 번째 데이터만 Dequeue되므로, 굳이 F(fornt 인덱스)가 필요 없다.
  • Dequeue가 일어날 때마다, 남아 있는 모든 데이터를 한 칸씩 앞으로 옮겨야 한다.

Dequeue : f가 가리키는 데이터 반환 후, f를 이동

[일반적인 Dequeue 방식]

이번엔 Dequeue 연산 시 F를 이동시키고 있다. 이 방식을 취하게 되면 Dequeue 연산에서 데이터의 이동이 필요하지 않다.

[R의 위치가 배열의 끝인 경우]

문자 E가 배열의 끝에 저장되어 더 이상 R을 옮길 수 없는 상황이다. 이 상황에서 어떻게 enqueue 연산을 진행할 수 있을까?

바로 R을 다시 배열의 맨 앞으로 이동시키면 된다!! (F 역시 마찬가지.)

  • 이렇게 배열의 끝에 도달하면 다시 처음으로 돌아가 회전하는 방식의 구조를 “원형 큐(circular queue)라 한다.”

 

2. 원형 기반 큐

[3번의 Enqueue 연산]

원형 큐도 앞서 보인 큐와 같이 첫 번째 데이터가 추가되는 순간 F, R이 동시에 그 데이터를 가리킨다. 이후 Dequeue연산을 두 번 진행하면 아래와 같다.

[2번의 Dequeue 연산]

이제 구현만 하면 될 것 같지만, 현재 원형 큐에서 생각해 볼 문제가 아직 남아있다.

[큐의 상태]

원형 큐에서 데이터가 모두 차 있는 경우와 비어 있는 경우는 모두 F가 R보다 한 칸 앞선 위치를 가리키는 형태를 갖는다.

  • 따라서 F와 R의 위치 정보만으로 큐가 비어 있는 상태인지, 가득 찬 상태인지 구분할 수 없다.

이러한 문제를 해결하기 위해 배열의 크기가 N일 때, 최대 N - 1개의 데이터만 저장하도록 제한한다.

즉, 배열에 N-1개의 데이터가 채워졌을 경우를 “꽉 찬 상태”로 간주하고, 항상 하나의 빈 공간을 유지한다.

[개선된 원형 큐의 꽉 찬 상황]

  1. enqueue 연산 시 R이 가리키는 위치를 한 칸 이동시킨 다음에 데이터를 저장한다.
  2. Dequeue 연산 시 F가 가리키는 위치를 한 칸 이동시킨 다음에 데이터를 반환한다.

따라서 R과 F가 같은 위치를 가리킨다면 큐가 “비어있는 상태” R이 가리키는 앞 위치가 F라면 큐가 “꽉 찬 상태”로 해석할 수 있다.

 

■ 원형 큐의 구현(배열 기반)

1. CircularQueue.h

#pragma once
constexpr int QUE_LENGTH = 100;
typedef int Data;

struct CircularQueue
{
	Data data[QUE_LENGTH];
	int front;
	int rear;
}; typedef CircularQueue CQueue;

void InitQueue(CQueue* pq);

void Enqueue(CQueue* pq, Data item);
Data Dequeue(CQueue* pq);
Data Peek(CQueue* pq);

bool IsEmpty(const CQueue* pq);

[CircularQueue.h]

앞서 정의한 큐의 추상 자료형을 기반으로 위의 헤더파일을 정의하였다.

 

2. CircularQueue.cpp

헤더파일에 선언된 함수의 구현을 보일 차례인데, 그에 앞서 아래의 함수를 살펴보자. 함수는 간단하지만 원형 큐의 핵심이라 할 수 있다.

int NextIndex(int index)
{
	return (index + 1 >= QUE_LENGTH - 1)
		? 0 : index + 1;
}

이는 큐의 다음 위치에 해당하는 배열의 인덱스 값을 반환하는 함수이다.

pos 를 전달하면 pos + 1의 값을 반환하지만, 큐의 길이보다 하나 작은 값이 인자로 전달된다면 0을 반환한다.

  • 이는 F와 R이 배열의 끝에 도달했으므로 앞으로 이동해야 함을 의미한다.

 

#include "CircularQueue.h"
#include <iostream>
using namespace std;

int NextIndex(int index);

void InitQueue(CQueue* pq)
{
	pq->front = pq->rear = 0;
}

void Enqueue(CQueue* pq, Data item)
{
	auto nextIndex = NextIndex(pq->rear);

	// Queue가 가득 찼는지 확인
	if(nextIndex == pq->front)
	{
		cout << "Queue is Full!" << endl;
		return;
	}

	pq->rear = nextIndex;
	pq->data[pq->rear] = item;
}

Data Dequeue(CQueue* pq)
{
	if(IsEmpty(pq))
	{
		cout << "Queue is Empty!" << endl;
		return -1;
	}

	pq->front = NextIndex(pq->front);
	return pq->data[pq->front];
}

Data Peek(CQueue* pq)
{
	if(IsEmpty(pq))
	{
		cout << "Queue is Empty!" << endl;
		return -1;
	}

	return pq->data[NextIndex(pq->front)];
}

int NextIndex(int index)
{
	return (index + 1 >= QUE_LENGTH - 1)
		? 0 : index + 1;
}

bool IsEmpty(const CQueue* pq)
{
	return pq->front == pq->rear;
}

[CircularQueue.cpp]

Queue에 2~100의 숫자 중 짝수만 Enqueue() 하고, 그 뒤에 Queue가 빌 때까지 Dequeue()하였다. 스택과 다르게 먼저 들어간 숫자가 먼저 나오는 “선입선출(FIFO)”구조로 만들어진 것을 확인할 수 있다.

 

■ 연결 리스트 기반 큐의 구현

배열을 기반으로 구현된 Stack을 Queue로 변경하려고 하면, 앞에서 살펴본 것처럼 고려해야 할 문제가 한두가지가 아니다.

 

하지만 ”연결 리스트 기반의 큐”는 배열 기반 구조에 비해 이러한 제약이 적기 때문에, 구조적으로는 스택 구현 방식을 어느 정도 재활용할 수 있지만, 아래와 같은 본질적인 동작 방식의 차이는 존재한다.

  • 스택은 Push와 Pop이 같은 위치에서 이루어지지만, 큐는 Queue와 Dequeue가 다른 위치에서 발생한다.

1. ListQueue.h

#pragma once
typedef int Data;

struct ListQueueNode
{
	Data data;
	ListQueueNode* next = nullptr;
}; typedef ListQueueNode Node;

struct ListQueue
{
	Node* front = nullptr;
	Node* rear = nullptr;
}; typedef ListQueue LQueue;

void InitQueue(LQueue* pq);

void Enqueue(LQueue* pq, Data item);
Data Dequeue(LQueue* pq);
Data Peek(LQueue* pq);

bool IsEmpty(const LQueue* pq);

 

위의 헤더파일에서 보이듯이 연결 리스트 기반 큐에서도 원형 큐와 마찬가지로 front와 rear을 유지해야 한다.

  • enqueue연산과 dequeue 연산이 이뤄지는 위치가 다르기 때문.

 

2. ListQueue.cpp

void InitQueue(LQueue* pq)
{
	if(pq->front != nullptr && pq->rear != nullptr)
	{
		// 기존 노드가 존재한다면 해제
		while(IsEmpty(pq) == false)
		{
			Dequeue(pq);
		}
	}

	pq->front = nullptr;
	pq->rear = nullptr;
}

처음 큐를 생성하면 front와 rear이 가리킬 대상이 없으니, 초기에는 nullptr로 설정한다.

  • 단, 데이터가 남아있는 상태에서 nullptr로 초기화 하면 기존 생성된 데이터에 접근할 수 없으니, Dequeue()를 호출하여 메모리 공간을 해제한다.
void Enqueue(LQueue* pq, Data item)
{
	auto node = new Node();
	node->data = item;
	node->next = nullptr;

	if(IsEmpty(pq) == true)
	{
		pq->front = node;
		pq->rear = node;
	}
	else
	{
		pq->rear->next = node;
		pq->rear = node;
	}
}

[Enqueue()]

큐에 아무런 데이터가 저장되지 않은 상태에서 데이터를 추가하면, F와 R모두 첫 번째 데이터를 가리켜야 한다.

  • 이후에 추가되는 데이터는 R만 움직이면 되므로 가장 끝에 있는 노드(R이 가리키는 노드)가 새 노드를 가리키게 한 다음, R이 새 노드를 가리키게 만든다.
Data Dequeue(LQueue* pq)
{
	if(IsEmpty(pq))
	{
		cout << "Queue is Empty!" << endl;
		return -1;
	}

	auto delNode = pq->front;
	auto retData = delNode->data;
	pq->front = delNode->next;

	delete delNode;
	return retData;
}

[Dequeue()]

Dequene 과정에서는 신경 써야 할 부분이 F 하나이므로, Enqueue처럼 다른 포인터(R)를 건드릴 필요는 없다.

  • F가 다음 노드를 가리키게 한다.
  • F가 이전에 가리키던 노드를 소멸시킨다.
#include "ListQueue.h"
#include <iostream>
using namespace std;

void InitQueue(LQueue* pq)
{
	if(pq->front != nullptr && pq->rear != nullptr)
	{
		// 기존 노드가 존재한다면 해제
		while(IsEmpty(pq) == false)
		{
			Dequeue(pq);
		}
	}

	pq->front = nullptr;
	pq->rear = nullptr;
}

void Enqueue(LQueue* pq, Data item)
{
	auto node = new Node();
	node->data = item;
	node->next = nullptr;

	if(IsEmpty(pq) == true)
	{
		pq->front = node;
		pq->rear = node;
	}
	else
	{
		pq->rear->next = node;
		pq->rear = node;
	}
}

Data Dequeue(LQueue* pq)
{
	if(IsEmpty(pq))
	{
		cout << "Queue is Empty!" << endl;
		return -1;
	}

	auto delNode = pq->front;
	auto retData = delNode->data;
	pq->front = delNode->next;

	delete delNode;
	return retData;
}

Data Peek(LQueue* pq)
{
	if(IsEmpty(pq))
	{
		cout << "Queue is Empty!" << endl;
		return -1;
	}

	return pq->front->data;
}

bool IsEmpty(const LQueue* pq)
{
	return pq->front == nullptr;
}

[ListQueue.cpp 전체 코드]

728x90

'C++ > 자료구조' 카테고리의 다른 글

[C++, 자료구조] 덱(Deque)  (1) 2025.12.22
[C++, 자료구조] stack의 응용 : 계산기 프로그램  (0) 2025.11.11
[C++, 자료구조] 스택(Stack)  (1) 2025.11.11
[C++, 자료구조] 양방향 연결 리스트(Doubly Linked List)  (0) 2025.10.23
[C++, 자료구조] 단일 연결 리스트(Singly Linked List)  (0) 2025.10.23
'C++/자료구조' 카테고리의 다른 글
  • [C++, 자료구조] 덱(Deque)
  • [C++, 자료구조] stack의 응용 : 계산기 프로그램
  • [C++, 자료구조] 스택(Stack)
  • [C++, 자료구조] 양방향 연결 리스트(Doubly Linked List)
브라더스톤
브라더스톤
유티니, C#과 관련한 여러 정보를 끄적여둔 블로그입니다. Email : dkavmdk98@gmail.com
  • 브라더스톤
    젊은 프로그래머의 슬픔
    브라더스톤
  • 전체
    오늘
    어제
    • 개발 노트 (51) N
      • Unity,C# (30)
        • Unity 정보 (7)
        • 알고리즘 (11)
        • 자료구조 (3)
        • 절차적생성(PCG) (9)
      • 게임수학 (14)
      • C++ (7) N
        • 자료구조 (7) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    자료구조
    transform.RotateAround
    최단경로찾기
    unity
    절차적던전생성
    알고리즘
    CustomWindow
    pcg
    C#
    스택
    PerlinNoise
    c++
    외적
    BSP
    이진공간분할
    정렬알고리즘
    절차적지형생성
    UI Toolkit
    게임수학
    커스텀 윈도우
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.3
브라더스톤
[C++, 자료구조] 큐(Queue)
상단으로

티스토리툴바