[창작 문제] 기준 이상 등록번호 안내
등록번호를 순서 없이 추가하면서 방문자가 제시한 기준 이상인 가장 작은 번호를 안내합니다. 중복 등록은 한 번으로 취급하며 없는 답은 null입니다.

이 문제는 BlogFlow 자료구조 학습용으로 독립 창작했습니다. 외부 문제의 지문이나 테스트를 옮기지 않았습니다.
문제
전시 안내 데스크는 등록된 작품 번호를 관리합니다. add 작업은 번호를 등록하고 next 작업은 기준 이상인 등록번호 중 가장 작은 번호를 묻습니다. 기준과 같은 번호가 있으면 그 번호가 답입니다. 어떤 등록번호도 기준 이상이 아니면 null을 반환하세요. 등록 취소 작업은 없습니다.
관련 구현 설명: JavaScript AVL 트리: 높이 불변식과 LL·RR·LR·RL 삽입 회전
입력·출력과 제약
JSON 객체 {ops}. 작업은 ["add",key] 또는 ["next",threshold]입니다.
next 작업의 답을 순서대로 담은 JSON 배열입니다. 각 답은 정수 또는 null입니다.
작업 수 0~100,000. 번호와 기준은 정수이고 절댓값 1,000,000,000 이하입니다. 같은 번호를 여러 번 등록할 수 있지만 집합 원소 수는 한 번만 증가합니다.
입력은 위 계약을 만족한다고 가정합니다. 온라인 채점기는 제공하지 않으며 로컬 Node.js에서 JSON 입력으로 실행합니다.
예시
예시 1 입력:
{"ops":[["next",15],["add",30],["add",10],["add",20],["add",20],["next",15],["next",30],["next",31]]}
예시 1 출력:
[null,20,30,null]
예시 2 입력:
{"ops":[]}
예시 2 출력:
[]
힌트
현재 노드가 기준 이상이면 후보로 기록하고 왼쪽에서 더 작은 답을 찾으세요. 현재 노드가 기준 미만이면 왼쪽은 볼 필요가 없습니다.
정답과 해설
정답 코드와 해설 펼치기
Node.js 24 CommonJS용 전체 풀이입니다. avl-problem.cjs로 저장하세요. 클래스 선언, solve 함수, 표준 입력 처리가 모두 들어 있어 가이드 파일을 별도로 가져오지 않습니다. 예시 입력을 input.json에 저장하고 터미널에서 실행합니다.
Windows PowerShell:
Get-Content -Raw -Encoding UTF8 .input.json | node .avl-problem.cjs
macOS / Linux:
node avl-problem.cjs < input.json
class AVLSet {
constructor() {
this.root = null;
this.size = 0;
}
check(key) {
if (!Number.isSafeInteger(key)) throw new TypeError('키는 안전한 정수여야 합니다.');
}
height(node) { return node?.height ?? 0; }
update(node) {
node.height = 1 + Math.max(this.height(node.left), this.height(node.right));
}
rotateRight(y) {
const x = y.left, middle = x.right;
x.right = y;
y.left = middle;
this.update(y);
this.update(x);
return x;
}
rotateLeft(x) {
const y = x.right, middle = y.left;
y.left = x;
x.right = middle;
this.update(x);
this.update(y);
return y;
}
add(key) {
this.check(key);
const before = this.size;
this.root = this.insert(this.root, key);
return this.size !== before;
}
insert(node, key) {
if (!node) {
this.size++;
return { key, left: null, right: null, height: 1 };
}
if (key < node.key) node.left = this.insert(node.left, key);
else if (key > node.key) node.right = this.insert(node.right, key);
else return node;
this.update(node);
const balance = this.height(node.left) - this.height(node.right);
if (balance > 1) {
if (key > node.left.key) node.left = this.rotateLeft(node.left);
return this.rotateRight(node);
}
if (balance < -1) {
if (key < node.right.key) node.right = this.rotateRight(node.right);
return this.rotateLeft(node);
}
return node;
}
has(key) {
this.check(key);
let node = this.root;
while (node) {
if (key === node.key) return true;
node = key < node.key ? node.left : node.right;
}
return false;
}
ceiling(key) {
this.check(key);
let node = this.root, candidate = null;
while (node) {
if (node.key >= key) {
candidate = node.key;
node = node.left;
} else node = node.right;
}
return candidate;
}
toArray() {
const result = [];
const visit = (node) => {
if (!node) return;
visit(node.left);
result.push(node.key);
visit(node.right);
};
visit(this.root);
return result;
}
}
function solve({ ops }) {
const tree = new AVLSet(), out = [];
for (const [type, key] of ops) {
if (type === 'add') tree.add(key);
else if (type === 'next') out.push(tree.ceiling(key));
else throw new Error('알 수 없는 작업입니다.');
}
return out;
}
module.exports = { solve };
if (require.main === module) {
const input = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
console.log(JSON.stringify(solve(input)));
}
아직 번호가 없을 때 next(15)는 null입니다. 30,10,20 등록은 LR 회전을 만들고 최종 루트가 20이 됩니다. 20을 다시 넣어도 집합은 {10,20,30}입니다. 기준 15의 최소 후보는 20, 기준 30의 후보는 자기 자신 30, 기준 31에는 후보가 없습니다. solve는 등록 시 add, 질의 시 ceiling만 호출하므로 전체 정렬을 반복하지 않습니다.
모든 노드에서 왼쪽 키는 node.key보다 작고 오른쪽 키는 큽니다. 중복은 새 노드로 만들지 않으므로 엄격한 부등호를 쓸 수 있습니다. 동시에 node.height는 1+max(왼쪽 높이,오른쪽 높이)이고, 균형 인수 balance=왼쪽 높이−오른쪽 높이는 −1,0,1이어야 합니다.
삽입은 먼저 일반 BST와 같은 한 경로를 내려갑니다. 재귀가 돌아올 때 그 경로의 높이를 다시 계산하고 균형을 고칩니다. 회전은 중위 순서를 바꾸지 않고 부모·자식 관계만 재배치합니다. 오른쪽 회전에서 x.right였던 middle은 y.left로 옮겨야 모든 키가 x< middle <y 순서를 보존합니다.
q개 작업, 최대로 저장한 서로 다른 키 수 u에 대해 최악 O(q log(u+1)) 시간입니다. 구조 O(u), 삽입 재귀 O(log(u+1)), 반환 결과 O(q) 공간이며 삭제 비용은 포함하지 않습니다.
표본과 경계를 직접 확인하려면 다음 코드를 avl-check.cjs로 저장하고 같은 폴더에서 node avl-check.cjs를 실행하세요. assert가 맞으면 출력 없이 종료하고, 다르면 예외와 함께 실패합니다.
const assert = require('node:assert/strict');
const { solve } = require('./avl-problem.cjs');
assert.deepEqual(
solve({
"ops": [
[
"next",
15
],
[
"add",
30
],
[
"add",
10
],
[
"add",
20
],
[
"add",
20
],
[
"next",
15
],
[
"next",
30
],
[
"next",
31
]
]
}),
[null,20,30,null],
);
assert.deepEqual(
solve({
"ops": []
}),
[],
);
연결 학습과 공식 자료
JavaScript AVL 트리: 높이 불변식과 LL·RR·LR·RL 삽입 회전에서 연산별 이유와 전체 복잡도를 이어서 확인할 수 있습니다. 다른 형식의 공식 연습은 LeetCode 1382 – Balance a Binary Search Tree를 참고하세요. 외부 문제의 정답이나 지문은 이 페이지에 복제하지 않았습니다.
공식 자료 확인일: 2026년 9월 10일. 구현은 이 글의 JavaScript 계약에 맞춰 독립적으로 작성했습니다. 다른 언어 라이브러리의 API와 숫자 범위가 그대로 적용되는 것은 아닙니다.
풀이 전에 확인할 순서
- 입력값과 출력값을 한 문장으로 다시 적습니다.
- 반복할 대상과 비교·저장할 값을 정합니다.
- 필요한 자료구조와 시간복잡도를 예상합니다.
- 코드를 보기 전에 손으로 작은 예제를 계산합니다.
힌트: 코딩테스트 JS AVL 트리: 기준 이상 최솟값 찾기
정답 코드를 바로 따라 쓰기보다, 본문에서 값이 갱신되는 조건과 반복 범위를 먼저 찾으세요. 반복 한 번마다 반드시 유지되어야 하는 값이 무엇인지 적으면 풀이의 중심 변수를 고르기 쉽습니다.
테스트 확인
- 가능한 가장 작은 입력
- 같은 값이나 문자가 반복되는 입력
- 정답이 처음 또는 마지막 위치에서 결정되는 입력
- 입력 제한에 가까운 경우의 실행 시간
확인 결과: 본문의 예제뿐 아니라 위 경계 사례에서도 예상값과 실제 출력이 같아야 풀이가 완료됩니다.
이 글이 도움이 되었나요?
코딩테스트 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의 새 글을 확인할 수 있습니다.