독립 창작 문제: 재생 대기 목록
곡을 뒤에 추가하고 앞에서 재생하며 현재 목록을 출력합니다. 비었다가 다시 추가하는 상황에서도 head와 tail을 일관되게 관리해야 합니다.

이 문제는 BlogFlow 학습용으로 독립 창작했습니다. 외부 문제의 지문·예시·테스트를 복제하지 않았습니다.
개념 연결: 단방향 연결 리스트 가이드
문제
ADD 제목은 재생 목록 끝에 곡을 추가합니다. PLAY는 첫 곡 제목을 출력한 뒤 제거하고, 목록이 비어 있으면 EMPTY를 출력합니다. LIST는 현재 곡 제목을 앞에서부터 쉼표로 이어 출력하고 비어 있으면 EMPTY를 출력합니다. 같은 제목의 곡은 별도 노드로 저장합니다.
입력·출력·제약
첫 줄은 정수 Q(0~50,000), 이후 Q줄은 정의된 명령입니다. 제목은 공백·쉼표 없는 1~40 코드포인트 문자열입니다. 명령과 제목 사이에는 공백 하나를 사용합니다. PLAY와 LIST 결과를 한 줄씩 출력하고 결과가 없으면 아무것도 출력하지 않습니다. 출력이 지나치게 커지지 않도록 전체 출력 길이는 줄바꿈 포함 1,000,000 UTF-16 코드 유닛 이하로 주어집니다.
예시 1
입력:
8
ADD 봄
ADD 여름
LIST
PLAY
ADD 여름
PLAY
PLAY
PLAY
출력:
봄,여름
봄
여름
여름
EMPTY
예시 2: 빈 상태
입력:
1
PLAY
출력:
EMPTY
힌트
처음 곡을 추가하면 head와 tail은 같은 노드입니다. 마지막 곡을 재생한 뒤에는 둘 다 null이어야 다음 추가가 정상적으로 연결됩니다.
정답 코드와 해설 펼치기
Node.js 24 CommonJS용 코드입니다. 전체를 linked-list-playlist-problem.cjs로 저장하고 예시 입력을 input.txt에 UTF-8로 저장합니다. 아래 명령은 셸에 맞게 하나를 골라 사용하세요.
PowerShell 7 (UTF-8 파이프):
Get-Content -Raw -Encoding UTF8 .\input.txt | node .\linked-list-playlist-problem.cjs
macOS / Linux Bash:
node linked-list-playlist-problem.cjs < input.txt
class SinglyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
append(value) {
const node = { value, next: null };
if (this.tail) {
this.tail.next = node;
} else {
this.head = node;
}
this.tail = node;
this.length += 1;
}
removeFirst() {
if (!this.head) {
return { done: true };
}
const node = this.head;
this.head = node.next;
node.next = null;
this.length -= 1;
if (this.length === 0) {
this.tail = null;
}
return { done: false, value: node.value };
}
find(value) {
for (let node = this.head; node; node = node.next) {
if (node.value === value) {
return node;
}
}
return null;
}
remove(value) {
let previous = null;
let current = this.head;
while (current && current.value !== value) {
previous = current;
current = current.next;
}
if (!current) {
return false;
}
if (previous) {
previous.next = current.next;
} else {
this.head = current.next;
}
if (current === this.tail) {
this.tail = previous;
}
current.next = null;
this.length -= 1;
return true;
}
toArray() {
const values = [];
for (let node = this.head; node; node = node.next) {
values.push(node.value);
}
return values;
}
}
function solve(input) {
const text = input.trim();
if (!text) {
return '';
}
const lines = text.split(/\r?\n/);
const count = Number(lines[0]);
const list = new SinglyLinkedList();
const output = [];
for (let i = 1; i <= count; i++) {
const [command, title] = lines[i].split(' ');
if (command === 'ADD') {
list.append(title);
} else if (command === 'PLAY') {
const result = list.removeFirst();
output.push(result.done ? 'EMPTY' : result.value);
} else if (command === 'LIST') {
output.push(list.toArray().join(',') || 'EMPTY');
}
}
return output.join('\n');
}
module.exports = { solve };
if (require.main === module) {
const input = require('node:fs').readFileSync(0, 'utf8');
process.stdout.write(solve(input));
}
명령과 상태 변화
| 명령 | 처리 뒤 목록 | 출력 |
|---|---|---|
| ADD 봄 | 봄 | 출력 없음 |
| ADD 여름 | 봄 → 여름 | 출력 없음 |
| LIST | 봄 → 여름 유지 | 봄,여름 |
| PLAY | 여름 | 봄 |
| ADD 여름 | 여름 → 여름 | 출력 없음 |
| PLAY 두 번 | 빈 목록 | 여름, 여름 |
| PLAY | 빈 목록 유지 | EMPTY |
왜 이 구현으로 맞는가
전체 정답에 SinglyLinkedList를 포함했습니다. append는 tail을 이미 알고 있어 바로 연결하고, removeFirst는 head를 다음 노드로 옮깁니다. 이 두 연산에 임의 인덱스 조회가 필요하지 않으므로 단방향 연결 리스트로 충분합니다. LIST가 필요할 때만 모든 노드를 순서대로 따라갑니다.
목록이 빈 상태의 불변식은 head===null, tail===null, length===0입니다. 한 노드를 추가하면 head와 tail이 같은 노드를 가리키며 tail.next는 null입니다. removeFirst로 마지막 노드를 없앨 때 tail을 그대로 두면 다음 append가 이미 제거한 노드 뒤에 연결되므로 tail도 초기화해야 합니다.
solve는 ADD에 대해서는 출력하지 않고 PLAY의 결과와 LIST의 스냅샷만 output에 넣습니다. LIST는 관찰일 뿐 목록을 제거하지 않습니다. 따라서 다음 PLAY는 LIST에 표시된 첫 곡을 그대로 반환해야 합니다. 중복 제목도 별개 노드라 하나를 재생해도 다음 같은 제목이 남을 수 있습니다.
가이드와 같은 클래스에는 find와 remove도 들어 있지만 이 문제의 solve는 append, removeFirst, toArray만 사용합니다. 노드 참조와 head·tail이 공개되어 있으므로 외부에서 next를 수정하지 않는 계약을 지킵니다. 노드를 직접 건드리지 않고 메서드로만 목록을 바꾸면 상태 추적이 단순해집니다.
시간·공간과 문자열 비용
append와 removeFirst는 노드 연산 기준 최악 O(1)이고 LIST는 현재 K개 노드를 방문한 뒤 제목을 결합합니다. 입력 문자 수 S, 출력 문자 수 R, 명령 수 Q로 쓰면 전체 시간은 O(S+Q+R)입니다. 반복 LIST의 출력 때문에 R이 Q에 비해 크게 늘 수 있어 단순 O(Q)라고 할 수 없습니다. 구조만 보면 O(K) 노드지만 입력 문자열·줄 배열·출력 누적과 반환 문자열을 포함한 전체 공간은 O(S+Q+R+K), K≤Q이므로 O(S+Q+R)입니다. 제목 복사·연결 비용도 문자 수에 포함했습니다.
표본을 직접 확인하는 코드
다음 코드를 linked-list-playlist-check.cjs로 저장한 뒤 node linked-list-playlist-check.cjs로 실행하세요. assert가 일치하면 출력 없이 종료하고, 결과가 다르면 예외가 발생합니다. 같은 폴더의 정답 파일을 가져오는 이 블록은 풀이 본체가 아니라 표본 확인용입니다.
const assert = require('node:assert/strict');
const { solve } = require('./linked-list-playlist-problem.cjs');
const input = "8\nADD 봄\nADD 여름\nLIST\nPLAY\nADD 여름\nPLAY\nPLAY\nPLAY";
const expected = "봄,여름\n봄\n여름\n여름\nEMPTY";
assert.equal(solve(input), expected);
assert.equal(solve("1\nPLAY"), "EMPTY");
assert.equal(solve('0'), '');
연결 학습
단방향 연결 리스트 가이드에서 메서드별 불변식을 확인하세요. 공식 문제 사이트의 백준 1406 에디터는 다른 계약을 가진 추가 연습입니다. 공식 링크 확인일: 2026년 9월 10일.
풀이 전에 확인할 순서
- 입력값과 출력값을 한 문장으로 다시 적습니다.
- 반복할 대상과 비교·저장할 값을 정합니다.
- 필요한 자료구조와 시간복잡도를 예상합니다.
- 코드를 보기 전에 손으로 작은 예제를 계산합니다.
힌트: 코딩테스트 JS 연결 리스트: 재생 대기 목록 관리
정답 코드를 바로 따라 쓰기보다, 본문에서 값이 갱신되는 조건과 반복 범위를 먼저 찾으세요. 반복 한 번마다 반드시 유지되어야 하는 값이 무엇인지 적으면 풀이의 중심 변수를 고르기 쉽습니다.
테스트 확인
- 가능한 가장 작은 입력
- 같은 값이나 문자가 반복되는 입력
- 정답이 처음 또는 마지막 위치에서 결정되는 입력
- 입력 제한에 가까운 경우의 실행 시간
확인 결과: 본문의 예제뿐 아니라 위 경계 사례에서도 예상값과 실제 출력이 같아야 풀이가 완료됩니다.
이 글이 도움이 되었나요?
코딩테스트 JavaScript 학습 순서
필수 49개 · 전체 49개
읽음 기록 관리
전체 과정 목차 (49개)
- 필수 길잡이 · 코딩테스트 JS 자료구조 로드맵: 배열, 해시, 스택, 투 포인터 순서
- 필수 학습 · 세 수 중 최솟값 JavaScript 조건문 풀이 정리
- 필수 학습 · 삼각형 판별하기 JavaScript 풀이
- 필수 학습 · 연필 개수 JavaScript 풀이
- 필수 학습 · 1부터 N까지 합 출력하기 JavaScript 풀이
- 필수 학습 · 최솟값 구하기 JavaScript 풀이|배열 순회와 비교 갱신 원리
- 필수 학습 · 홀수 JavaScript 풀이: 조건 판별과 결과 처리 정리
- 필수 학습 · 10부제 JavaScript 풀이: 끝자리 비교로 위반 차량 수 세기
- 필수 학습 · A를 #으로 JavaScript 풀이: 문자열 순회와 치환
- 필수 학습 · 문자 찾기 JavaScript 풀이: 문자열 순회로 개수 세기
- 필수 학습 · 대문자 찾기 JavaScript 풀이
- 필수 학습 · 대문자로 통일 JavaScript 풀이
- 필수 학습 · 대소문자 변환 JavaScript 풀이
- 필수 학습 · 일곱 난쟁이 JavaScript 풀이: 두 명을 제외하는 완전탐색
- 필수 학습 · 코딩테스트 JS Map 풀이: 학급 회장 득표수 세기
- 필수 학습 · 코딩테스트 JS 스택 풀이: 올바른 괄호 검증하기
- 필수 학습 · 코딩테스트 JS 스택 풀이: 괄호문자 제거하기
- 필수 학습 · 코딩테스트 JS 스택 풀이: 크레인 인형뽑기 처리법
- 필수 학습 · 코딩테스트 JS 스택 풀이: 후위식 연산 계산하기
- 필수 학습 · 코딩테스트 JS 스택 풀이: 쇠막대기 레이저 절단 개수 세기
- 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 두 정렬 배열 합치기
- 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 공통 원소 추출하기
- 필수 학습 · 코딩테스트 JS 슬라이딩 윈도우 풀이: 최대 매출 구간 합 계산하기
- 필수 학습 · JavaScript 투 포인터: 합이 M인 연속 부분수열 개수
- 필수 학습 · 코딩테스트 JS 해시 풀이: 모든 아나그램 찾기
- 필수 학습 · 가장 긴 문자열 JavaScript 풀이
- 필수 학습 · 가운데 문자 출력 JavaScript 풀이
- 필수 학습 · 중복문자제거 JavaScript 풀이
- 필수 학습 · 코딩테스트 JS 고급: 최소 힙으로 다익스트라 최단 경로 구하기
- 필수 학습 · 코딩테스트 JS Union-Find: 연결 성분 수와 크기 구하기
- 필수 학습 · 코딩테스트 JS Trie: 접두사에 맞는 단어 수 세기
- 필수 학습 · 코딩테스트 JS Fenwick Tree: 값 갱신과 구간 합 처리
- 필수 학습 · 코딩테스트 JS 세그먼트 트리: 단일 대입과 구간 합
- 필수 학습 · 코딩테스트 JS LRU 캐시: 지도 타일 재사용 기록
- 필수 학습 · 코딩테스트 JS AVL 트리: 기준 이상 최솟값 찾기
- 필수 학습 · 코딩테스트 JS 큐: 상담 창구 대기열 명령 처리
- 필수 학습 · 코딩테스트 JS 연결 리스트: 재생 대기 목록 관리 현재 글
- 필수 학습 · JavaScript 원형 덱 연습: 최근 기록 창과 되돌리기
- 필수 학습 · JavaScript 해시 테이블 연습: 정규화 문자열 빈도와 등장 순서
- 필수 학습 · JavaScript 트리 순회 연습: 깊이별 노드 묶기
- 필수 학습 · JavaScript BST 연습: 닫힌 구간의 중복 키 보고서
- 필수 학습 · JavaScript 최소 힙 연습: 동률 순서를 지키는 작업 스케줄러
- 필수 학습 · JavaScript 그래프 연습: 연결 구역 크기를 작은 순서로 출력하기
- 필수 학습 · 중복단어제거 JavaScript 풀이
- 필수 학습 · TypeScript 이진 탐색 연습: 숫자 카드 존재 여부 확인
- 필수 학습 · 큰 수 출력하기 JavaScript 풀이
- 필수 학습 · 보이는 학생 JavaScript 풀이
- 필수 학습 · 가위바위보 JavaScript 풀이
- 필수 학습 · 점수계산 JavaScript 풀이
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.