211124, 자료구조 Ch 07-1

Min Hyeok·2021년 11월 24일
0

오늘은 스택만큼 유명한 "Queue (큐)" 를 알아보자.

Ch.07 Queue

07-1

액션~ 큐!!

가 아니고, 스택이랑 거의 비슷하다.

스택은 후입선출 구조였다. 나중에 넣은 애일수록 먼저 나온다는거다.

이거는 그냥 거의 개념은 비슷한데, 다르다. 선입선출(先入先出)이다. 먼저 넣은 애가 먼저 나온다는거다. 그냥 그게 끝이다.

이 큐의 ADT도 그냥 stack이랑 큰 차이가 없다.

Q. 앞서 Stack의 ADT는 뭐였을까? (복습해보자)

A. Push(저장), Pop(데이터 뽑기), Peek(맨 위에 뭐있나 훑어보기), Empty(비었는지 확인), Init(초기화).

놀랍게도, 큐도 똑같다. 저장, 뽑기, 훑기, empty, 초기화.

void QueueInit (Queue *pq); // 초기화
int QIsEmpty (Queue *pq); // 비었냐?

void Enqueue (Queue *pq, Data data); // 넣기
Data Dequeue (Queue *pq); // 뽑기

Data QPeek (Queue *pq); // 훑기

이렇게 다섯개다.

그리고, 이 큐도 배열 기반 / 연결 리스트 기반 각각의 방법으로 구현해낼 수 있다.

스택이랑 뭐 큰 차이 없을거같지? 어림도없다. 한번 보자.

07-2

우선, 큐의 논리적인 구조에 대해 이해해보자.

직접 그린 그림으로 설명해주겠따.

위와 같은 구조로 QueueInit을 해주면 초기상태로 초기화가 된다.

그 다음, 각각 A, B, D를 En (enqueue, 데이터 추가 과정) 해주면 위와같이 데이터가 하나씩 쌓인다. 그리고 F(Front), R(Rear)이 있는데, 이는 연결리스트의 head포인터와 tail포인터를 생각해주면 된다.

마지막으로 Dequeue 과정을 해주면 먼저 들어간 A가 빠져나오게 된다.

근데, 이 과정엔 단점이 하나 있다.

만약 Queue의 마지막 두 박스에 문자 B,D 가 각각 들어가있다고 가정을 하자. 근데 여기서 데이터를 더 추가하려면 어떻게 해야할까.

.. 이 단점을 해소하기 위해 흔히 사용하는 개념이 "원형 큐" 이다.

Circular Queue

아래와 같은 구조다. 현재 F와 R이 가리키고 있는 곳이 0인 지점이고, 이 원형큐는 ARR_LEN이 4인 큐가 되는 것이다.

그리고, 지금 여기서 F와 R이 가리키고 있는 이 지점은 채우지 않을 것이다. 연결리스트의 더미노드같은 느낌이라고 생각하면 된다.

한 줄로 요약하면, 배열의 길이가 N이라면, 데이터가 N-1개가 채워지면 이것이 꽉 찬 상태(Full)이라고 한다.

"엥? 굳이 네개의 공간에서 하나를 왜 버려요?"

왜냐면, 이 네개의 공간을 다 쓰면 F와 R이 꽉 찬 경우와 비어있는 경우를 구분할 수 없기 때문이다.

만약 위의 비어있는 상태에서 enqueue, dequeue를 해주면

이렇게 될 거다.

Enqueue를 해주면 Rear이 한 칸 뒤로 이동한 후 데이터를 삽입.

Dequeue를 해주면 Front가 한 칸 뒤로 이동한 후 데이터를 삭제.

만약 데이터가 꽉 차있으면 R의 앞에 F가 있을거고,

만약 데이터가 텅 비어있다면 F와 R이 같은 곳을 가리킬거다.

이런 것들을 감안해서, 원형큐를 구현해주면 되겠다.

/* CirQ.h */

#ifndef __CIR_Q_H__
#define __CIR_Q_H__

#define TRUE	1
#define FALSE   0

#define QUE_LEN 100
typedef int Data;

typedef struct _que {
    int front;
    int rear;
    Data queArr[QUE_LEN];
} CQueue

typedef CQueue Queue;

void QueueInit (Queue *pq);
int QIsEmpty (Queue *pq);

void Enqueue (Queue *pq, Data data);
Data Dequeue (Queue *pq);

Data QPeek (Queue *pq);

#endif

그 다음은 여기서 정의한 ADT들의 함수를 직접 구현해줘야 하는데, 그 전에 한가지 함수의 구현을 하겠다.

int NextPosIdx(int pos) { // F나 R을 다음 위치로 옮겨주는 함수
    if (pos == QUE_LEN-1) return 0;
    else return pos+1;
}

내가 얘를 보고 이해를 못해서 어제 머리가 조금 아팠었다.

조금 디테일하게 읊어보겠다.

일단. 위에 주석을 달아놓은것 처럼 NextPosIdx 라는 함수는 큐의 다음위치에 해당하는 배열의 인덱스 값을 반환하는 함수이다.

근데 여기서, 만약 전달받은 pos값이 이 Queue의 마지막 인덱스값에 있다면?

그러니까 쉽게 말해서, 위에 내가 그림으로 계속 설명한 Queue를 예로 들면, 이 큐의 QUE_LEN = 4이다. 네칸이니까.

그런데 이 데이터가 차고 차고 하다가 마지막인 네번째칸 (배열을 기준으론 세번째칸. arr[3]을 생각하자.) 에 다다랐을때가 Pos == QUE_LEN-1 이랑 같은 말이란거다.

그러면 0을 반환한다.

이 "0을 반환한다"의 의미는 뭘까?

우리가 지금 위에서 계속 그린 원형큐는 그냥 배열을 원형으로 휘어놓은것에 불과하다. 그래서 내가 지금 arr[3]에 다다르면 원래대로라면 다음 칸은 없어야하는데, 이 큐는 원형큐라서 계속 돌고 돌 수 있다. 그래서 arr[3]의 다음은 arr[0]이 된다. 다시 처음으로 돌아왔다는거지.

그래서 0을 반환한다.

아직 끝까지 안갔을 경우에는 그냥 다음 위치로 간다는 거고.

그렇다는거다.

/* CirQ.c */

#include <stdio.h>
#include <stdlib.h>
#include "CirQ.h"

void QueueInit (Queue *pq) {
    pq->front = 0;
    pq->rear = 0;
}

int QIsEmpty (Queue *pq){
    if (pq->front == pq->rear) {
        return TRUE;
    } else return FALSE;
}

int NextPosIdx(int pos) {
    if (pos == QUE_LEN-1) return 0;
    else return pos+1;
}

void Enqueue (Queue *pq, Data data); {
    if  (NextPosIdx(pq->rear) == pq->front) { // 큐가 꽉 차있으면
        printf("넣을 공간 없어요\n");
        exit(-1);
    }
    
    pq->rear = NextPosIdx(pq->rear); // rear을 한 칸 뒤로 옮기고
    pq->queArr[pq->rear] = data; // 현재 rear 위치에 데이터 넣기
}
Data Dequeue (Queue *pq) {
    if (QIsEmpty(pq)) {
        printf("뺄 게 없어요\n");
        exit(-1);
    }
    
    pq->front = NextPosIdx(pq->front); // front를 한 칸 뒤로 옮기고
    return pq->queArr[pq->front]; // 현재 front 위치에 데이터 넣기
}

Data QPeek (Queue *pq) {
    if (QIsEmpty(pq)) {
    	printf("볼 거 없어\n");
        exit(-1);
    }
    
    return pq->queArr[pq->front];
}

그렇다.

오늘은 일단

여기까지.

0개의 댓글