Data Structure : (6) 큐 (Queue)
Data Structure :: 큐 (Queue)
큐 (Queue) 란?
사진출처:
freepik
"먼저 들어온 데이터가 먼저 나가는 FIFO(First-In First-Out) 특성을 가진 자료구조이다."
- 큐 (Queue)는 새로운 데이터가 뒤에서 추가되고, 앞에서 데이터가 하나씩 삭제되는 구조를 가지고 있다.
- 스택(Stack)과의 구조상으로 다른 점
- 스택: 데이터의 삽입과 삭제가 같은 위치(top)에서 일어난다.
- 큐: 데이터의 삽입은 후단(rear)에서, 삭제는 전단(front) 에서 일어난다.
- 큐도 스택과 마찬가지로 배열과 연결 리스트를 이용하여 구현할 수 있다.
사진출처:
geeksforgeeks
큐의 ADT
큐 ADT
// 큐의 ADT는 스택의 ADT 와 거의 유사하다.
// 객체: 0개 이상의 요소들로 구성된 선형 리스트
∙연산:
▪ create(max_size) ::=
최대 크기가 max_size인 공백큐를 생성한다.
▪ init(q) ::=
큐를 초기화한다.
▪ is_empty(q) ::=
if(size == 0) return TRUE;
else return FALSE;
▪ is_full(q) ::=
if(size == max_size) return TRUE;
else return FALSE;
▪ enqueue(q, e) ::=
if( is_full(q) ) queue_full 오류;
else q의 끝에 e를 추가한다.
▪ dequeue(q) ::=
if( is_empty(q) ) queue_empty 오류;
else q의 맨 앞에 있는 e를 제거하여 반환한다.
▪ peek(q) ::=
if( is_empty(q) ) queue_empty 오류;
else q의 맨 앞에 있는 e를 읽어서 반환한다.
큐의 삽입(enqueue) , 삭제(dequeue) 연산
큐의 삽입(enqueue) , 삭제(dequeue) 연산 과정
- 삽입(enqueue):
- 큐에 요소를 추가하는 연산.
- 큐의 제일 뒤에(rear) 새로운 요소를 추가한다.
- 삭제(dequeue):
- 큐의 요소를 삭제하는 연산.
- 큐의 제일 앞의(front) 요소를 꺼내서 외부로 반환.
선형 큐 (Linear Queue)
선형 큐의 구현
- 정수형 1차원 배열을 정의한다.
- enqueue, dequeue ADT를 위한 변수 front, rear를 선언한다.
front는 큐에 삽입된 첫번째 요소, rear는 큐에 가장 마지막으로 삽입된 요소를 가리킨다.
- front와 rear의 초기값은 -1 이다.
- 큐에 데이터가 enqueue 되면 rear를 하나 증가시킨후 해당 위치에 데이터를 저장한다.
- 큐에 데이터가 dequeue 되면 front를 하나 증가시킨후 front가 가리키는 위치에 있는 데이터를 삭제한다.
선형 큐의 Process 과정
// 간단한 선형 큐의 구현
#include<stdio.h>
#include<stdlib.h>
#define MAX_QUEUE_SIZE 5
typedef int element;
typedef struct { // 큐 타입
int front;
int rear;
element data[MAX_QUEUE_SIZE];
} QueueType;
// 오류함수
void error(char *message) {
fprintf(stderr, "%s\n", message);
exit(1);
}
void init_queue(QueueType *q) {
q->rear = -1;
q->front = -1;
}
void queue_print(QueueType *q) {
for(int i = 0; i < MAX_QUEUE_SIZE; i++) {
if (i <= q->front || i> q->rear)
printf(" | ");
else
printf("%d | ", q->data[i]);
}
printf("\n");
}
int is_full(QueueType *q) {
return q->rear == MAX_QUEUE_SIZE - 1;
}
// queue의 front가 삭제되며 증가하다가 rear와 같아지는 순간 queue는 공백 상태가 된다.
int is_empty(QueueType *q) {
return q->rear == q->front;
}
// 큐 구조체 내부에 배열 타입으로 선언된 data[]의
// 인덱스를 증가시킨후(rear 초기값 = -1) 해당 위치부터 요소 추가
void enqueue(QueueType *q, int item) {
if(is_full(q)) {
error("큐가 포화상태이므로 enqueue 할 수 없습니다.");
}
else {
q->data[++(q->rear)] = item;
}
}
// 큐 구조체 내부에 배열 타입으로 선언된 data[]의
// 인덱스를 증가시킨후(front 초기값 = -1) 해당 위치부터 요소 삭제 후 반환
int dequeue(QueueType *q) {
if(is_empty(q)) {
error("큐가 이미 공백상태이므로 dequeue 할 수 없습니다.");
return 0;
}
else {
int item = q->data[++(q->front)];
return item;
}
}
int main(void) {
int item = 0;
QueueType *q = (QueueType *)malloc(sizeof(QueueType));
init_queue(q);
enqueue(q, 10); queue_print(q);
enqueue(q, 20); queue_print(q);
enqueue(q, 30); queue_print(q);
item = dequeue(q); queue_print(q);
item = dequeue(q); queue_print(q);
item = dequeue(q); queue_print(q);
return 0;
}
선형 큐 실행 결과
원형 큐 (Circular Queue)
원형 큐의 구현
- 선형 큐의 ADT를 보면 찝찝한 부분이 보인다.
- 큐의 삽입과 삭제 연산을 도와주는 front 와 rear 변수의 값이 연산이 이루어질수록 한없이 증가한다는 것이다.
- 이 경우 data[] 배열의 끝에 도달하게 되면 front 변수가 증가하며 지나온 비어있는 이전 배열 공간을 사용하지 못한다.
- 배열내의 요소들을 이동시켜 해결할 수 있지만 비효율적이기에 원형 큐가 개발되었다.
원형 큐 (Circular queue)
☝🏻 위 그림처럼 배열을 선형이 아닌 원형으로 생각해보자. ☝🏻
front와 rear의 값이 배열의 끝인(MAX_QUEUE_SIZE - 1)에 도달하게 되면
다음에 증가되는 값은 data[0] 에 저장되도록 구현하는 것이다.
실제 배열이 원형으로 변화 되는 것은 아니지만 개념상으로 배열 data[]의 인덱스에 변화를 주는 것이다.
- 원형 큐에서는 front와 rear의 개념에 약간의 변화가 생긴다.
- front와 rear의 초기값은 -1 이 아닌 0 으로 설정.
- 따라서 첫 데이터도 data[0] 이 아닌 data[1]부터 저장.
- front는 항상 첫번째 요소의 하나 앞을 가리키며 rear는 마지막 요소를 가리킨다.
- 삽입 시: rear의 값 1증가 후 해당 위치에 요소 저장.
- 삭제 시: front의 값 1증가 후 해당 위치의 요소 삭제 및 반환.
- 원형 큐의 공백 상태: front와 rear의 값이 같을 시.
- 원형 큐의 포화 상태: front가 rear보다 한칸 앞에 있을 시.
원형 큐 (Circular queue)의 동작
원형 큐의 포화 상태 검사
- 원형 큐는 한 칸의 자리를 비워둔다.
- 배열의 모든 칸을 사용하게 되면 포화 및 공백 상태일때 모두 front와 rear의 값이 같아져 구분 할 수 없게 된다.
- 후에 요소들의 개수를 저장하고 있는 count 변수를 사용하게 되면 비워두지 않아도 된다.
- 원형 큐의 공백 상태 : front == rear
- 원형 큐의 포화 상태 : front == (rear + 1) % MAX_QUEUE_SIZE
원형 큐 (Circular queue) 의 상태 검사
// 공백 상태 검출 함수
void is_empty(QueueType *q) {
return (q->front == q->rear;)
}
// 포화 상태 검출 함수
int is_full(QueueType *q) {
// front 가 rear 보다 한 칸 앞에 있으면 포화.
return (q->front == (q->rear + 1) % MAX_QUEUE_SIZE)
}
원형 큐의 삽입, 삭제 알고리즘
원형 큐의 삽입, 삭제 알고리즘
- 원형 큐에서의 삽입, 삭제 알고리즘에서 중요한 점은
삽입이나 삭제를 하기전에 front 와 rear 를 원형으로 회전시켜야 한다는 것이다.
- 원형 회전은 나머지 연산자 %를 이용하여 쉽게 구현 가능하다.
front <- (front + 1) % MAX_QUEUE_SIZE; rear <- (rear + 1) % MAX_QUEUE_SIZE;
- 위의 식에 의하여 front와 rear 값은 (MAX_QUEUE_SIZE - 1)에서 하나가 증가되면 0이 된다.
- 즉, MAX_QUEUE_SIZE 가 5이면 front 와 rear 값은 0,1,2,3,4,0 과 같이 변화한다.
// 원형 큐에서의 삽입, 삭제 알고리즘
enqueue(Q, x):
rear<-(rear + 1) % MAX_QUEUE_SIZE;
Q[rear] <- x;
dequeue(Q):
front<-(front + 1) % MAX_QUEUE_SIZE;
return Q[front];
원형 큐의 구현
원형 큐를 C언어로 구현해보자.
#include<stdio.h>
#include <stdlib.h>
#define MAX_QUEUE_SIZE 5
typedef int element;
typedef struct {
int front;
int rear;
element data[MAX_QUEUE_SIZE];
}QueueType;
// 오류 함수
void error (char *message) {
fprintf(stderr, "%s\n", message);
exit(1);
}
// 큐 초기화
void init_queue(QueueType *q) {
q->front = q->rear = 0;
}
// 공백 검출
int is_empty(QueueType *q) {
return q->front == q->rear;
}
// 포화 검출
int is_full(QueueType *q) {
return q->front == (q->rear + 1) % MAX_QUEUE_SIZE;
}
// 원형큐 출력 함수
void queue_print(QueueType *q)
{
printf("QUEUE(front=%d rear=%d) = ", q->front, q->rear);
if (!is_empty(q)) {
int i = q->front;
do {
i = (i + 1) % (MAX_QUEUE_SIZE);
printf("%d | ", q->data[i]);
if (i == q->rear)
break;
} while (i != q->front);
}
printf("\n");
}
void enqueue(QueueType *q, element item) {
if(is_full(q)){
error("큐가 이미 포화 상태입니다.");
}
else {
q-> rear = (q->rear + 1) % MAX_QUEUE_SIZE;
q->data[q->rear] = item;
}
}
element dequeue(QueueType *q) {
if(is_empty(q)) {
error("큐가 이미 공백 상태입니다.");
return 0;
}
else {
q->front = (q->front + 1) % MAX_QUEUE_SIZE;
return q->data[q->front];
}
}
element peek(QueueType *q) {
if(is_empty(q)) {
error("큐가 이미 공백 상태입니다.");
}
return q->data[(q->front + 1) % MAX_QUEUE_SIZE];
}
int main(void) {
QueueType *q = (QueueType *)malloc(sizeof(QueueType));
int element;
init_queue(q);
printf("----데이터 추가 단계----\n");
while (!is_full(q)) {
printf("정수를 입력하시오: ");
scanf("%d", &element);
enqueue(q, element);
queue_print(q);
}
printf("-----큐는 포화 상태입니다.-----\n\n");
printf("-----데이터 삭제 단계-----\n");
while (!is_empty(q)) {
element = dequeue(q);
printf("꺼내진 정수 : %d \n", element);
queue_print(q);
}
printf("----큐는 공백 상태입니다.----\n");
}
실행 결과
원형 큐 (Circular queue) 의 실행 결과
덱 (deque) 이란?
덱 (deque)
덱(deque)
은 double-ended queue의 줄임말큐의 전단(front)과 후단(rear) 에서 모두 삽입과 삭제가 가능한 큐.
덱의 구조
📣 하지만 여전히 큐의 중간에서 데이터의 삽입 및 삭제 등의 수정은 구현되지 않는다!! 📣
덱의 ADT
덱 ADT
∙객체: n개의 element형으로 구성된 요소들의 순서있는 모임 ∙연산: ▪ create() ::= // 덱을 생성한다. ▪ init(dq) ::= // 덱을 초기화한다. ▪ is_empty(dq) ::= // 덱이 공백상태인지를 검사한다. ▪ is_full(dq) ::= // 덱이 포화상태인지를 검사한다. ▪ add_front(dq, e) ::= // 덱의 앞에 요소를 추가한다. ▪ add_rear(dq, e) ::= // 덱의 뒤에 요소를 추가한다. ▪ delete_front(dq) ::= // 덱의 앞에 있는 요소를 반환한 다음 삭제한다 ▪ delete_rear(dq) ::= // 덱의 뒤에 있는 요소를 반환한 다음 삭제한다. ▪ get_front(q) ::= // 덱의 앞에서 삭제하지 않고 앞에 있는 요소를 반환한다. ▪ get_rear(q) ::= // 덱의 뒤에서 삭제하지 않고 뒤에 있는 요소를 반환한다.
- 덱은 스택과 큐의 연산들을 모두 가지고 있다.
- add_front(D) == push(S)
- delete_front(D) == pop(S) == dequeue(Q)
- add_rear(D) == enqueue(Q)
- 덱이 추가적으로 가지고 있는 연산들
- get_front(D)
- get_rear(D)
- delete_rear(D)
사용 연산에 따른 덱의 구조 변화
덱의 front() 관련 연산만 사용 = Stack 처럼 사용가능
덱의 삽입은 rear()연산, 삭제는 front()연산 만을 사용 = Queue 처럼 사용가능
덱의 연산
덱의 연산
덱의 연산 과정
배열을 이용한 덱의 구현
배열을 이용하여 덱을 구현해보자.
- 원형 큐와 덱은 공통점이 많다.
- 원형 큐에서 그대로 사용할 수 있는 연산들 🔽
- is_empty()
- is_full()
- size()
- init_queue()
- print_dequeue()
- add_rear() //enqueue()
- delete_front()
- get_front()
- 새롭게 추가된 연산들 🔽
- delete_rear() // 원형 큐에서와 다르게 반대 방향으로의 회전이 필요하다.
- add_front() // 원형 큐에서와 다르게 반대 방향으로의 회전이 필요하다.
- get_rear() // 공백 상태가 아닌경우 rear가 가리키는 항목을 반환.
🔥 add_front() & delete_rear(): 🔥
- 원형 큐에서와 다르게 반대 방향으로의 회전이 필요하다.
- front 나 rear 를 감소시켜야 한다.
- 만약 음수가 된다면 MAX_DEQUE_SIZE 를 더해주어야 한다.
- 따라서 다음과 같이 변경된다.
front ◀️ (front - 1 + MAX_DEQUE_SIZE) % MAX_DEQUE_SIZE; rear ◀️ (rear - 1 + MAX_DEQUE_SIZE) % MAX_DEQUE_SIZE;
배열을 이용한 덱의 구현
배열을 이용하여 원형 덱을 구현해보자. - C언어
#include<stdio.h>
#include<stdlib.h>
#define MAX_q_SIZE 5
typedef int element;
typedef struct {
element data[MAX_q_SIZE];
int front, rear;
}DequeType;
//오류 함수
void error(char *message) {
fprintf(stderr,"%s\n", message);
exit(1);
}
//초기화
void init_deque(DequeType *q){
q->front = q->rear = 0;
}
//공백 상태 검출
int is_empty(DequeType *q) {
return q->front == q->rear;
}
//포화 상태 검출
int is_full(DequeType *q) {
return q->front == (q->rear + 1) % MAX_q_SIZE;
}
//원형 큐 출력
// 원형큐 출력 함수
void deque_print(DequeType *q)
{
printf("DEQUE(front=%d rear=%d) = ", q->front, q->rear);
if (!is_empty(q)) {
int i = q->front;
do {
i = (i + 1) % (MAX_q_SIZE);
printf("%d | ", q->data[i]);
if (i == q->rear)
break;
} while (i != q->front);
}
printf("\n");
}
// front 삭제
element delete_front(DequeType *q) {
if(is_empty(q)) {
error("덱이 공백 상태입니다.");
}
// 추가 (front + 1 하여 삭제 후 한바퀴를 돌린다.)
q->front = (q->front + 1) % MAX_q_SIZE;
return q->data[q->front];
}
//get front
element get_front(DequeType *q) {
if(is_empty(q)) {
error("큐가 공백 상태입니다.");
}
return q->data[(q->front + 1) % MAX_q_SIZE];
}
void add_front(DequeType *q, element item) {
if(is_full(q)) {
error("큐가 공백 상태입니다.");
}
q->data[q->front] = item;
q->front = (q->front - 1 + MAX_q_SIZE) % MAX_q_SIZE;
}
// rear 삽입
void add_rear(DequeType *q, element item) {
if(is_full(q)){
error("덱이 이미 포화 상태입니다.");
}
// 추가 (rear + 1하여 공간을 확보 후 원형 덱 한바퀴를 돌린다.)
q->rear = (q->rear + 1) % MAX_q_SIZE;
q->data[q->rear] = item;
}
element delete_rear(DequeType *q) {
int prev = q->rear; // rear에 있던 데이터 삭제 전 반환해야하는 rear 데이터 임시 저장.
if(is_empty(q)) {
error("덱이 공백 상태입니다.");
}
q->rear = (q->rear - 1 + MAX_q_SIZE) % MAX_q_SIZE;
return q->data[prev];
}
element get_rear(DequeType *q) {
if(is_empty(q)) {
error("덱이 공백 상태입니다.");
}
return q->data[q->rear];
}
int main(void)
{
DequeType *q = (DequeType *)malloc(sizeof(DequeType));
init_deque(q);
for (int i = 0; i < 3; i++) {
add_front(q, i);
deque_print(q);
}
for (int i = 0; i < 3; i++) {
delete_rear(q);
deque_print(q);
}
return 0;
}
배열을 이용한 원형 덱 실행 결과
큐의 응용: 시뮬레이션
고객과 서비스를 제공하는 장소의 대기 행렬을 큐를 사용하여 시뮬레이션 해보자.
요구 사항
- 직원: 1명
- 대기행렬: 큐(Queue)
- 고객마다의 입장 간격: random
- 고객마다의 서비스 시간: random
- 고객들은 입장 순서대로 서비스를 받는다.
- 시뮬레이션이 끝나면 고객들의 평균 대기시간을 출력.
알고리즘
- 시뮬레이션은 하나의 반복 루프. - 현재 시각을 나타내는 clock 변수 하나 증가. - is_customer_arrived() 함수 호출 - 랜덤 난수를 생성 후 시뮬레이션 파라미터 변수인 arrival_prov() 와 비교하여 작으면 새로운 고객의 입장으로 판단. - 고객의 ID, 도착시간, 서비스 시간 등의 정보를 만들어 구조체에 복사 - 고객 정보 구조체를 파라미터로 큐의 삽입함수 enqueue() 호출.
원형 큐를 이용한 시뮬레이션 구현
원형 큐를 사용해서 비즈니스 시뮬레이션을 구현해보자. - C언어
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define MAX_QUEUE_SIZE 5
typedef struct {
int id;
int arrival_time;
int service_time;
} element;
typedef struct {
int front;
int rear;
element data[MAX_QUEUE_SIZE];
}QueueType;
// 오류함수
void error(char *message) {
fprintf(stderr, "%s\n", message);
exit(1);
}
// 큐 초기화
void init_queue(QueueType *q) {
q->front = q->rear = 0;
}
int is_empty(QueueType *q) {
return (q->front == q->rear);
}
int is_full(QueueType *q) {
return q->front == (q->rear + 1) % MAX_QUEUE_SIZE;
}
//삽입 함수
void enqueue(QueueType *q, element item) {
if(is_full(q)) {
error("큐가 이미 포화 상태입니다.");
}
q->data[(q->rear + 1) % MAX_QUEUE_SIZE] = item;
}
// 삭제 함수
element dequeue(QueueType *q) {
if(is_empty(q)) {
error("큐가 공백 상태입니다.");
}
q->front = (q->front + 1) % MAX_QUEUE_SIZE;
return q->data[q->front];
}
// 탐색
element peek(QueueType *q) {
if(is_empty(q)) {
error("큐가 공백 상태입니다.");
}
return q->data[(q->front + 1) % MAX_QUEUE_SIZE];
}
//----------------원형 큐 코드-----------------
int main(void) {
int minutes = 60; // 60분이 지나면 시뮬레이션 종료 및 대기시간 반환
int total_wait = 0;
int total_customers = 0;
int service_time = 0;
int service_customer;
QueueType q; // 큐(대기열)은 한개.
init_queue(&q);
srand(time(NULL));
for(int clock = 0; clock < minutes; clock++) {
printf("현재 시각: %d\n", clock);
// 0~9 사이의 난수가 3보다 작으면 새로운 고객의 입장으로 판단.
if((rand()%10) < 3) {
element customer;
customer.id = total_customers++;
customer.arrival_time = clock;
// 고객의 서비스 시간은 1~3 사이의 난수
customer.service_time = rand() % 3 + 1;
enqueue(&q, customer); // 고객 입장
printf("고객 %d 이 %d 분에 들어옵니다. 업무 처리시간: %d분\n", customer.id, customer.arrival_time, customer.service_time);
// service time이 남아있으면 service time 감소
}
if(service_time > 0) {
printf("고객 %d 업무 처리 중입니다. \n", service_customer);
service_time--;
}
else { // service time이 끝나면 dequeue()
// 대기열에 고객이 남아 있으면 실행
if(!is_empty(&q)) {
element customer = dequeue(&q);
service_customer = customer.id;
service_time = customer.service_time;
printf("고객 %d이 %d분에 업무를 시작합니다. 대기 시간은 %d분 이었습니다.\n", customer.id, clock, clock - customer.arrival_time);
total_wait += clock - customer.arrival_time;
}
}
}
printf("전체 대기시간은 %d분 입니다.", total_wait);
return 0;
}
최대한의 설명을 코드 블럭 내의 주석으로 달아 놓았습니다.
혹시 이해가 안가거나 추가적인 설명이 필요한 부분, 오류 등의 피드백은 언제든지 환영합니다!
긴 글 읽어주셔서 감사합니다. 큐 (Queue) 포스팅을 마칩니다.
참고: C언어로 쉽게 풀어쓴 자료구조 <개정 3판> 천인국, 공용해, 하상국 지음
Task Lists
- Data Structure : 스택 (Stack) 이란?
- 스택의 특징, 스택의 구조, 스택의 추상 데이터 타입(ADT), 스택의 연산
- 시스템 스택을 이용한 함수 호출
- 배열을 이용해 스택을 구현
- 동적 배열 스택 프로그램
- 스택의 응용 1 : 괄호 검사
- 스택의 응용 2-1 : 후위 표기 수식의 계산
- 스택의 응용 2-2 : 중위 표기식을 후위 표기식으로 변환
- 스택의 응용 3 : 미로 문제 (Maze Solving Problem)
Comments