코딩테스트 해시 풀이: 모든 아나그램 찾기

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

읽기 전에 알아두면 좋아요

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

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

과정 위치와 선택 복습 안내

과정 위치: 구간과 탐색 · 문제

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

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

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

두 문자열이 주어지는 아나그램 찾기 문제는 어떤 연속 구간의 개수를 구하나요?

두 문자열을 입력받아 기준 문자열과 문자 구성이 같은 연속 구간의 개수를 구하는 문제입니다. 문자열·빈도·연속 구간의 기본을 준비하고, 아래 입력·반환 계약을 확인해 직접 풀어보세요.

적용 조건·범위: S와 T의 길이를 N과 M, 두 빈도표에 저장되는 문자 종류 수의 상한을 U라 하면 두 Map의 추가 공간은 O(U)입니다. 각 창을 별도 문자열이나 배열로 저장하지 않고, 답은 개수 숫자 하나라 O(1) 출력 공간입니다. 이미 전달된 S·T 문자열의 저장까지 포함하면 전체 공간은 O(N+M+U)입니다. 문자 종류가 작은 고정 범위라는 조건에서만 빈도표를 O(1) 공간으로 단순화할 수 있습니다.

공식 문서 참고: 관련 개념: MDN Map의 조회·갱신·삭제

문제

코딩테스트 JS 해시 풀이: 모든 아나그램 찾기 전반부 흐름 정리

S문자열에서 T문자열과 아나그램이 되는 S의 부분문자열의 개수를 구하는 프로그램을 작성하는 문제입니다.
아나그램 판별 시 대소문자는 서로 다른 문자로 구분되며, 부분문자열은 반드시 연속된 문자열이어야 합니다.

첫 번째 줄에는 문자열 S가 주어지고, 두 번째 줄에는 문자열 T가 주어집니다.
S문자열의 길이는 최대 10,000을 넘지 않으며, T문자열의 길이는 S문자열의 길이보다 작거나 같습니다.

출력은 S단어 안에서 T문자열과 아나그램 관계가 되는 부분문자열의 총 개수를 출력해야 합니다.

예를 들어 S = “bacaAacba”, T = “abc”가 주어졌을 경우,
{bac}, {acb}, {cba} 총 3개의 부분문자열이 아나그램에 해당하므로 정답은 3이 됩니다.

원자료와 확인 범위

2026-10-12에 제공된 학습 자료 섹션 5. 효율성(투포인터 알고리즘, 슬라이딩윈도우, 해쉬).pdf의 8쪽 ‘모든 아나그램 찾기(해쉬, 투포인터, 슬라이딩 윈도우)’와 문제 조건·예제를 대조했습니다. 같은 ZIP의 해당 문제 TXT는 풀이 코드 자료입니다. 이 자료의 최초 출제 원문·저자·공개 배포 주소까지 확인한 것은 아닙니다.

이번 대조는 원자료의 문제 조건과 예제 확인입니다. 이 글의 전체 코드를 새로 실행하거나 공식 채점에 제출한 결과를 뜻하지 않습니다. 아래 예제·자가 확인 항목은 직접 실행해 기대 출력과 비교하는 기준입니다.

직접 풀어보기

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

0 / 6,000바이트

solution(s, t)에 s(문자열), t(문자열)를 전달하고 정수를 반환합니다. 아래 문제의 유효 입력 조건을 따릅니다. 표준 입력과 출력 코드는 작성하지 않습니다. Tab은 다음 항목으로 이동합니다. 입력 영역 오른쪽 아래를 끌어 높이를 조절할 수 있습니다.

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

해답·해설 보기
JavaScript 해답
function solution(s, t) {
    const a = Array.from(s), b = Array.from(t);
    if (b.length === 0 || b.length > a.length) return 0;
    const need = new Map(), window = new Map();
    for (const ch of b) need.set(ch,(need.get(ch) || 0) + 1);
    let count = 0;
    for (let i = 0; i < a.length; i++) {
        window.set(a[i],(window.get(a[i]) || 0) + 1);
        if (i >= b.length) {
            const old = a[i - b.length], value = window.get(old) - 1;
            if (value === 0) window.delete(old);
            else window.set(old, value);
        }
        if (i < b.length - 1 || window.size !== need.size) continue;
        let same = true;
        for (const[ch, value] of need) if (window.get(ch) !== value) {
            same = false;
            break;
        }
        if (same) count++;
    }
    return count;
}

solution(s, t)에 s(문자열), t(문자열)를 전달하고 정수를 반환합니다. 아래 문제의 유효 입력 조건을 따릅니다. 표준 입력과 출력 코드는 작성하지 않습니다.

해설

코딩테스트 JS 해시 풀이: 모든 아나그램 찾기 후반부 흐름 정리

처음 이 문제를 접했을 때, 직접적인 풀이를 떠올리지 못하고 접근 방법에서 막혔습니다.
문자열을 하나하나 잘라 비교하는 방식은 떠올릴 수 있었지만, 시간 복잡도 측면에서 적절한지 판단하기 어려웠습니다.

만약 S의 모든 부분 문자열을 생성해 정렬 후 비교한다면, 문자열 길이가 커질수록 연산량이 급격히 증가합니다.
따라서 이 문제는 단순 구현 문제가 아니라, 자료구조와 알고리즘 선택이 핵심인 문제입니다.

이 지점에서 해시(Map)를 사용해 문자 빈도를 관리하고, 슬라이딩 윈도우를 통해 연속된 구간을 효율적으로 이동시키는 접근이 필요해집니다.

풀이

먼저 문제의 전체 해답 코드를 한 번에 확인한 뒤, 아래에서 단계별 코드와 함께 흐름을 나누어 설명합니다.
한 줄씩 따라가며 이해할 수 있도록 실제 코딩테스트 풀이 순서에 맞게 구성했습니다.

function compareMaps(map1, map2) {
  if (map1.size !== map2.size) return false;
  for (let [key, val] of map1) {
    if (!map2.has(key) || map2.get(key) !== val) return false;
  }
  return true;
}

function solution(s, t) {
  let answer = 0;
  let tH = new Map();
  let sH = new Map();

  for (let x of t) {
    tH.set(x, (tH.get(x) || 0) + 1);
  }

  let len = t.length - 1;
  for (let i = 0; i < len; i++) {
    sH.set(s[i], (sH.get(s[i]) || 0) + 1);
  }

  let lt = 0;
  for (let rt = len; rt < s.length; rt++) {
    sH.set(s[rt], (sH.get(s[rt]) || 0) + 1);

    if (compareMaps(sH, tH)) answer++;

    sH.set(s[lt], sH.get(s[lt]) - 1);
    if (sH.get(s[lt]) === 0) sH.delete(s[lt]);
    lt++;
  }

  return answer;
}

1단계는 기준이 되는 문자열 T의 문자 빈도를 해시(Map)로 만드는 과정입니다.
이후 모든 비교는 이 해시를 기준으로 이루어집니다.

let tH = new Map();
for (let x of t) {
  tH.set(x, (tH.get(x) || 0) + 1);
}

아나그램 판별은 문자 종류와 개수가 완전히 같아야 하므로,
문자열 자체가 아니라 빈도 구조를 저장하는 것이 핵심입니다.

2단계는 슬라이딩 윈도우를 시작하기 전, S 문자열에서 미리 윈도우 크기만큼 해시를 구성하는 과정입니다.
윈도우 크기는 항상 T의 길이와 같아야 합니다.

let sH = new Map();
let len = t.length - 1;
for (let i = 0; i < len; i++) {
  sH.set(s[i], (sH.get(s[i]) || 0) + 1);
}

이렇게 하면 이후 오른쪽 포인터가 이동할 때마다,
정확히 길이가 T인 구간을 유지할 수 있습니다.

3단계부터 실제 슬라이딩 윈도우가 시작됩니다.
오른쪽 포인터를 이동시키며 새로운 문자를 해시에 추가합니다.

let lt = 0;
for (let rt = len; rt < s.length; rt++) {
  sH.set(s[rt], (sH.get(s[rt]) || 0) + 1);

  if (compareMaps(sH, tH)) answer++;

  sH.set(s[lt], sH.get(s[lt]) - 1);
  if (sH.get(s[lt]) === 0) sH.delete(s[lt]);
  lt++;
}

이 구간에서 가장 중요한 포인트는 두 가지입니다.
첫째, 해시 비교는 항상 윈도우 크기가 동일할 때만 수행됩니다.
둘째, 왼쪽 포인터에서 빠지는 문자는 반드시 정리해 주어야 합니다.

특히 빈도가 0이 된 문자를 삭제하지 않으면,
해시 크기 비교에서 오답이 발생할 수 있으므로 반드시 처리해야 합니다.

빈도표와 입력·출력 공간

S와 T의 길이를 N과 M, 두 빈도표에 저장되는 문자 종류 수의 상한을 U라 하면 두 Map의 추가 공간은 O(U)입니다. 각 창을 별도 문자열이나 배열로 저장하지 않고, 답은 개수 숫자 하나라 O(1) 출력 공간입니다. 이미 전달된 S·T 문자열의 저장까지 포함하면 전체 공간은 O(N+M+U)입니다. 문자 종류가 작은 고정 범위라는 조건에서만 빈도표를 O(1) 공간으로 단순화할 수 있습니다.

정리

이 문제는 아나그램이라는 개념보다, 연속된 부분 문자열을 어떻게 효율적으로 검사할 것인지가 핵심입니다.
해시와 슬라이딩 윈도우를 함께 사용하면 시간 복잡도를 크게 줄일 수 있으며,
문자열 관련 문제에서 매우 자주 활용되는 패턴입니다.

해시 빈도 집계 풀이를 더 짧게 점검하려면 JavaScript 해시 Map 학급 회장를 먼저 확인해보세요.

슬라이딩 윈도우 풀이 흐름을 더 연습하려면 JavaScript 최대 매출 슬라이딩 윈도우처럼 고정 길이 구간을 다루는 문제와 함께 보면 좋습니다.

QnA

Q. 왜 문자열을 정렬해서 비교하면 안 되나요?
정렬 방식은 구현은 쉽지만, 매번 정렬이 발생해 시간 복잡도가 크게 증가합니다. 이 문제의 입력 크기에서는 비효율적입니다.

Q. Map 대신 객체를 사용해도 되나요?
가능하지만, 키의 개수 관리와 비교 측면에서는 Map이 더 명확하고 안전합니다.

Q. 이 패턴은 어디에 또 사용되나요?
부분 문자열, 연속 구간 합, 빈도 비교 문제 등에서 거의 동일한 구조로 반복 등장합니다.

Q. 면접에서 이 문제를 설명하라고 하면 어떻게 말해야 하나요?
해시로 문자 빈도를 관리하고, 슬라이딩 윈도우로 연속 구간을 유지하며 비교했다고 설명하면 됩니다.

Q. 시간 복잡도는 어떻게 되나요?
S 길이를 N, T 길이를 M, 비교하는 문자 종류 수 상한을 U라 하면 평균 상수 시간 Map 접근 모델에서 O(M + N×U)입니다. U가 고정된 작은 문자집합일 때만 O(N+M)로 단순화할 수 있습니다.

Q. 대소문자 조건은 왜 중요한가요?
문자 자체를 키로 사용하기 때문에 대소문자 구분 여부가 결과에 직접적인 영향을 줍니다.

과정 마무리 실습

이미 푼 문제 한 개를 해설 없이 다시 풀고 경계 입력 세 개를 추가하세요.

완료 기준: 예상 출력과 실제 출력을 비교하고 시간 복잡도 및 다른 풀이의 장단점을 설명합니다.

이어서 공부할 과정: 자료구조 첫 글

전체 학습 경로

이 문제를 푼 뒤 확인하기

길이가 같은 서로 다른 배열의 문자 빈도와 중복 문자가 들어간 창의 빈도 변화를 비교해 보세요.

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

풀이 함수를 독립 실행해 확인합니다

선수 지식은 배열·조건문·반복문과 위에서 설명한 자료구조입니다. 풀이의 완성 함수 블록 하나를 solution.cjs로 저장하고, 그 아래에 다음 호출을 붙입니다. 앞의 비교용 풀이와 같은 이름의 함수를 함께 붙이지 않습니다. 터미널에서 node solution.cjs로 실행합니다.

console.log(JSON.stringify(solution(...["abcd", "aa"])));

이 변형 호출의 기대 JSON 출력은 0입니다. 기존 함수 블록의 console.log가 있으면 그 출력 다음 줄에서 확인합니다.

완성 블록의 compareMaps와 solution을 함께 저장해야 합니다. 여기서는 비어 있지 않은 영문 문자열을 실습합니다. 현재 코드의 T 순회와 S 인덱스 접근은 이모지 등 서로게이트 쌍에 대해 같은 문자 단위를 사용하지 않으므로 일반 유니코드 문자열용 구현으로 간주하지 않습니다.

글 소개·학습 준비·전체 목차

주요 포인트 한눈에 보기

T의 문자 빈도를 Map으로 저장하고, S에서 길이가 T와 같은 연속 구간의 빈도와 비교합니다. 창을 한 칸 옮길 때 새 문자를 더하고 빠진 문자를 빼며 빈도가 0인 키는 삭제합니다. 두 빈도표가 같은 창만 세어 아나그램 개수를 구합니다.

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 해설을 가리고 다시 풀고, 경계 입력을 하나 바꾸어 예상 결과와 실제 결과를 비교하세요. 포인터를 움직여도 답을 버리지 않는 이유와 양수·0·음수의 차이를 설명하세요.

다음 추천 · 알고리즘 개념: 이진 탐색의 불변식: 탐색 구간·중간값·종료 조건

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

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

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

읽음 기록

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

이 글을 쓴 해비

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

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기