읽기 전에 알아두면 좋아요
아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.
- JavaScript 최소 힙 구현: 우선순위 큐의 push·pop과 비교 함수힙은 가장 작은 후보나 가장 큰 후보를 먼저 꺼내도록 순서를 유지합니다. 작은 우선순위와 입력 순서의 복합 비교 기준을 적용합니다.
- JavaScript 배열: 인덱스 조회와 삽입·삭제 비용배열은 여러 값을 순서대로 담습니다. 인덱스로 값을 읽고 반복문으로 하나씩 살펴봅니다. 인덱스 접근과 배열을 순회할 때의 연산 비용을 확인합니다.
개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.
과정 위치와 선택 복습 안내
과정 위치: 트리·그래프와 최단거리 · 문제
과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.
JavaScript로 문법·실행 복습하기 (선택)
개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.
작업의 우선순위와 동률 규칙이 주어지는 스케줄러 문제는 무엇을 반환하나요?
작업의 우선순위와 동률 규칙을 지키며 실행 결과를 반환하는 문제입니다. 배열·비교 기준·최소 힙의 기본을 준비하고, 아래 입력·반환 계약을 확인해 직접 풀어보세요.
적용 조건·범위: 모든 작업이 처음부터 있고 전체 결과만 필요하면 (priority,입력 순번)으로 한 번 정렬하는 해법도 같은 점근 시간이며 더 짧습니다. 힙 구현의 학습 가치는 동적 우선순위 큐에 있습니다. 아래 코드에는 실행 중 우선순위 변경이나 임의 작업 취소 기능이 없으며 그런 기능을 있다고 가정하지 않습니다.
독립 창작 문제
이 문제의 상황·입출력 계약·예시는 이 글을 위해 직접 구성했습니다. 공식 문제를 번역하거나 복제한 지문이 아닙니다. 구현 개념은 JavaScript 최소 힙 구현: 우선순위 큐의 push·pop과 비교 함수에서 먼저 확인할 수 있습니다.
실행 대기 중인 작업 목록 tasks가 주어집니다. priority가 작은 작업부터 처리한 ID 목록을 반환하세요. priority가 같으면 입력 배열에서 먼저 등장한 작업을 먼저 처리합니다. 작업 ID가 같아도 별개 요청이므로 합치거나 제거하지 않습니다.
이 문제는 실행 중에 새 작업이 도착하는 시뮬레이션이 아닙니다. 모든 작업을 먼저 넣은 뒤 하나씩 꺼냅니다. 일괄 입력만 놓고 보면 정렬로도 풀 수 있지만, 여기서는 이후 삽입과 추출이 섞이는 상황에도 사용할 수 있는 최소 힙의 비교 계약을 연습합니다.
입력·출력과 제약
표준 입력은 {tasks} JSON 객체입니다. tasks는 길이 Q가 0~20000인 배열이며 각 작업은 {id,priority}입니다. id는 길이 1~100의 문자열, priority는 -1000000~1000000의 정수입니다. 입력은 이 형식을 만족합니다. 표준 출력은 작업이 처리되는 순서대로 ID를 담은 JSON 배열입니다.
priority가 음수여도 더 작은 숫자가 먼저라는 규칙은 같습니다. 출력에는 모든 작업이 정확히 한 번씩 나타나므로 중복 ID도 그대로 반복됩니다. 입력 작업 객체를 수정하지 않고 새 객체에 내부용 order를 추가합니다.
입력 예시
{"tasks":[{"id":"메일","priority":2},{"id":"백업","priority":-1},{"id":"점검","priority":2},{"id":"메일","priority":0}]}
출력 예시
["백업","메일","메일","점검"]
실행 방법
Node.js CommonJS 환경에서 정답의 전체 코드를 heap-problem.cjs로 저장합니다. 입력 예시를 UTF-8 input.json 파일로 저장한 뒤 Windows PowerShell에서는 아래 명령을 실행하세요. 내장 node:fs 외에 다른 파일이나 패키지가 필요하지 않습니다.
Get-Content -Raw -Encoding UTF8 ./input.json | node ./heap-problem.cjs
macOS·Linux의 Bash에서는 아래 입력 리다이렉션을 사용할 수 있습니다. 이 < 문법은 PowerShell 명령이 아닙니다.
node ./heap-problem.cjs < input.json
힌트
비교의 첫 기준을 priority, 두 번째 기준을 입력 인덱스 order로 두세요. ID의 사전순은 요구된 기준이 아닙니다. 힙이 동률을 알아서 안정적으로 처리할 것이라고 가정하지 않아야 합니다.
직접 풀어보기
먼저 선택한 언어의 입력·반환 계약에 맞춰 작성해 보세요. 힌트 받기와 정답과 비교하기는 코드를 실행하지 않는 AI 분석입니다. 해답·해설은 아래에서 필요할 때 펼칠 수 있습니다.
AI 힌트
코드를 실행한 채점 결과가 아닌 AI의 코드 분석입니다.
해답·해설 보기
'use strict';
class MinHeap {
constructor(compare =(a, b) => a - b) {
if (typeof compare !== 'function') throw new TypeError('compare must be a function');
this.compare = compare;
this.data = [];
}
get size() {
return this.data.length;
}
peek() {
return this.data[0];
}
push(value) {
if (value === undefined) throw new TypeError('undefined is reserved for an empty heap');
let hole = this.data.length;
this.data.push(value);
while (hole > 0) {
const parent = Math.floor((hole - 1) / 2);
if (this.compare(this.data[parent], value) <= 0) break;
this.data[hole] = this.data[parent];
hole = parent;
}
this.data[hole] = value;
}
pop() {
if (this.data.length === 0) return undefined;
const minimum = this.data[0];
const last = this.data.pop();
if (this.data.length === 0) return minimum;
let hole = 0;
while (hole * 2 + 1 < this.data.length) {
let child = hole * 2 + 1;
const right = child + 1;
if (right < this.data.length &&
this.compare(this.data[right], this.data[child]) < 0) {
child = right;
}
if (this.compare(last, this.data[child]) <= 0) break;
this.data[hole] = this.data[child];
hole = child;
}
this.data[hole] = last;
return minimum;
}
}
function solve( {
tasks
}) {
const heap = new MinHeap((a, b) => a.priority - b.priority || a.order - b.order);
tasks.forEach((task, order) => heap.push( {
... task, order
}));
const result = [];
while (heap.size > 0) result.push(heap.pop().id);
return result;
}
원문 JSON 객체를 파싱한 객체 input을 solve(input)에 전달하고 배열을 반환합니다. 표준 입출력은 포함하지 않습니다.
import heapq
def solve(input):
heap = [(t['priority'],i,t['id']) for i,t in enumerate(input['tasks'])]
heapq.heapify(heap)
return [heapq.heappop(heap)[2] for _ in range(len(heap))]
원문 JSON 객체를 파싱한 dict input을 solve(input)에 전달하고 list를 반환합니다. 표준 입출력은 포함하지 않습니다.
import java.util.*;
class Solution {
public String[] solve(String[] ids, int[] priorities) {
PriorityQueue < Integer > q = new PriorityQueue < >((a, b)->
priorities[a] != priorities[b]
? Integer.compare(priorities[a], priorities[b])
: Integer.compare(a, b)
);
for (int i = 0; i < ids.length; i++) q.add(i);
String[] out = new String[ids.length];
for (int i = 0; i < out.length; i++) out[i] = ids[q.remove()];
return out;
}
}
동일 길이 ids/priorities 배열에 각 작업의 ID와 우선순위를 같은 인덱스로 전달합니다. solve가 String[]를 반환합니다.
#include <string>
#include <vector>
#include <queue>
std::vector < std::string > solve(const std::vector < std::string > & ids, const std::vector < int > & priorities) {
auto later = [&](int a, int b) {
return priorities[a] != priorities[b] ? priorities[a] > priorities[b] : a > b;
};
std::priority_queue < int, std::vector < int >, decltype(later) > q(later);
for (int i = 0; i <(int) ids.size(); i++) q.push(i);
std::vector < std::string > out;
while (! q.empty()) {
out.push_back(ids[q.top()]);
q.pop();
}
return out;
}
동일 길이 ids/priorities 벡터를 입력 순서대로 전달합니다. solve가 ID 벡터를 반환합니다. C++11 이상.
using System.Collections.Generic;
public class Solution {
static bool Before(int a, int b, int[] p) {
return p[a] < p[b] ||(p[a] == p[b] && a < b);
}
public string[] Solve(string[] ids, int[] priorities) {
var heap = new List < int >();
for (int i = 0; i < ids.Length; i++) {
int k = heap.Count;
heap.Add(i);
while (k > 0) {
int parent =(k - 1) / 2;
if (! Before(heap[k], heap[parent], priorities)) break;
int t = heap[k];
heap[k] = heap[parent];
heap[parent] = t;
k = parent;
}
}
var result = new List < string >();
while (heap.Count > 0) {
result.Add(ids[heap[0]]);
int last = heap[heap.Count - 1];
heap.RemoveAt(heap.Count - 1);
if (heap.Count == 0) break;
heap[0] = last;
int k = 0;
while (k * 2 + 1 < heap.Count) {
int child = k * 2 + 1;
if (child + 1 < heap.Count && Before(heap[child + 1], heap[child], priorities)) child++;
if (! Before(heap[child], heap[k], priorities)) break;
int t = heap[k];
heap[k] = heap[child];
heap[child] = t;
k = child;
}
}
return result.ToArray();
}
}
동일 길이 ids/priorities 배열을 입력 순서대로 전달합니다. Solve가 string[]를 반환합니다.
한 파일로 실행하는 전체 정답
'use strict';
class MinHeap {
constructor(compare = (a, b) => a - b) {
if (typeof compare !== 'function') throw new TypeError('compare must be a function');
this.compare = compare;
this.data = [];
}
get size() {
return this.data.length;
}
peek() {
return this.data[0];
}
push(value) {
if (value === undefined) throw new TypeError('undefined is reserved for an empty heap');
let hole = this.data.length;
this.data.push(value);
while (hole > 0) {
const parent = Math.floor((hole - 1) / 2);
if (this.compare(this.data[parent], value) <= 0) break;
this.data[hole] = this.data[parent];
hole = parent;
}
this.data[hole] = value;
}
pop() {
if (this.data.length === 0) return undefined;
const minimum = this.data[0];
const last = this.data.pop();
if (this.data.length === 0) return minimum;
let hole = 0;
while (hole * 2 + 1 < this.data.length) {
let child = hole * 2 + 1;
const right = child + 1;
if (right < this.data.length &&
this.compare(this.data[right], this.data[child]) < 0) {
child = right;
}
if (this.compare(last, this.data[child]) <= 0) break;
this.data[hole] = this.data[child];
hole = child;
}
this.data[hole] = last;
return minimum;
}
}
function solve({ tasks }) {
const heap = new MinHeap((a, b) => a.priority - b.priority || a.order - b.order);
tasks.forEach((task, order) => heap.push({ ...task, order }));
const result = [];
while (heap.size > 0) result.push(heap.pop().id);
return result;
}
module.exports = { solve };
if (require.main === module) {
const fs = require('node:fs');
const input = JSON.parse(fs.readFileSync(0, 'utf8').replace(/^\uFEFF/, ''));
process.stdout.write(JSON.stringify(solve(input)) + '\n');
}
정답 해설: 안정성을 비교 키로 바꿉니다
tasks.forEach의 인덱스는 입력 순서이며 모든 작업에서 다릅니다. (priority,order)를 사전식으로 비교하면 서로 다른 작업의 순서가 하나로 정해집니다. priority 차이가 0일 때만 order 차이를 사용하는 || 표현으로 이 계약을 구현합니다. 문제의 숫자 범위에서는 차이 연산도 안전한 정수입니다.
최소 힙의 부모는 항상 자식보다 앞선 작업입니다. 모든 작업을 push한 뒤 루트부터 pop하면 현재 남은 작업 중 가장 작은 비교 키가 차례로 나옵니다. 같은 우선순위라도 order가 작을수록 앞서므로 힙 내부 교체가 입력 순서를 바꿀 수 없습니다.
| 작업 ID | priority | order | 처리 순위 |
|---|---|---|---|
| 메일(첫 번째) | 2 | 0 | 3 |
| 백업 | -1 | 1 | 1 |
| 점검 | 2 | 2 | 4 |
| 메일(두 번째) | 0 | 3 | 2 |
두 메일 요청은 ID가 같아도 order가 다르고 priority도 다릅니다. 결과에서 문자열만 보면 두 요청을 구분하지 못하지만 각각 한 번씩 실행된 것입니다. 반환 결과를 Set으로 만들면 요청 하나를 잃게 됩니다. 힙에 원본 객체를 그대로 넣고 order를 덧쓰지 않는 이유도 입력에 부수 효과를 만들지 않기 위해서입니다.
복잡도와 대안
풀이 직접 실행
본문 풀이에 예제 입력을 넣고, 입력을 바꿔 계산 결과를 비교합니다.
예제 크게 보기 (새 탭)작업 Q개를 하나씩 push하고 Q번 pop하므로 시간은 O(Q log(Q+1))입니다. 비교는 작은 정수 두 개만 사용해 O(1)입니다. 입력 문자열 총길이를 C라고 하면 입력·힙 객체·출력 배열을 포함한 공간은 O(Q+C)입니다. 문자열은 참조로 사용하므로 매 비교마다 ID 전체를 읽지 않습니다.
모든 작업이 처음부터 있고 전체 결과만 필요하면 (priority,입력 순번)으로 한 번 정렬하는 해법도 같은 점근 시간이며 더 짧습니다. 힙 구현의 학습 가치는 동적 우선순위 큐에 있습니다. 아래 코드에는 실행 중 우선순위 변경이나 임의 작업 취소 기능이 없으며 그런 기능을 있다고 가정하지 않습니다.
직접 확인할 경계
빈 tasks, 작업 하나, 모든 priority가 같은 목록, 음수 priority, 중복 ID를 확인하세요. 각 작업에 입력 인덱스를 붙여 배열 정렬한 결과를 기준으로 무작위 테스트하면 힙이 우선순위와 동률 계약을 동시에 지키는지 검증할 수 있습니다.
함께 연습하고 확인할 자료
Kth Largest Element in a Stream: 스트림에서 K번째 큰 값을 유지하는 외부 연습입니다. 이 글의 모든 작업 출력 문제와는 목표가 다릅니다.
관련 개념 참고: Princeton Algorithms — Priority Queues 공식 자료와 문제 페이지는 2026년 9월 10일 확인했습니다. 외부 문제의 지문·예시를 복제하지 않고 이 글의 예제와 창작 문제를 별도로 구성했습니다.
이 문제를 푼 뒤 확인하기
우선순위가 같은 작업을 넣은 순서대로 꺼내는지와 힙이 빈 상태를 확인해 보세요.
본문의 예제와 이 반례를 직접 실행해 예상 출력과 비교하고, 결과가 다르면 입력 조건과 반복 범위를 다시 확인하세요.
글 소개·학습 준비·전체 목차

핵심 요약
우선순위가 낮은 숫자인 작업부터 실행하되 동률은 입력 순서를 지킵니다. 작업 ID의 중복과 정렬 안정성을 구분하는 독립 창작 문제입니다.
이 글이 도움이 되었나요?
이어서 공부하기
체크포인트: 해설을 가리고 다시 풀고, 경계 입력을 하나 바꾸어 예상 결과와 실제 결과를 비교하세요. 층별 방문 순서와 최솟값 추출을 추적하고, 다익스트라의 비음수 조건과 오래된 후보를 설명하세요.
다음 추천 · 자료구조 개념: JavaScript 그래프 구현: 인접 리스트·인접 행렬 비교와 BFS
권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.
원래 과정 목록 순서 (선택)
읽음 기록
읽음 — / 전체 58개 · 필수 58개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.