알고리즘 & 자료구조

C언어로 자료구조 만들기 - 큐(Queue), 스택(Stack)

자, 지난 포스트에서 구현한 링크드 리스트를 기반으로 큐, 스택을 만들어보자. 링크드 리스트에서 대부분 구현이 되었기 때문에 설명할 내용은 많이 없다. 큐와 스택 둘 다 순회는 지원하지 않을 것이며 검색도 지원하지 않고 삽입과 삭제에 대한 것만이 존재한다.

 

https://pronist.tistory.com/76

 

C언어로 자료구조 만들기 - 링크드 리스트(Linked List)

이번에는 지난 포스트에 이어서 C언어로 자료구조를 구현해볼 텐데, 이번에는 링크드 리스트다. 양 옆으로 삽입과 삭제가 가능한 양방향 링크드 리스트를 구현하고, 또한 중간에 데이터를 삽입

pronist.tistory.com

https://github.com/pronist/Data-Structure-In-C

 

pronist/Data-Structure-In-C

a Data Structure library in C. Contribute to pronist/Data-Structure-In-C development by creating an account on GitHub.

github.com

큐(Queue)

FIFO(First In First Out) 구조를 가지며 가장 먼저 삽입된 데이터가 나올 때도 가장 먼저 나온다. 이 점이 큐의 가장 핵심이다. 우리가 게임에서 큐를 돌린다고 할 때, 먼저 큐에 진입한 플레이어가 매칭이 먼저 잡히는 것과 마찬가지다.

 

Queue

큐의 구현에서는 링크드 리스트에서 구현한 함수를 그대로 사용한다.

#include "List.h"

typedef struct __Queue__ {
  /**
   * @private
   */
  List _ls
} Queue;

Queue queue(size_t size, ...);

/**
 * Create Queue
 *
 * @public
 */
Queue queue(size_t size, ...) {
  List ls = list(0);
  Queue qu = { ._ls = ls };
  va_list ap;
  va_start(ap, size);
  for (int i = 0; i < size; i++) {
    ls_push_back(&qu._ls, va_arg(ap, void*));
  }
  va_end(ap);
  return qu;
}

qu_*

큐에서 구현할 함수는 매우 적으며, 구현 자체도 한 두줄이면 끝난다. 다만 큐의 특성에 따라 사용하는 함수를 잘 보아야한다. 가장 먼저 삽입된 데이터가 가장 먼저 나온다는 사실을 다시 한 번 기억하자.

void  qu_push(Queue*, void*);
void  qu_pop(Queue*);

void  qu_clear(Queue*);

Node* qu_front(Queue*);
Node* qu_rear(Queue*);

qu_push(Queue*, void*), qu_pop(Queue*)

큐에는 front, rear 포인터가 존재하는데, 구현에서 명시적으로 나타내지는 않았다, 그러나 링크드 리스트를 사용하여 구현 할 때 이것이 어떻게 연결되는 지는 알아야 한다. 큐를 초기화할 때 ls_push_back() 이었으므로 링크드 리스트의 가장 끝에 최신 데이터가 존재한다. 따라서 큐에 새로운 값을 넣을 때도 ls_push_back() 으로 사용한다. 즉, 큐의 rear 포인터는 링크드 리스트의 back 에 대응한다.

 

링크드 리스트를 기반으로 하였기에 일반적인 큐의 개념도와 다를 수 있다.
/**
 * Push new node at Back
 *
 * @public
 */
void qu_push(Queue* queue, void* element) {
  ls_push_back(&queue->_ls, element);
}

/**
 * Remove node at Front
 *
 * @public
 */
void qu_pop(Queue* queue) {
  ls_pop_front(&queue->_ls);
}

값을 삭제할 때 가장 먼저 들어온 값을 제거해야 한다. ls_pop_back() 은 가장 나중에 들어온 값을 의미하므로 ls_pop_front() 를 사용하며 가장 먼저들어온 값을 제거한다.

 

qu_clear(Queue*)

이 함수는 큐 내부에 있는 링크드 리스트를 날린다.

/**
 * Clear Queue
 *
 * @public
 */
void qu_clear(Queue* queue) {
  ls_clear(&queue->_ls);
}

qu_front(Queue*), qu_rear(Queue*)

이 함수는 큐에서 가장 먼저 들어온 포인터인 front, 가장 나중에 들어온 포인터인 rear 값을 반환한다. 각각 링크드 리스트에서 ls_begin(), ls_end() 에 대응한다.

/**
 * Get front element
 *
 * @public
 */
Node* qu_front(Queue* queue) {
  return ls_begin(&queue->_ls);
}

/**
 * Get rear element
 *
 * @public
 */
Node* qu_rear(Queue* queue) {
  return ls_end(&queue->_ls);
}

어떻게 사용해볼까?

큐의 특성을 잘 확인해서 반환 값을 예상해야 한다.

Queue qu = queue(1, "Hello, world!");
qu_push(&qu, "Who are you?");
qu_push(&qu, "Goodbye");

// GoodBye
printf("%s", (const char*)qu_rear(&qu)->element);
// Hello, world!
printf("%s", (const char*)qu_front(&qu)->element);

// Remove "Hello, world" Node
qu_pop(&qu);

// Who are you?
printf("%s", (const char*)qu_front(&qu)->element);

스택(Stack)

스택은 큐와는 다르게 동작한다. FILO(First In Last Out), 가장 먼저 들어온 것이 제일 나중에 나간다. 즉, 스택은 차곡 차곡 쌓이는 구조라고 볼 수 있다.

 

Stack

링크드 리스트를 기반으로 하므로 마찬가지로 ls_push_back() 을 쓰자.

#include "List.h"

typedef struct __Stack__ {
  /**
   * @private
   */
  List _ls
} Stack;

Stack stack(size_t size, ...);

/**
 * Create Stack
 *
 * @public
 */
Stack stack(size_t size, ...) {
  List ls = list(0);
  Stack st = { ._ls = ls };
  va_list ap;
  va_start(ap, size);
  for (int i = 0; i < size; i++) {
    ls_push_back(&st._ls, va_arg(ap, void*));
  }
  va_end(ap);
  return st;
}

st_*

스택은 큐 보다도 제공하는 함수가 적다. 사실 그렇게 많은 것이 필요없기 때문이다.

void  st_push(Stack*, void*);
void  st_pop(Stack*);

void  st_clear(Stack*);

Node* st_top(Stack*);

st_push(Stack*, void*), st_pop(Stack*)

스택에는 top, bottom 포인터가 존재하며, 링크드 리스트에서 top 은 back 에 해당하도록 하였다. top 은 가장 나중에 추가된 데이터가 있는 곳이므로, ls_pop_back() 을 사용하면 스택에서 값을 의도에 맞게 제거할 수 있다.

/**
 * Push new node at Back
 *
 * @public
 */
void st_push(Stack* stack, void* element) {
  ls_push_back(&stack->_ls, element);
}

/**
 * Remove node at Back
 *
 * @public
 */
void st_pop(Stack* stack) {
  ls_pop_back(&stack->_ls);
}

st_clear(Stack*)

큐와 구현이 똑같아서 생략할까 하지만 내용이 짧아서 넣기로 했다.

/**
 * Clear Stack
 *
 * @public
 */
void st_clear(Stack* stack) {
  ls_clear(&stack->_ls);
}

st_top(Stack*)

스택은 top, bottom 포인터 두 개가 존재하지만, 사실상 bottom 포인터에 큰 의미를 둘 필요는 없다. 중요한건 top 이다.

/**
 * Get top element
 *
 * @public
 */
Node* st_top(Stack* stack) {
  return ls_end(&stack->_ls);
}

어떻게 사용해볼까?

큐와의 차이점을 잘 살펴야 한다. 출력결과는 차이가 없을지언정 여러 번의 실험이 필요하다.

Stack st = stack(1, "Hello, world!");
st_push(&st, "Who are you?");
st_push(&st, "Goodbye");

// GoodBye
printf("%s", (const char*)st_top(&st)->element);

// Remove "GoodBye" Node
st_pop(&st);

// Who are you?
printf("%s", (const char*)st_top(&st)->element);