fos-blog/study
01 / 홈02 / 카테고리
01 / 홈02 / 카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • langgraph 페이지로 이동
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
  • ai 페이지로 이동
    • agent 페이지로 이동
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] CJ푸드빌 커머스/F&B 도메인 설계 면접 대비 — 슬롯 경험을 주문·결제·쿠폰·매장 상태 설계로 번역하기
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 주문 상태와 데이터 정합성 기본기 — CJ푸드빌 면접 대비
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리 — CJ푸드빌 디지털 채널 백엔드 면접 대비
    • [초안] F&B 주문/매장/픽업 상태머신 설계 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 1,600만 고객과 올영세일을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
  • finance 페이지로 이동
    • industry-cycle 페이지로 이동
    • investing 페이지로 이동
    • stock-notes 페이지로 이동
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • [초안] CJ푸드빌 디지털 채널 Back-end 개발자 직무 분석
    • [초안] CJ푸드빌 디지털 채널 Back-end 면접 답변집 — 슬롯 도메인 경험을 커머스/F&B 설계로 번역하기
    • [초안] F&B / e-Commerce 운영 모니터링과 장애 대응 인터뷰 정리
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/algorithm/[초안] Heap과 PriorityQueue…
algorithm

[초안] Heap과 PriorityQueue로 뚫는 라이브 코딩 Top-K 패턴 (Java)

라이브 코딩에서 "정렬하면 되지 않나요?"라고 먼저 답하는 순간, 면접관의 다음 질문은 거의 정해져 있다. "배열이 엄청 크고 k가 작으면요?" 이 한 마디에 무너지지 않으려면 Heap과 PriorityQueue를 손에 익혀둬야 한다. 이 문서는 HackerRank 스타일의 라이브 면접에서 Heap 계열 문제를 만났을 때, 접근 방식을 말로 설명하고 Jav...

2026.04.21·11 min read·26 views

라이브 코딩에서 "정렬하면 되지 않나요?"라고 먼저 답하는 순간, 면접관의 다음 질문은 거의 정해져 있다. "배열이 엄청 크고 k가 작으면요?" 이 한 마디에 무너지지 않으려면 Heap과 PriorityQueue를 손에 익혀둬야 한다. 이 문서는 HackerRank 스타일의 라이브 면접에서 Heap 계열 문제를 만났을 때, 접근 방식을 말로 설명하고 Java 코드로 구현하고 엣지 케이스까지 커버하는 전 과정을 한 번에 훈련하기 위한 스터디 팩이다.

왜 이 주제가 중요한가

시니어 백엔드 라이브 코딩에서 Heap/PriorityQueue는 정렬·이분탐색·해시맵 다음으로 가장 자주 나오는 자료구조다. 특히 다음 세 가지 상황에서 반드시 손이 먼저 나가야 한다.

  • Top-K / Bottom-K: "가장 큰 k개", "가장 자주 등장한 k개 단어", "가장 가까운 k개 좌표"
  • 스트리밍 / 온라인 처리: 전체를 다 볼 수 없거나 다 볼 필요가 없는 경우의 실시간 중앙값, 실시간 Top-K
  • k-way merge: 여러 정렬된 스트림을 합치는 경우. Kafka 파티션별 정렬 로그 병합, 여러 DB 샤드에서 가져온 정렬된 결과 머지 등 실무에서 백엔드가 자주 마주친다.

실무 관점에서도 의미가 크다. Top-K는 "전체를 ORDER BY 해서 LIMIT k 할 것인가, 아니면 스트리밍 집계에서 k 크기 힙으로 유지할 것인가"라는 설계 선택으로 그대로 이어진다. 면접관은 알고리즘 문제를 풀면서도 이런 설계 감각을 가진 지원자를 찾는다.

핵심 개념 정리

Heap이란 무엇인가

Heap은 부모-자식 관계에 대한 순서 조건만 만족시키는 완전 이진 트리다. 전체 정렬이 아니라 "루트가 항상 최솟값(또는 최댓값)"이라는 약한 조건만 보장한다. 그래서 정렬보다 저렴하다.

  • 삽입: O(log n)
  • 루트 조회(peek): O(1)
  • 루트 제거(poll): O(log n)
  • 임의 원소 탐색: O(n) — 이 점을 잊어서 contains 남발하면 코드가 느려진다.

왜 정렬 대신 Heap을 쓰는가

라이브 코딩에서 "정렬 O(n log n) vs 힙 O(n log k)"를 바로 꺼낼 수 있어야 한다.

  • n = 1,000,000, k = 10 → 정렬 ~2천만 연산, 힙 ~330만 연산. 메모리도 n이 아니라 k만 쓰면 된다.
  • 데이터가 스트리밍이면 정렬 자체가 불가능하다. 힙은 한 원소씩 흘려 넣으며 유지할 수 있다.
  • "최댓값 하나만 필요하다"면 힙도 과하다. 그냥 선형 스캔 O(n)이 더 빠르고 단순하다. 즉 k가 1이면 힙을 쓰지 말라는 것도 기억해두면 면접에서 신중함을 보여줄 수 있다.

기준 정리:

상황선택
k = 1, 전체 1회 스캔선형 스캔
k ≪ n, 한 번만크기 k 힙
전체 정렬 결과가 필요정렬
스트리밍, 계속 갱신힙 (또는 두 개 힙)
k-way merge각 스트림의 head를 담는 힙

min heap과 max heap 구분

Java PriorityQueue는 기본이 min heap이다. 즉 peek()은 최솟값이다. 이 점이 Top-K 문제에서 가장 흔한 혼동 포인트다.

  • "가장 큰 k개를 구하라" → min heap을 쓴다. 힙 크기를 k로 유지하면서, 들어오는 값이 힙 루트(현재 k개 중 최소)보다 크면 루트를 제거하고 새 값을 넣는다. 끝나면 힙에 남은 k개가 답이다.
  • "가장 작은 k개를 구하라" → max heap을 쓴다. 반대로 동작한다.
  • 실수 포인트: "가장 큰 k개니까 max heap이 당연하지" 하고 전부 집어넣으면 공간이 O(n)이 된다. Top-K의 핵심은 반대 방향 힙을 크기 k로 유지하는 것이다.

Comparator의 구조

Java에서 힙을 제어하는 건 99% comparator다. 시그니처는 int compare(a, b)이고 규칙은 단순하다.

  • 음수를 반환하면 a가 먼저(=우선순위 높음=루트에 가까움)
  • 양수를 반환하면 b가 먼저
  • 0이면 동등

즉 "먼저 나와야 하는 걸 '작게' 만들면 된다". min heap으로 동작하기 때문이다.

java
// 값이 큰 것부터 나오게(=max heap)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
 
// 빈도 높은 단어 먼저, 동률이면 사전순 먼저
PriorityQueue<Map.Entry<String, Integer>> pq = new PriorityQueue<>((a, b) -> {
    if (!a.getValue().equals(b.getValue())) {
        return Integer.compare(b.getValue(), a.getValue()); // freq desc
    }
    return a.getKey().compareTo(b.getKey()); // lex asc
});

실무 백엔드 관점에서의 PriorityQueue

알고리즘 문제 너머의 쓰임도 같이 떠올릴 수 있어야 면접에서 깊이가 생긴다.

  • 스케줄러 / 작업 큐: 실행 시각이 가장 빠른 작업을 먼저 꺼내는 타이머 큐. DelayQueue, ScheduledThreadPoolExecutor 내부가 전부 힙 기반이다.
  • Dijkstra / A*: 가중 그래프 최단 경로에서 현재까지의 최소 거리 노드를 꺼내는 데 힙을 쓴다. 라우팅, 물류, 게임 네비게이션에서 그대로 쓰이는 구조다.
  • 외부 정렬(External Sort): 메모리에 다 못 올리는 큰 파일을 정렬할 때 k-way merge 단계에서 힙을 쓴다. 실무에서 로그/이벤트 배치 처리할 때 본 경험을 묶어서 이야기하면 좋다.
  • Rate limiter / 만료 처리: "가장 먼저 만료될 토큰"을 O(log n)으로 꺼내야 할 때.

이런 사례를 미리 하나씩 붙여두면 "왜 힙을 썼나요?" 질문이 왔을 때 "Top-K 유지 + O(log n) 갱신 비용 + 루트만 보면 되는 구조" 같은 답을 자연스럽게 내놓을 수 있다.

흔한 버그 패턴

라이브에서 떨어뜨리는 실수들은 거의 이 목록 안에서 나온다.

  1. 기본이 min heap이라는 걸 잊는다. "가장 큰 값부터"를 원했는데 peek()이 최솟값이라 로직이 완전히 뒤집힌다.
  2. 정수 뺄셈 comparator. (a, b) -> a - b는 a나 b가 음수일 때 오버플로로 부호가 뒤집힐 수 있다. 항상 Integer.compare(a, b)를 쓴다.
  3. k번째와 k개를 혼동한다. "k번째로 큰 값"은 힙 크기 k짜리 min heap의 peek()이고, "가장 큰 k개"는 그 힙 전체다.
  4. size 체크 순서. offer → if (pq.size() > k) pq.poll(); 흐름은 명확하지만, "힙이 비었으면 무조건 넣고, 아니면 루트와 비교"로 쓰면 경계 케이스를 놓치기 쉽다.
  5. tie-breaker 정의 누락. 빈도가 같으면 어떻게 정렬할지 명시하지 않으면 테스트가 흔들린다. 문제 조건을 다시 읽고 tie-breaker를 반드시 comparator에 넣는다.
  6. 불변 값을 바꾼다. 힙에 들어간 객체의 비교 기준 필드를 외부에서 바꾸면 힙 불변식이 깨진다. PriorityQueue는 내부 재정렬을 해주지 않는다.
  7. remove(Object) 남용. O(n) 탐색이다. "임의 원소 제거"가 자주 필요하면 힙이 아니라 TreeSet 또는 "lazy deletion + 두 번째 힙" 패턴을 고려해야 한다.
  8. Iterator 순서를 정렬 순서로 착각. for (X x : pq)는 순서 보장이 없다. 정렬 순서로 보려면 poll()을 반복해야 한다.

라이브 면접에서 접근 방식을 설명하는 법

HackerRank 스타일은 말하면서 코딩이 기본이다. 다음 6단계 템플릿을 외워두면, 문제를 받자마자 흐름을 선점할 수 있다.

  1. 문제 재진술: "정리하면 … 이 조건이 핵심이고, 입력 크기는 … 범위군요."
  2. 브루트포스부터 선언: "가장 단순한 방법은 전부 정렬 후 앞에서 k개 가져오는 O(n log n)입니다."
  3. 제약을 건드려 업그레이드: "n은 크고 k는 작다고 하셨으니 O(n log k)로 줄일 수 있는 힙 접근이 맞아 보입니다."
  4. 자료구조 결정을 말로 확정: "가장 큰 k개니까 크기 k의 min heap을 유지하겠습니다. 루트가 현재 k개 중 최솟값이라, 새로 들어오는 값이 루트보다 크면 교체합니다."
  5. 엣지 케이스 먼저 수집: "k가 0이거나 배열 길이보다 크면? 중복 값이 있으면? 음수는? null 입력 가능성은?"
  6. 구현 후 직접 드라이런: 작은 입력 2~3개로 루프를 말로 돌린다. 시간·공간 복잡도를 다시 한 번 말한다.

이 흐름은 "코드를 얼마나 빨리 쳤는가"보다 "어떤 근거로 자료구조를 선택했는가"를 보여주기 때문에 시니어 포지션에서 특히 유리하다.

로컬 연습 환경

  • JDK 17 이상 (PriorityQueue 자체는 오래된 API지만, record·switch 패턴을 섞어 쓰면 편하다.)
  • 빌드 도구 없이 javac, java 한 파일로 돌리는 게 가장 빠르다. 라이브 환경을 가정하고 IDE 자동완성 없이 풀어보는 훈련도 최소 일주일에 두 번 한다.

실행 루틴 예시:

bash
mkdir -p ~/coding-live/heap && cd ~/coding-live/heap
# 문제 1개당 파일 하나. main에 테스트 케이스를 직접 박아서 실행.
javac TopKFrequent.java && java TopKFrequent

연습 규칙:

  • 타이머 25분을 맞춘다. 라이브 면접 한 문제 분량을 가정.
  • 풀이 중 웹 검색 금지. PriorityQueue API가 기억나지 않으면 기억나는 만큼만 쓰고 나중에 확인.
  • 끝난 뒤엔 "내 설명이 그대로 녹음돼도 면접관이 납득했을까?"를 기준으로 회고.

연습 문제 2개

두 문제 모두 풀이와 Java 전체 코드는 <details> 블록에 숨겨두었다. 먼저 25분 타이머로 직접 풀고, 그다음 펼쳐서 비교한다.

문제 1 (쉬움) — Kth Largest Element

정수 배열 nums와 정수 k가 주어질 때, k번째로 큰 값을 반환하라.

  • 제약: 1 <= k <= nums.length <= 10^5, -10^4 <= nums[i] <= 10^4.
  • 예시
    • nums = [3,2,1,5,6,4], k = 2 → 5
    • nums = [3,2,3,1,2,4,5,5,6], k = 4 → 4
  • 면접관이 이어서 물어볼 수 있는 것: "n이 10^9이고 k가 10이면 어떻게 바꿀 건가요?" → 크기 k min heap 전략을 그대로 유지할 수 있다고 답한다.
풀이와 Java 전체 코드 보기

접근: 전체 정렬은 O(n log n). 대신 크기 k짜리 min heap을 유지한다. 배열을 훑으면서 힙에 값을 넣고, 크기가 k를 넘으면 루트(=현재 k+1개 중 최솟값)를 버린다. 마지막에 힙의 루트가 k번째로 큰 값이다. 복잡도 O(n log k), 공간 O(k).

드라이런 (nums = [3,2,1,5,6,4], k = 2):

  • 3 넣고 heap=[3]
  • 2 넣고 heap=[2,3]
  • 1 넣고 size>2 → 1 버림, heap=[2,3]
  • 5 넣고 size>2 → 2 버림, heap=[3,5]
  • 6 넣고 size>2 → 3 버림, heap=[5,6]
  • 4 넣고 size>2 → 4 버림, heap=[5,6]
  • 루트 = 5 ✅

자주 하는 실수: max heap에 전부 넣고 k번 poll()하는 것. 답은 맞지만 O(n log n)이라 "정렬이랑 뭐가 다르죠?" 질문에 흔들린다.

java
import java.util.PriorityQueue;
 
public class KthLargest {
 
    public static int findKthLargest(int[] nums, int k) {
        if (nums == null || k <= 0 || k > nums.length) {
            throw new IllegalArgumentException("invalid input");
        }
        PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);
        for (int num : nums) {
            if (minHeap.size() < k) {
                minHeap.offer(num);
            } else if (num > minHeap.peek()) {
                minHeap.poll();
                minHeap.offer(num);
            }
        }
        return minHeap.peek();
    }
 
    public static void main(String[] args) {
        System.out.println(findKthLargest(new int[]{3, 2, 1, 5, 6, 4}, 2)); // 5
        System.out.println(findKthLargest(new int[]{3, 2, 3, 1, 2, 4, 5, 5, 6}, 4)); // 4
        System.out.println(findKthLargest(new int[]{-1, -2, -3, -4}, 2)); // -2
    }
}

문제 2 (중간) — Top K Frequent Words

문자열 배열 words와 정수 k가 주어질 때, 가장 자주 등장한 단어 k개를 빈도 내림차순으로 반환하라. 빈도가 같으면 사전순 오름차순으로 정렬한다.

  • 제약: 1 <= words.length <= 5 * 10^4, 1 <= k <= 고유 단어 수.
  • 예시
    • words = ["i","love","leetcode","i","love","coding"], k = 2 → ["i","love"]
    • words = ["the","day","is","sunny","the","the","the","sunny","is","is"], k = 4 → ["the","is","sunny","day"]
  • 이 문제에서 면접관이 보고 싶은 것: tie-breaker comparator, 크기 k 유지 전략, 결과를 어떻게 원하는 순서로 뽑는지.
풀이와 Java 전체 코드 보기

접근:

  1. HashMap으로 빈도를 센다. O(n).
  2. 크기 k짜리 "반대 방향" 힙을 만든다. 최종 정렬 기준은 "빈도 desc, 단어 asc"지만, 힙에서는 peek()이 가장 약한 후보(=빈도 작고, 빈도 같으면 단어 큰 쪽)가 되어야 넘치는 순간 루트만 버리면 된다. 즉 comparator가 뒤집힌다.
  3. 힙에 전부 넣고 넘칠 때마다 루트를 버린다. 마지막엔 k개가 남는다.
  4. 결과는 poll()을 반복해 거꾸로 쌓는다. 그래야 빈도 desc / 단어 asc 순서가 된다.

복잡도 O(n log k), 공간 O(n + k).

자주 하는 실수:

  • 힙 comparator를 "빈도 desc, 단어 asc"로 그대로 넣는 것. 그러면 넘쳤을 때 버려야 할 것은 가장 뒤에 있는데, poll()은 앞을 버린다. 답이 완전히 반대가 된다.
  • tie-breaker를 Integer.compare로만 쓰고 문자열 비교를 빼먹는 것.
  • 최종 결과를 poll 순서대로 넣어서 정렬이 거꾸로 나오는 것. Collections.reverse 또는 add(0, x)로 맞춘다.
java
import java.util.*;
 
public class TopKFrequentWords {
 
    public static List<String> topKFrequent(String[] words, int k) {
        Map<String, Integer> freq = new HashMap<>();
        for (String w : words) {
            freq.merge(w, 1, Integer::sum);
        }
 
        // 힙 기준: "약한 것이 위로". 빈도 오름차, 빈도 같으면 단어 내림차.
        PriorityQueue<Map.Entry<String, Integer>> heap = new PriorityQueue<>((a, b) -> {
            int freqCmp = Integer.compare(a.getValue(), b.getValue());
            if (freqCmp != 0) {
                return freqCmp;
            }
            return b.getKey().compareTo(a.getKey());
        });
 
        for (Map.Entry<String, Integer> e : freq.entrySet()) {
            heap.offer(e);
            if (heap.size() > k) {
                heap.poll();
            }
        }
 
        List<String> result = new ArrayList<>(k);
        while (!heap.isEmpty()) {
            result.add(heap.poll().getKey());
        }
        Collections.reverse(result); // 빈도 desc, 단어 asc 순으로 정렬
        return result;
    }
 
    public static void main(String[] args) {
        System.out.println(topKFrequent(
                new String[]{"i", "love", "leetcode", "i", "love", "coding"}, 2));
        // [i, love]
 
        System.out.println(topKFrequent(
                new String[]{"the", "day", "is", "sunny", "the", "the", "the",
                        "sunny", "is", "is"}, 4));
        // [the, is, sunny, day]
    }
}

드라이런 포인트 (k = 2, 입력 첫 예시):

  • 빈도: {i:2, love:2, leetcode:1, coding:1}
  • 힙에 넣는 동안 루트(가장 약한 후보)가 바뀌면서, 최종적으로 i, love만 남는다.
  • poll() 순서는 love → i (약한 순), reverse 후 [i, love].

면접 답변 프레이밍

"Heap을 써본 경험이 있나요?"라는 질문은 자료구조 지식을 확인하려는 게 아니라, 언제 쓸지를 판단할 수 있는가를 보려는 것이다. 답변 구조는 다음을 따른다.

  1. 정의: "Heap은 부모-자식 간 순서만 보장하는 완전 이진 트리이고, Java에선 PriorityQueue로 씁니다. 기본이 min heap이라 Top-K 문제에선 반대 방향 힙을 쓰는 게 포인트입니다."
  2. 언제 쓰는가: "전체 정렬이 아니라 상위 몇 개만 필요한 경우, 또는 스트리밍처럼 전체를 한 번에 볼 수 없는 경우에 씁니다."
  3. 실무 예시 묶기: "최근에 … 요청에서 상위 N개 사용자만 뽑아야 했는데, 전체 정렬 대신 크기 N 힙으로 유지해서 메모리와 시간을 모두 줄였습니다." (실제 경험이 없다면 "스케줄러에서 가장 빠른 실행 시각의 작업을 꺼내는 구조도 힙 기반이라 개념이 동일합니다"처럼 구조적으로 설명한다.)
  4. 한계: "임의 원소 제거나 검색은 O(n)이라, 그 기능이 필요하면 TreeSet 또는 해시맵 + 힙 조합을 검토합니다."
  5. 확장: "실시간 중앙값 같은 문제는 min heap과 max heap 두 개를 균형 맞춰 운영하는 패턴이 있습니다."

체크리스트

  • PriorityQueue 기본이 min heap이라는 것을 1초 안에 답할 수 있다.
  • Top-K "가장 큰 k개"를 묻는 순간 반사적으로 "크기 k의 min heap"이라고 말한다.
  • comparator에서 a - b 대신 Integer.compare를 쓴다.
  • tie-breaker를 comparator 한 블록 안에서 분기 처리할 수 있다.
  • 힙 크기 유지 로직(offer 후 if size > k poll)을 외워서 쓴다.
  • 정렬 O(n log n) vs 힙 O(n log k)을 숫자로 설명할 수 있다.
  • PriorityQueue의 순회 순서가 정렬 순서가 아니라는 것을 안다.
  • 힙에 들어간 원소의 비교 키를 외부에서 변경하면 안 된다는 걸 안다.
  • remove(Object)가 O(n)이라는 것을 알고, 잦은 임의 삭제면 다른 자료구조를 고려한다.
  • 실시간 중앙값, Dijkstra, k-way merge 세 가지 실무 패턴을 한 문장으로 설명할 수 있다.
  • 라이브 코딩에서 문제 재진술 → 브루트포스 → 힙 업그레이드 → 엣지 케이스 → 드라이런 순서로 말하며 푼다.
on this page
  • 01왜 이 주제가 중요한가
  • 02핵심 개념 정리
  • Heap이란 무엇인가
  • 왜 정렬 대신 Heap을 쓰는가
  • min heap과 max heap 구분
  • Comparator의 구조
  • 03실무 백엔드 관점에서의 PriorityQueue
  • 04흔한 버그 패턴
  • 05라이브 면접에서 접근 방식을 설명하는 법
  • 06로컬 연습 환경
  • 07연습 문제 2개
  • 문제 1 (쉬움) — Kth Largest Element
  • 문제 2 (중간) — Top K Frequent Words
  • 08면접 답변 프레이밍
  • 09체크리스트

댓글 (0)