-
우선순위 큐(Priority Queue)란?Data Structure 2024. 6. 26. 06:28728x90
우선순위 큐(priority queue)
▶ 우선순위를 가진 항목들을 저장하는 큐(queue)
▶ FIFO 순서가 아니라 우선 순위가 높은 데이터가 먼저 나가게 된다.
자료구조 삭제되는 요소 스택 가장 최근에 들어온 데이터 (LIFO) 큐 가장 먼저 들어온 데이터 (FIFO) 우선순위 큐 가장 우선순위가 높은 데이터 환자 치료의 예시를 들어보자.
▶ 큐: 먼저 온 사람을 먼저 치료
▶ 스택: 나중에 온 사람을 먼저 치료
▶ 우선순위 큐: 위급한 사람을 먼저 치료
우선순위 큐(priority queue)는 가장 일반적인 큐임.
스택(stack)이나 FIFO 큐를 우선순위 큐로 구현할 수 있다.
우선순위
▶ 시간: 스택, 큐
▶ 스택, 큐에서는 시간에 따라 자료구조를 조직화
▶ 다른 가치: 우선순위 큐
▶ 따라서 우선순위 큐는 스택이나 큐보다 일반적인 구조
▶ 키 (= 우선순위 값) 필드가 필요
우선순위 큐의 추상자료형 (Abstarct Data Type)
▶ 객체: n개의 element형의 우선 순위를 가진 요소들의 모임
▶ 연산
1) create(): 우선순위 큐를 생성한다.
2) init(q): 우선순위 큐 q를 초기화한다.
3) is_empty(q): 우선순위 큐 q가 비어있는지를 검사한다.
4) is_full(q): 우선순위 큐 q가 가득 찼는가를 검사한다.
5) insert(q, x): 우선순위 큐 q에 요소 x를 추가한다.
6) delete(q): 우선순위 큐로부터 가장 우선순위가 높은 요소를 삭제하고 이 요소를 반환한다.
▶ 가장 중요한 연산은 insert 연산(요소 삽입), delete 연산(요소 삭제)이다.
▶ 우선순위 큐는 2가지로 구분
1) 최소 우선순위 큐
2) 최대 우선순위 큐
우선순위 큐 구현 방법
▶ 배열(array)을 이용한 우선순위 큐
▶ 연결리스트(linked list)를 이용한 우선순위 큐
▶ 히프(heap)를 이용한 우선순위 큐
먼저, 배열을 이용한 방법을 알아보자.
정렬된 배열(array)
▶ 우선순위를 기준으로 오름차순으로 정렬
▶ 가장 우선순위가 큰 데이터는 항상 배열의 마지막에 위치삽입 함수
삽입 함수는 삽입 위치를 찾아야 함
▶ 일단 키 값을 기준으로 처음부터 삽입 위치까지 탐색. 시간 복잡도는 O(n)
▶ 해당 위치에 키 입력을 위한 데이터 이동 필요, O(n)
▶ 따라서 삽입 시, 시간 복잡도는 O(n)
삭제 함수
삭제 함수는 마지막 데이터를 리턴, 배열 데이터의 개수를 하나 줄임
▶ 삭제 시, 시간 복잡도는 O(1)
C언어로 구현해보자.
#include <stdio.h> #include <stdlib.h> #define MAX_SIZE 100 typedef int element; typedef struct { element data[MAX_SIZE]; int size; } PriorityQueue; // 우선순위 큐 초기화 void init_queue(PriorityQueue *pq) { pq->size = 0; } // 우선순위 큐가 비었는지 확인 int is_empty(PriorityQueue *pq) { return pq->size == 0; } // 우선순위 큐가 가득 찼는지 확인 int is_full(PriorityQueue *pq) { return pq->size == MAX_SIZE; } // 우선순위 큐에 요소 삽입 void enqueue(PriorityQueue *pq, element item) { if (is_full(pq)) { fprintf(stderr, "큐가 포화상태입니다.\n"); return; } int i = pq->size - 1; while (i >= 0 && pq->data[i] > item) { pq->data[i + 1] = pq->data[i]; i--; } pq->data[i + 1] = item; pq->size++; } // 우선순위 큐에서 요소 삭제 element dequeue(PriorityQueue *pq) { if (is_empty(pq)) { fprintf(stderr, "큐가 공백상태입니다.\n"); return -1; // 오류 값 반환 } return pq->data[--pq->size]; } // 우선순위 큐 출력 void print_queue(PriorityQueue *pq) { for (int i = 0; i < pq->size; i++) { printf("%d ", pq->data[i]); } printf("\n"); } // 우선순위 큐 테스트 int main() { PriorityQueue pq; init_queue(&pq); enqueue(&pq, 3); enqueue(&pq, 1); enqueue(&pq, 4); enqueue(&pq, 1); enqueue(&pq, 5); enqueue(&pq, 9); printf("Priority Queue: "); print_queue(&pq); printf("Dequeue: %d\n", dequeue(&pq)); printf("Dequeue: %d\n", dequeue(&pq)); printf("Priority Queue after dequeue: "); print_queue(&pq); return 0; }
정렬 안된 배열(array)
정렬되지 않은 배열은 정렬된 배열과 비교하여 삽입과 삭제 함수에서 차이를 보인다.
삽입 함수
삽입 함수는 새로운 데이터를 무조건 배열 끝에 붙임
▶ 삽입의 시간 복잡도는 O(1)
삭제 함수
삭제 함수는 삭제할 데이터를 찾아야 함
▶ 삭제 시, 가장 높은 우선 순위를 처음부터 끝까지 뒤져야 함. 시간 복잡도는 O(n)
▶ 삭제 후, 데이터 이동 필요. 시간 복잡도는 O(n)
▶ 따라서, 삭제의 시간 복잡도는 O(n)
C언어로 구현해보자.
#include <stdio.h> #include <stdlib.h> #define MAX_SIZE 100 typedef int element; typedef struct { element data[MAX_SIZE]; int size; } PriorityQueue; // 우선순위 큐 초기화 void init_queue(PriorityQueue *pq) { pq->size = 0; } // 우선순위 큐가 비었는지 확인 int is_empty(PriorityQueue *pq) { return pq->size == 0; } // 우선순위 큐가 가득 찼는지 확인 int is_full(PriorityQueue *pq) { return pq->size == MAX_SIZE; } // 우선순위 큐에 요소 삽입 void enqueue(PriorityQueue *pq, element item) { if (is_full(pq)) { fprintf(stderr, "큐가 포화상태입니다.\n"); return; } pq->data[pq->size++] = item; } // 우선순위 큐에서 최솟값 요소 찾기 int find_min_index(PriorityQueue *pq) { if (is_empty(pq)) { fprintf(stderr, "큐가 공백상태입니다.\n"); return -1; // 오류 값 반환 } int min_index = 0; for (int i = 1; i < pq->size; i++) { if (pq->data[i] < pq->data[min_index]) { min_index = i; } } return min_index; } // 우선순위 큐에서 요소 삭제 element dequeue(PriorityQueue *pq) { if (is_empty(pq)) { fprintf(stderr, "큐가 공백상태입니다.\n"); return -1; // 오류 값 반환 } int min_index = find_min_index(pq); element min_value = pq->data[min_index]; // 삭제한 요소 뒤의 모든 요소를 한 칸 앞으로 이동 for (int i = min_index; i < pq->size - 1; i++) { pq->data[i] = pq->data[i + 1]; } pq->size--; return min_value; } // 우선순위 큐 출력 void print_queue(PriorityQueue *pq) { for (int i = 0; i < pq->size; i++) { printf("%d ", pq->data[i]); } printf("\n"); } // 우선순위 큐 테스트 int main() { PriorityQueue pq; init_queue(&pq); enqueue(&pq, 3); enqueue(&pq, 1); enqueue(&pq, 4); enqueue(&pq, 1); enqueue(&pq, 5); enqueue(&pq, 9); printf("Priority Queue: "); print_queue(&pq); printf("Dequeue: %d\n", dequeue(&pq)); printf("Dequeue: %d\n", dequeue(&pq)); printf("Priority Queue after dequeue: "); print_queue(&pq); return 0; }
정렬 안된 배열 VS 정렬된 배열
둘의 차이를 비교해보자.
▶ 정렬된 배열과 정렬 안된 배열을 사용할 때의 삽입, 삭제 효율이 서로 뒤바뀐 모습
▶ 삭제가 빨라야 한다면 정렬된 배열을 사용
▶ 삽입이 빨라야 한다면 정렬 안된 배열을 사용
▶ 삽입 삭제 전체를 감안하면 두 방법 모두 O(n)의 시간 복잡도이다.
그 다음, 연결 리스트를 이용한 우선순위 큐 구현 방법을 알아보자.정렬된 연결 리스트(linked list)에 의한 구현
▶ 우선순위를 기준으로 내림차순으로 정렬
▶ 우선순위가 가장 높은 데이터를 헤드 포인터가 직접 가리킴
▶ 삭제의 시간 복잡도는 O(1)
▶ 삽입 함수는 키 값을 기준으로 삽입 위치를 찾아야 함.
▶ 삽입 위치가 마지막일 경우가 최악의 경우임. 시간 복잡도는 O(n)
▶ 배열처럼 이동은 불필요. 포인터만 변경하면 됨
▶ 삭제와 삽입 모두를 감안한 시간 복잡도는 O(1) + O(n) = O(n)이다.
C언어로 구현해보자.
#include <stdio.h> #include <stdlib.h> typedef int element; typedef struct Node { element data; struct Node* next; } Node; typedef struct { Node* head; } PriorityQueue; // 우선순위 큐 초기화 void init_queue(PriorityQueue* pq) { pq->head = NULL; } // 우선순위 큐가 비었는지 확인 int is_empty(PriorityQueue* pq) { return pq->head == NULL; } // 우선순위 큐에 요소 삽입 void enqueue(PriorityQueue* pq, element item) { Node* new_node = (Node*)malloc(sizeof(Node)); new_node->data = item; new_node->next = NULL; // 정렬된 위치에 삽입 if (is_empty(pq) || pq->head->data > item) { new_node->next = pq->head; pq->head = new_node; } else { Node* current = pq->head; while (current->next != NULL && current->next->data <= item) { current = current->next; } new_node->next = current->next; current->next = new_node; } } // 우선순위 큐에서 요소 삭제 element dequeue(PriorityQueue* pq) { if (is_empty(pq)) { fprintf(stderr, "큐가 공백상태입니다.\n"); return -1; // 오류 값 반환 } Node* temp = pq->head; element min_value = temp->data; pq->head = pq->head->next; free(temp); return min_value; } // 우선순위 큐 출력 void print_queue(PriorityQueue* pq) { Node* current = pq->head; while (current != NULL) { printf("%d ", current->data); current = current->next; } printf("\n"); } // 우선순위 큐 테스트 int main() { PriorityQueue pq; init_queue(&pq); enqueue(&pq, 3); enqueue(&pq, 1); enqueue(&pq, 4); enqueue(&pq, 1); enqueue(&pq, 5); enqueue(&pq, 9); printf("Priority Queue: "); print_queue(&pq); printf("Dequeue: %d\n", dequeue(&pq)); printf("Dequeue: %d\n", dequeue(&pq)); printf("Priority Queue after dequeue: "); print_queue(&pq); return 0; }
정렬 안된 연결 리스트(linked list)에 의한 구현
▶ 삽입될 데이터를 가장 첫 노드에 삽입. 시간 복잡도는 O(1)
▶ 배열처럼 이동은 불필요. 포인터만 변경하면 됨.▶ 삭제를 위해 가장 큰 데이터를 처음부터 마지막까지 탐색해야 함. 탐색의 시간 복잡도는 O(n)
▶ 삭제와 삽입 모두를 감안한 시간 복잡도는 O(n) + O(1) = O(n)
C언어로 구현해보자.
#include <stdio.h> #include <stdlib.h> typedef int element; typedef struct Node { element data; struct Node* next; } Node; typedef struct { Node* head; } PriorityQueue; // 우선순위 큐 초기화 void init_queue(PriorityQueue* pq) { pq->head = NULL; } // 우선순위 큐가 비었는지 확인 int is_empty(PriorityQueue* pq) { return pq->head == NULL; } // 우선순위 큐에 요소 삽입 void enqueue(PriorityQueue* pq, element item) { Node* new_node = (Node*)malloc(sizeof(Node)); new_node->data = item; new_node->next = pq->head; pq->head = new_node; } // 우선순위 큐에서 최솟값 요소 찾기 Node* find_min_node(PriorityQueue* pq) { if (is_empty(pq)) { return NULL; } Node* min_node = pq->head; Node* current = pq->head; while (current != NULL) { if (current->data < min_node->data) { min_node = current; } current = current->next; } return min_node; } // 우선순위 큐에서 요소 삭제 element dequeue(PriorityQueue* pq) { if (is_empty(pq)) { fprintf(stderr, "큐가 공백상태입니다.\n"); return -1; // 오류 값 반환 } Node* min_node = find_min_node(pq); element min_value = min_node->data; // 최솟값 노드를 삭제 if (min_node == pq->head) { pq->head = pq->head->next; } else { Node* current = pq->head; while (current->next != min_node) { current = current->next; } current->next = min_node->next; } free(min_node); return min_value; } // 우선순위 큐 출력 void print_queue(PriorityQueue* pq) { Node* current = pq->head; while (current != NULL) { printf("%d ", current->data); current = current->next; } printf("\n"); } // 우선순위 큐 테스트 int main() { PriorityQueue pq; init_queue(&pq); enqueue(&pq, 3); enqueue(&pq, 1); enqueue(&pq, 4); enqueue(&pq, 1); enqueue(&pq, 5); enqueue(&pq, 9); printf("Priority Queue: "); print_queue(&pq); printf("Dequeue: %d\n", dequeue(&pq)); printf("Dequeue: %d\n", dequeue(&pq)); printf("Priority Queue after dequeue: "); print_queue(&pq); return 0; }
각 구현 방법에 따른 성능을 비교해보자. (시간 복잡도)
표현 방법 삽입 삭제 정렬 안된 배열 O(1) O(n) 정렬 안된 연결 리스트 O(1) O(n) 정렬된 배열 O(n) O(1) 정렬된 연결 리스트 O(n) O(1) 히프 (heap) O(logn) O(logn) ▶ n이 1000일 경우, O(n) 알고리즘은 1000초, O(logn) 알고리즘은 10초가 소요된다.
히프(heap)에 대해서는 다음 게시글에서 다뤄보도록 하겠다.
다음에 계속...
728x90'Data Structure' 카테고리의 다른 글
우선순위 큐 구현 방법 - 히프(Heap) (0) 2024.06.26 해싱(Hashing)이란? (0) 2024.06.26 이진 트리, 이진 탐색 트리의 연산 등... (0) 2024.06.26 트리(Tree)란? (0) 2024.05.26 CircularLinkedList, DoublyLinkedList, etc... (0) 2024.05.14