읽기 전에 알아두면 좋아요
아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.
- JavaScript 원형 덱 구현: 양끝 삽입·삭제와 고정 용량 버퍼덱은 앞과 뒤 양쪽에서 값을 넣고 뺄 수 있는 대기열입니다. 양끝 삽입·삭제와 용량 초과 정책을 분리합니다.
- 큐와 FIFO: head 인덱스로 JavaScript 대기열 만들기큐는 먼저 넣은 값을 먼저 꺼냅니다. 대기열에 넣고 꺼낼 때의 순서를 확인합니다. 먼저 들어온 값을 먼저 꺼내는 큐의 동작을 확인합니다.
개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.
과정 위치와 선택 복습 안내
과정 위치: 자료구조 개념과 문제 · 문제
과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.
JavaScript로 문법·실행 복습하기 (선택)
개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.
최근 기록 창 문제는 어떤 두 동작을 처리해 어떤 형태의 결과를 반환하나요?
제한된 최근 기록 창의 추가와 되돌리기를 처리해 결과 배열을 반환하는 문제입니다. 배열·인덱스·덱의 기본을 준비하고, 아래 입력·반환 계약을 확인해 직접 풀어보세요.
적용 조건·범위: 표준 입력은 하나의 JSON 객체 {capacity,events}입니다. capacity는 1~1000의 정수, events는 길이 Q가 0~1000인 배열입니다. 각 이벤트는 {type:”add”,value} 또는 {type:”undo”}이며 value는 null, 불리언, 안전 정수 또는 길이 100 이하 문자열입니다. undefined는 JSON 값이 아니므로 문제 입력에는 없습니다. 입력은 이 형식을 만족한다고 가정합니다.
본문 근거: Design Circular Deque
독립 창작 문제
이 문제의 상황·입출력 계약·예시는 이 글을 위해 직접 구성했습니다. 공식 문제를 번역하거나 복제한 지문이 아닙니다. 구현 개념은 JavaScript 원형 덱 구현: 양끝 삽입·삭제와 고정 용량 버퍼에서 먼저 확인할 수 있습니다.
기록 창은 처음에 비어 있고 최대 capacity개의 값을 오래된 순서부터 저장합니다. add 이벤트가 오면 뒤에 값을 넣습니다. 이미 가득 찼다면 가장 오래된 값 하나를 앞에서 제거한 뒤 새 값을 넣습니다. undo 이벤트는 현재 창의 가장 최근 값 하나만 제거하며 빈 창에서는 아무 일도 하지 않습니다.
각 이벤트를 처리한 직후의 창을 배열로 복사해 결과에 추가하세요. 퇴출되었거나 undo로 지운 기록은 복구하지 않습니다. undo가 이전 add 작업의 전체 상태를 되돌리는 명령이 아니라 현재 창의 마지막 항목을 삭제하는 명령이라는 점에 유의하세요.
입력·출력과 제약
표준 입력은 하나의 JSON 객체 {capacity,events}입니다. capacity는 1~1000의 정수, events는 길이 Q가 0~1000인 배열입니다. 각 이벤트는 {type:”add”,value} 또는 {type:”undo”}이며 value는 null, 불리언, 안전 정수 또는 길이 100 이하 문자열입니다. undefined는 JSON 값이 아니므로 문제 입력에는 없습니다. 입력은 이 형식을 만족한다고 가정합니다.
표준 출력은 Q개의 스냅숏을 가진 JSON 배열입니다. 각 스냅숏의 항목은 오래된 것부터 최근 것 순서입니다. capacity를 넘지 않아야 하며 같은 값의 중복도 별개 기록으로 보관합니다. 반환한 스냅숏은 나중 이벤트로 변경되면 안 됩니다.
입력 예시
{"capacity":2,"events":[{"type":"add","value":"봄"},{"type":"add","value":"여름"},{"type":"add","value":"가을"},{"type":"undo"},{"type":"undo"},{"type":"undo"}]}
출력 예시
[["봄"],["봄","여름"],["여름","가을"],["여름"],[],[]]
실행 방법
Node.js CommonJS 환경에서 정답의 전체 코드를 deque-problem.cjs로 저장합니다. 입력 예시를 UTF-8 input.json 파일로 저장한 뒤 Windows PowerShell에서는 아래 명령을 실행하세요. 내장 node:fs 외에 다른 파일이나 패키지가 필요하지 않습니다.
Get-Content -Raw -Encoding UTF8 ./input.json | node ./deque-problem.cjs
macOS·Linux의 Bash에서는 아래 입력 리다이렉션을 사용할 수 있습니다. 이 < 문법은 PowerShell 명령이 아닙니다.
node ./deque-problem.cjs < input.json
힌트
덱 자체를 “가득 차면 자동 삭제”로 바꾸지 않아도 됩니다. pushBack 실패 여부를 보고 popFront와 재삽입을 수행하세요. 매번 같은 내부 배열 참조를 결과에 넣지 말고 논리 순서를 복사해야 이전 상태가 유지됩니다.
직접 풀어보기
먼저 선택한 언어의 입력·반환 계약에 맞춰 작성해 보세요. 힌트 받기와 정답과 비교하기는 코드를 실행하지 않는 AI 분석입니다. 해답·해설은 아래에서 필요할 때 펼칠 수 있습니다.
AI 힌트
코드를 실행한 채점 결과가 아닌 AI의 코드 분석입니다.
해답·해설 보기
'use strict';
class CircularDeque {
constructor(capacity) {
if (! Number.isInteger(capacity) || capacity < 1) {
throw new RangeError('capacity must be a positive integer');
}
this.data = new Array(capacity);
this.capacity = capacity;
this.front = 0;
this.length = 0;
}
pushFront(value) {
if (this.length === this.capacity) return false;
this.front =(this.front - 1 + this.capacity) % this.capacity;
this.data[this.front] = value;
this.length += 1;
return true;
}
pushBack(value) {
if (this.length === this.capacity) return false;
const index =(this.front + this.length) % this.capacity;
this.data[index] = value;
this.length += 1;
return true;
}
popFront() {
if (this.length === 0) return {
done : true
};
const value = this.data[this.front];
this.data[this.front] = undefined;
this.front =(this.front + 1) % this.capacity;
this.length -= 1;
return {
done : false, value
};
}
popBack() {
if (this.length === 0) return {
done : true
};
const index =(this.front + this.length - 1) % this.capacity;
const value = this.data[index];
this.data[index] = undefined;
this.length -= 1;
return {
done : false, value
};
}
toArray() {
const result = [];
for (let offset = 0; offset < this.length; offset += 1) {
result.push(this.data[(this.front + offset) % this.capacity]);
}
return result;
}
}
function solve( {
capacity, events
}) {
const deque = new CircularDeque(capacity);
const snapshots = [];
for (const event of events) {
if (event.type === 'add') {
if (! deque.pushBack(event.value)) {
deque.popFront();
deque.pushBack(event.value);
}
} else if (event.type === 'undo') {
deque.popBack();
} else {
throw new TypeError('unknown event type');
}
snapshots.push(deque.toArray());
}
return snapshots;
}
원문 JSON 객체를 파싱한 객체 input을 solve(input)에 전달하고 배열을 반환합니다. 표준 입출력은 포함하지 않습니다.
def solve(input):
cap=input['capacity']
data=[None]*cap
front=size=0
out=[]
for event in input['events']:
if event['type']=='add':
if size==cap:
front=(front+1)%cap
size-=1
data[(front+size)%cap]=event['value']
size+=1
elif size:
size-=1
data[(front+size)%cap]=None
out.append([data[(front+i)%cap] for i in range(size)])
return out
원문 JSON 객체를 파싱한 dict input을 solve(input)에 전달하고 list를 반환합니다. 표준 입출력은 포함하지 않습니다.
import java.util.*;
class Solution {
static class Event {
String type;
Object value;
Event(String t, Object v) {
type = t;
value = v;
}
}
public Object[][] solve(int capacity, Event[] events) {
Object[] data = new Object[capacity];
int front = 0, size = 0;
List < Object[] > out = new ArrayList < >();
for (Event e : events) {
if (e.type.equals("add")) {
if (size == capacity) {
front =(front + 1) % capacity;
size--;
}
data[(front + size) % capacity] = e.value;
size++;
} else if (size > 0) {
size--;
data[(front + size) % capacity] = null;
}
Object[] row = new Object[size];
for (int i = 0; i < size; i++) row[i] = data[(front + i) % capacity];
out.add(row);
}
return out.toArray(new Object[0][]);
}
}
capacity와 Event[]를 전달합니다. Event(type,value)에서 value는 null/Boolean/Long/String입니다. undo의 value는 null로 둡니다. 스냅숏 Object[][] 반환.
#include <cstddef>
#include <string>
#include <vector>
#include <variant>
using Value = std::variant < std::nullptr_t, bool, long long, std::string >;
struct Event {
std::string type;
Value value = nullptr;
};
std::vector < std::vector < Value >> solve(int capacity, const std::vector < Event > & events) {
std::vector < Value > data(capacity);
int front = 0, size = 0;
std::vector < std::vector < Value >> out;
for (const auto & e : events) {
if (e.type == "add") {
if (size == capacity) {
front =(front + 1) % capacity;
size--;
}
data[(front + size) % capacity] = e.value;
size++;
} else if (size) {
size--;
data[(front + size) % capacity] = nullptr;
}
std::vector < Value > row;
for (int i = 0; i < size; i++) row.push_back(data[(front + i) % capacity]);
out.push_back(row);
}
return out;
}
C++17 이상. Event(type,value)의 Value는 nullptr,bool,long long,UTF-8 std::string 중 하나입니다. add와 undo 타입을 그대로 전달하고 스냅숏 2차원 벡터를 반환합니다.
using System.Collections.Generic;
public class Solution {
public class Event {
public string type;
public object value;
public Event(string t, object v) {
type = t;
value = v;
}
}
public object[][] Solve(int capacity, Event[] events) {
var data = new object[capacity];
int front = 0, size = 0;
var result = new List < object[] >();
foreach(var e in events) {
if (e.type == "add") {
if (size == capacity) {
front =(front + 1) % capacity;
size--;
}
data[(front + size) % capacity] = e.value;
size++;
} else if (size > 0) {
size--;
data[(front + size) % capacity] = null;
}
var row = new object[size];
for (int i = 0; i < size; i++) row[i] = data[(front + i) % capacity];
result.Add(row);
}
return result.ToArray();
}
}
capacity와 Event[]를 전달합니다. value는 null,bool,long,string이며 undo는 null을 사용합니다. object[][] 스냅숏을 반환합니다.
한 파일로 실행하는 전체 정답
'use strict';
class CircularDeque {
constructor(capacity) {
if (!Number.isInteger(capacity) || capacity < 1) {
throw new RangeError('capacity must be a positive integer');
}
this.data = new Array(capacity);
this.capacity = capacity;
this.front = 0;
this.length = 0;
}
pushFront(value) {
if (this.length === this.capacity) return false;
this.front = (this.front - 1 + this.capacity) % this.capacity;
this.data[this.front] = value;
this.length += 1;
return true;
}
pushBack(value) {
if (this.length === this.capacity) return false;
const index = (this.front + this.length) % this.capacity;
this.data[index] = value;
this.length += 1;
return true;
}
popFront() {
if (this.length === 0) return { done: true };
const value = this.data[this.front];
this.data[this.front] = undefined;
this.front = (this.front + 1) % this.capacity;
this.length -= 1;
return { done: false, value };
}
popBack() {
if (this.length === 0) return { done: true };
const index = (this.front + this.length - 1) % this.capacity;
const value = this.data[index];
this.data[index] = undefined;
this.length -= 1;
return { done: false, value };
}
toArray() {
const result = [];
for (let offset = 0; offset < this.length; offset += 1) {
result.push(this.data[(this.front + offset) % this.capacity]);
}
return result;
}
}
function solve({ capacity, events }) {
const deque = new CircularDeque(capacity);
const snapshots = [];
for (const event of events) {
if (event.type === 'add') {
if (!deque.pushBack(event.value)) {
deque.popFront();
deque.pushBack(event.value);
}
} else if (event.type === 'undo') {
deque.popBack();
} else {
throw new TypeError('unknown event type');
}
snapshots.push(deque.toArray());
}
return snapshots;
}
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');
}
정답 해설: 자료구조와 정책을 분리합니다
CircularDeque는 가득 찬 삽입에 false만 돌려줍니다. solve는 이 실패를 기록 창의 퇴출 조건으로 해석해 앞의 한 항목을 제거합니다. 용량이 1 이상이므로 한 번 제거한 다음 재삽입은 반드시 성공합니다. 이 논리는 버퍼 정책이며 덱 메서드의 보편적인 동작은 아닙니다.
undo는 popBack의 반환값이 done:true여도 그대로 진행합니다. 빈 상태의 undo가 유효한 명령이기 때문입니다. 이벤트마다 toArray가 새 배열을 만들어 snapshots에 넣으므로 그 뒤 덱의 슬롯이 재사용되어도 지난 스냅숏은 바뀌지 않습니다. 이 문제의 값은 원시값으로 제한해 깊은 객체 복사 문제도 피합니다.
| 이벤트 | 처리 | 스냅숏 |
|---|---|---|
| add 봄 | 뒤에 추가 | [봄] |
| add 여름 | 뒤에 추가 | [봄,여름] |
| add 가을 | 봄 퇴출 후 추가 | [여름,가을] |
| undo | 가을 삭제 | [여름] |
| undo | 여름 삭제 | [] |
| undo | 빈 상태 유지 | [] |
불변식은 이벤트 i개를 처리한 뒤 덱이 “지금 창에 남은 기록”만 순서대로 보관하고 길이는 capacity 이하라는 것입니다. add가 남길 수 있는 가장 오래된 기록만 하나 버리고 undo가 마지막 하나만 지우므로 각 분기가 이 조건을 유지합니다. 출력은 그 상태를 그대로 복사합니다.
복잡도
K는 capacity, Q는 이벤트 수, S는 모든 스냅숏 길이의 합입니다. 덱 갱신은 이벤트마다 O(1)이지만 출력 복사는 O(S)이므로 총 시간은 O(Q+K+S), 최악 O(K+QK)입니다. 덱만 보면 O(K) 공간이지만 입력 JSON과 결과까지 보관하는 전체 프로그램은 O(K+Q+S+C) 공간입니다. C는 입력 문자열 총길이이며 최악의 스냅숏 원소 수는 QK입니다.
직접 확인할 경계
풀이 직접 실행
본문 풀이에 예제 입력을 넣고, 입력을 바꿔 계산 결과를 비교합니다.
예제 크게 보기 (새 탭)capacity=1에서 연속 add, 첫 이벤트 undo, events=[], 같은 값 두 번, null 저장, 물리 배열 끝을 여러 번 지나는 이벤트를 확인하세요. 최종 창만 맞는지 비교하지 말고 모든 중간 스냅숏을 배열 기반 기준 구현과 비교해야 undo 이전의 출력 복사 오류를 찾을 수 있습니다.
함께 연습하고 확인할 자료
Design Circular Deque: 양끝 삽입·삭제 API를 직접 설계하는 외부 연습입니다. 반환 계약은 이 글과 다르므로 공식 문제의 요구를 별도로 읽으세요.
관련 개념 참고: 위 공식 문제의 API 계약을 함께 살펴보세요. 공식 자료와 문제 페이지는 2026년 9월 10일 확인했습니다. 외부 문제의 지문·예시를 복제하지 않고 이 글의 예제와 창작 문제를 별도로 구성했습니다.
이 문제를 푼 뒤 확인하기
원형 버퍼가 끝에서 처음으로 돌아오는 순간과 되돌릴 기록이 없는 경우를 확인해 보세요.
본문의 예제와 이 반례를 직접 실행해 예상 출력과 비교하고, 결과가 다르면 입력 조건과 반복 범위를 다시 확인하세요.
글 소개·학습 준비·전체 목차

핵심 요약
고정 크기의 최근 기록 창을 유지하면서 가장 최근 기록을 되돌립니다. 덱의 삽입 실패와 기록 퇴출을 구분하고 매 이벤트 뒤의 논리 순서를 반환하는 독립 창작 문제입니다.
이 글이 도움이 되었나요?
이어서 공부하기
체크포인트: 해설을 가리고 다시 풀고, 경계 입력을 하나 바꾸어 예상 결과와 실제 결과를 비교하세요. 삽입·제거 전후의 상태를 그리고, 내장 도구를 쓰는 풀이와 직접 구현의 목적을 구분하세요.
다음 추천 · 알고리즘 개념: 버블 정렬: 이웃 비교로 가장 큰 값을 뒤에 확정하기
권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.
원래 과정 목록 순서 (선택)
읽음 기록
읽음 — / 전체 58개 · 필수 58개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.