👉🏻 큐 (Queue, '대기열') : 양 쪽 끝이 열려 있는 선형 데이터 구조로 먼저 들어간 데이터가 먼저 나오는 FIFO(First In First Out) 혹은 LILO(Last In Last Out)의 특징을 가집니다. 쉽게 말하면 우리가 실생활에서 순서를 위해 줄서는 모든 행동이 이에 해당합니다. 대표적인 예로 수강신청시 대기 순서대로 접속되는 것을 겪어 보셨을 것입니다.
(여기서 새로고침하면 줄의 맨 뒤로 옮겨가서 다시 줄서는 거 아시죠..?😱)입력과 출력의 방향이 각 고정되어 목록의 한 쪽 끝에서 모든 입력이 이루어지고, 다른 쪽 끝에서 모든 출력이 이루어집니다.
java.util.Queue
를, 파이썬은 deque
를 제공하고 있습니다. 그러나 자바스크립트의 경우 내장된 큐가 없어서 직접 자료 구조를 작성해야할 필요가 있습니다.다섯가지 유형의 큐가 있습니다.
한쪽 뒤에서만 입력받을 수 있고, 양쪽에서 요소 삭제가 가능한 큐 유형입니다. 이런 종류의 대기열은 FIFO를 따르지 않습니다. 최근에 삽입된 데이터를 제거해야 하고, 그러한 경우가 관련 없는 데이터, 성능 문제 등 일 수 있는 경우에 사용됩니다.
양쪽에서 입력을 받을 수 있고 요소 삭제는 한쪽에서만 수행할 수 있습니다. 입력이 실행될 우선 순위가 있고, 입력이 가장 먼저 실행되도록 첫 번째 위치에 배치될 수 있는 경우에 사용됩니다.
FIFO 원리에 따르지만, 배열의 맨 마지막 위치를 쓰면 다시 첫 번째 위치로 연결되어 큐를 채웁니다. 마지막 위치가 시작위치와 연결되는 원형구조를 띄어 '링 버퍼' 라고도 부릅니다. 마지막 위치와 시작위치를 연결하는 원형구조에 두개의 포인터가 움직이며 큐 연산을 합니다. 주로 메모리 관리, 교통시스템, CPU 스케줄링에 쓰입니다. 설정된 시간에 따라 신호등을 하나씩 반복적으로 켜거나, 운영체제가 실행 준비가 되었거나 특정 이벤트 발생을 기다리는 프로세스 대기열울 유지하는 경우에 사용되고 있습니다.
양쪽 끝에서 모두 삽입과 삭제 작업이 가능한 큐 데이터 구조입니다. 스택 작업과 큐 작업을 모두 지원하므로 둘 다로 사용할 수 있습니다. 요소를 제거하거나 추가해야 하는 문제를 효율적으로 해결할 수 있습니다. 데이터 처리에 더 많은 유연성이 제공되며 요소를 여러 방향으로 처리해야 하는 응용 프로그램에 사용할 수 있습니다. 구현시 이중 연결 리스트로 구현하는 게 가장 적합합니다.
각 요소에 우선순위 수준이 할당되는 대기열 유형입니다. 우선순위 수준이 높은 요소가 먼저 처리됩니다. 특정 작업이나 데이터 요소를 더 높은 우선 순위로 처리해야 하는 애플리케이션에 사용됩니다. 운영체제작업 예약 및 네트워크 패킷 예약이 있습니다. (자세한 내용은 해당 게시물을 참고하세요.)
Method | 시간 복잡도 | 작 업 |
---|---|---|
Enqueue(x) | O(1) | 큐 끝에 요소를 추가 또는 저장합니다. |
Dequeue() | O(1) | 큐에서 요소를 제거합니다. |
front / peek() | O(1) | 큐의 front 노드의 데이터 요소를 삭제하지 않고 반환합니다. |
rear() | O(1) | 요소를 제거하지 않고 뒤쪽 끝에 있는 요소를 반환합니다. |
isFull() | O(1) | 큐가 가득 찾는 지 확인합니다. |
isEmpty() | O(1) | 큐가 비어 있는 지 확인합니다. |
큐는 바로 처리할 필요 없이 FIFO 순서로 처리해야 하는 경우에 사용됩니다.
push()
와 shift()
메서드로 큐의 기능을 구현할 수 있습니다. FIFO 원칙을 준수해 데이터의 삽입과 삭제가 가능하지만, 상당한 시간이 소요됩니다.🌿
shift()
method의 작동방식Queue를 Array로 구현했을 때, 상당한 시간이 걸리는 이유는
shift()
메소드의 작동방식 때문입니다.
- 배열의 첫번째 원소 (
arr[0]
)에 접근해 원소를 반환하고 배열에서 제거합니다.- arr[0]의 공간이 비어있으므로 나머지 배열 원소들을 앞으로 한 칸씩 당겨서 재 정렬 을 해줍니다.
- 그래야 다음
shift()
로arr[0]
에 접근했을 때 값을 가져올 수 있기 때문입니다.
👉🏻 즉, 두번째 과정으로 매번
shift()
메서드로 값을 가져올 때마다 재정렬을 요하므로 O(n)만큼의 추가적인 시간이 더 소요됩니다.
push()
)하고 앞쪽에서 요소를 제거(shift()
)합니다. class Queue {
constructor() {
this.arr = [];
}
isEmpty() {
return this.arr.length === 0;
}
enqueue(element) {
this.arr.push(element);
}
dequeue() {
if(this.isEmpty()) return "Underflow!😱";
return this.arr.shift();
}
front() {
if(this.isEmpty()) return "큐가 비어있는걸요?🤷🏻♀️";
return this.arr[0];
}
rear() {
if(this.isEmpty()) return "큐가 비어있는걸요?🤷🏻♀️";
return this.arr[this.arr.length-1];
}
}
👉🏻 (구현은 했지만) 아주 간단한 문제가 아니라면 queue를 사용하는 문제에서 배열을 사용하는 것은 시간 초과가 날 확률이 높습니다.
class Node {
constructor(value) {
this.data = value;
this.next = null;
}
}
let front = null, rear = null;
// enqueue() 작업은 뒤쪽에 새 노드를 추가하고 rear를 다음 노드로 이동하면 됩니다.
function enqueue(value) {
let temp - new Node(value);
if(rear === null) {
front = rear = temp;
return;
}
rear.next = temp;
rear = temp;
}
// dequeue()는 앞쪽 노드를 제거하고 pront를 다음 노드로 이동합니다.
function dequeue() {
if(front === null) return;
let temp = front;
front = front.next;
if(front === null) rear = null;
}
front
)과 뒤(rear
)에 두 인덱스를 추적해야 합니다. 이 때 단순히 인덱스를 증가시키며 추적하면 front
와 rear
에 도달해 문제가 생길 수 있으므로 큐의 최대 크기를 먼저 정의해야 합니다. (➡️ 문제에 대한 근본적 해결책은 원형 큐로 해결할 수 있습니다.)