코딩테스트 LRU 캐시: 지도 타일 재사용 기록

··설명 약 4분·작성: 해비·블로그 소개

읽기 전에 알아두면 좋아요

아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.

개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.

과정 위치와 선택 복습 안내

과정 위치: 심화 선택 실습 · 문제

과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.

JavaScript로 문법·실행 복습하기 (선택)

개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

지도 타일 LRU 문제는 캐시 용량과 요청을 받아 어떤 두 결과를 반환하나요?

캐시 용량과 지도 타일 요청을 입력받아 적중 여부와 최종 보관 키를 반환하는 문제입니다. 키 조회·연결 리스트·LRU의 기본을 준비하고, 아래 입력·반환 계약을 확인해 직접 풀어보세요.

적용 조건·범위: 길이가 제한된 키와 해시 Map 기대 모델에서 q개 요청은 기대 상환 O(q), 마지막 keys는 O(c), 보유 구조 O(c+1), 출력 O(q+c)입니다. c는 capacity와 실제 서로 다른 요청 수 중 작은 값입니다. Map 최악 상수 시간은 보장하지 않습니다.

본문 근거: MDN Map: 키 비교와 접근 복잡도 요구

[창작 문제] 지도 타일의 재사용 기록

최근 사용한 타일만 보관하며 각 요청의 캐시 적중 여부와 마지막 보관 순서를 구합니다. 적중한 요청도 최근 사용 시점을 갱신합니다.

LRU 캐시의 개념을 표현한 표지 일러스트
주제를 표현한 개념 표지입니다. 정확한 동작과 값의 변화는 아래 코드와 표에서 확인하세요.

이 문제는 BlogFlow 자료구조 학습용으로 독립 창작했습니다. 외부 문제의 지문이나 테스트를 옮기지 않았습니다.

문제

지도 미리보기는 capacity개 타일 이름을 보관할 수 있습니다. requests의 타일을 순서대로 요청할 때 이미 캐시에 있으면 true, 없으면 false를 적습니다. 누락 타일은 캐시에 넣고, 가득 찼으면 가장 오래 사용하지 않은 타일을 버립니다. capacity가 0이면 모든 요청이 누락이고 아무것도 보관하지 않습니다.

관련 구현 설명: JavaScript LRU 캐시: Map과 이중 연결 리스트의 불변식

입력·출력과 제약

JSON 객체 {capacity,requests}. requests는 타일 이름 문자열의 배열입니다.

JSON 객체 {hits,keys}. hits는 요청별 불리언 배열, keys는 최종 캐시에 남은 타일을 최근 사용 순서부터 나열한 배열입니다.

0 ≤ capacity ≤ 100,000, 요청 수 0~100,000. 타일 이름은 1~40 코드포인트 문자열이며 완전히 같은 문자열일 때 같은 타일로 취급합니다.

입력은 위 계약을 만족한다고 가정합니다. 온라인 채점기는 제공하지 않으며 로컬 Node.js에서 JSON 입력으로 실행합니다.

예시

예시 1 입력:

{"capacity":2,"requests":["북","남","북","동","남","남"]}

예시 1 출력:

{"hits":[false,false,true,false,false,true],"keys":["남","동"]}

예시 2 입력:

{"capacity":0,"requests":["a","a"]}

예시 2 출력:

{"hits":[false,false],"keys":[]}

힌트

hit는 캐시를 갱신하기 전에 판단해야 합니다. hit일 때도 노드를 MRU로 옮겨야 다음 퇴출 대상이 맞습니다.

정답과 해설

직접 풀어보기

먼저 선택한 언어의 입력·반환 계약에 맞춰 작성해 보세요. 힌트 받기와 정답과 비교하기는 코드를 실행하지 않는 AI 분석입니다. 해답·해설은 아래에서 필요할 때 펼칠 수 있습니다.

0 / 6,000바이트

solution(capacity, requests)에 정수와 문자열 배열을 전달하고 {hits: boolean[], keys: string[]}를 반환합니다. 입력은 문제의 유효한 범위를 만족합니다. 표준 입력·출력 없이 결과를 반환하세요. Tab은 다음 항목으로 이동합니다. 입력 영역 오른쪽 아래를 끌어 높이를 조절할 수 있습니다.

비밀키·개인정보는 입력하지 마세요. 코드는 이 페이지에만 유지됩니다.

해답·해설 보기
JavaScript 해답
function solution(capacity, requests) {
    const map = new Map();
    const head = {
        next : null
    }, tail = {
        prev : head
    };
    head.next = tail;
    const detach = node => {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    };
    const front = node => {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    };
    const hits = [];
    for (const key of requests) {
        const node = map.get(key);
        hits.push(node !== undefined);
        if (node) {
            detach(node);
            front(node);
        }
        else if (capacity > 0) {
            if (map.size === capacity) {
                const old = tail.prev;
                detach(old);
                map.delete(old.key);
            }
            const fresh = {
                key
            };
            map.set(key, fresh);
            front(fresh);
        }
    }
    const keys = [];
    for (let node = head.next; node !== tail; node = node.next) keys.push(node.key);
    return {
        hits, keys
    };
}

solution(capacity, requests)에 정수와 문자열 배열을 전달하고 {hits: boolean[], keys: string[]}를 반환합니다. 입력은 문제의 유효한 범위를 만족합니다. 표준 입력·출력 없이 결과를 반환하세요.

Node.js 24 CommonJS용 전체 풀이입니다. lru-problem.cjs로 저장하세요. 클래스 선언, solve 함수, 표준 입력 처리가 모두 들어 있어 가이드 파일을 별도로 가져오지 않습니다. 예시 입력을 input.json에 저장하고 터미널에서 실행합니다.

Windows PowerShell:
Get-Content -Raw -Encoding UTF8 ./input.json | node ./lru-problem.cjs

macOS / Linux:
node lru-problem.cjs < input.json
class LRUCache {
  constructor(capacity) {
    if (!Number.isInteger(capacity) || capacity < 0 || capacity > 1_000_000) {
      throw new RangeError('용량은 0~1,000,000 정수여야 합니다.');
    }
    this.capacity = capacity;
    this.map = new Map();
    this.head = { prev: null, next: null };
    this.tail = { prev: this.head, next: null };
    this.head.next = this.tail;
  }
  detach(node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
    node.prev = null;
    node.next = null;
  }
  attachFront(node) {
    node.prev = this.head;
    node.next = this.head.next;
    this.head.next.prev = node;
    this.head.next = node;
  }
  get(key) {
    const node = this.map.get(key);
    if (!node) return undefined;
    this.detach(node);
    this.attachFront(node);
    return node.value;
  }
  put(key, value) {
    if (value === undefined) throw new TypeError('undefined는 누락 표시로 예약합니다.');
    if (this.capacity === 0) return;
    const old = this.map.get(key);
    if (old) {
      old.value = value;
      this.detach(old);
      this.attachFront(old);
      return;
    }
    if (this.map.size === this.capacity) {
      const victim = this.tail.prev;
      this.detach(victim);
      this.map.delete(victim.key);
    }
    const node = { key, value, prev: null, next: null };
    this.map.set(key, node);
    this.attachFront(node);
  }
  keys() {
    const result = [];
    for (let node = this.head.next; node !== this.tail; node = node.next) result.push(node.key);
    return result;
  }
}

function solve({ capacity, requests }) {
  const cache = new LRUCache(capacity), hits = [];
  for (const key of requests) {
    const hit = cache.get(key) !== undefined;
    hits.push(hit);
    if (!hit) cache.put(key, true);
  }
  return { hits, keys: cache.keys() };
}

module.exports = { solve };

if (require.main === module) {
  const input = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
  console.log(JSON.stringify(solve(input)));
}

북, 남 요청은 모두 누락입니다. 세 번째 북은 적중해 북이 MRU가 됩니다. 동이 들어오면 남이 퇴출됩니다. 다음 남은 누락이라 북을 퇴출하고 마지막 남은 적중입니다. solve는 payload에 true를 저장하므로 get의 undefined와 확실히 구분됩니다. 최종 keys 호출은 전체 요청 처리가 끝난 뒤 한 번만 수행합니다.

head와 tail은 실제 데이터를 담지 않는 센티널입니다. head.next가 MRU, tail.prev가 LRU입니다. 비었을 때 head.next===tail, tail.prev===head입니다. 실제 노드마다 node.prev.next===node, node.next.prev===node가 유지되어야 어느 위치든 같은 두 줄로 제거할 수 있습니다.

Map의 각 값은 리스트에 정확히 한 번 등장하는 같은 노드 객체입니다. 리스트에서만 퇴출하면 Map에 오래된 항목이 남고, Map에서만 지우면 리스트 길이가 용량을 넘습니다. 따라서 퇴출은 detach와 map.delete를 같이 수행합니다.

길이가 제한된 키와 해시 Map 기대 모델에서 q개 요청은 기대 상환 O(q), 마지막 keys는 O(c), 보유 구조 O(c+1), 출력 O(q+c)입니다. c는 capacity와 실제 서로 다른 요청 수 중 작은 값입니다. Map 최악 상수 시간은 보장하지 않습니다.

표본과 경계를 직접 확인하려면 다음 코드를 lru-check.cjs로 저장하고 같은 폴더에서 node lru-check.cjs를 실행하세요. assert가 맞으면 출력 없이 종료하고, 다르면 예외와 함께 실패합니다.

const assert = require('node:assert/strict');
const { solve } = require('./lru-problem.cjs');

assert.deepEqual(
  solve({
  "capacity": 2,
  "requests": [
    "북",
    "남",
    "북",
    "동",
    "남",
    "남"
  ]
}),
  {"hits":[false,false,true,false,false,true],"keys":["남","동"]},
);
assert.deepEqual(
  solve({
  "capacity": 0,
  "requests": [
    "a",
    "a"
  ]
}),
  {"hits":[false,false],"keys":[]},
);

연결 학습과 공식 자료

JavaScript LRU 캐시: Map과 이중 연결 리스트의 불변식에서 연산별 이유와 전체 복잡도를 이어서 확인할 수 있습니다. 다른 형식의 공식 연습은 LeetCode 146 – LRU Cache를 참고하세요. 외부 문제의 정답이나 지문은 이 페이지에 복제하지 않았습니다.

공식 자료 확인일: 2026년 9월 10일. 구현은 이 글의 JavaScript 계약에 맞춰 독립적으로 작성했습니다. 다른 언어 라이브러리의 API와 숫자 범위가 그대로 적용되는 것은 아닙니다.

이 문제를 푼 뒤 확인하기

풀이 직접 실행

본문 풀이에 예제 입력을 넣고, 입력을 바꿔 계산 결과를 비교합니다.

예제 크게 보기 (새 탭)

이미 있던 타일을 다시 사용해 최근 순서가 바뀌는 경우와 용량이 가득 찬 경우를 추적해 보세요.

본문의 예제와 이 반례를 직접 실행해 예상 출력과 비교하고, 결과가 다르면 입력 조건과 반복 범위를 다시 확인하세요.

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 해설을 가리고 다시 풀고, 경계 입력을 하나 바꾸어 예상 결과와 실제 결과를 비교하세요. 표본 하나에 더해 경계 입력과 작은 기준 풀이를 비교하고 왜 이 구조를 고르는지 설명하세요.

다음 추천 · 자료구조 개념: JavaScript AVL 트리: 높이 불변식과 LL·RR·LR·RL 삽입 회전

같은 주제에서 다음 개념과 예제로 이해를 넓힙니다.

권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.

원래 과정 목록 순서 (선택)

읽음 기록

읽음 — / 전체 58개 · 필수 58개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기