주요 포인트 한눈에 보기
문자열 아나그램 문제는 해시(Map)와 슬라이딩 윈도우를 함께 활용하는 대표적인 코딩테스트 유형입니다.
이 글에서는 직접 풀지 못한 상태에서 문제를 어떻게 분석하고, 왜 이 풀이 구조가 나오는지 흐름 중심으로 정리합니다.
문제

S문자열에서 T문자열과 아나그램이 되는 S의 부분문자열의 개수를 구하는 프로그램을 작성하는 문제입니다.
아나그램 판별 시 대소문자는 서로 다른 문자로 구분되며, 부분문자열은 반드시 연속된 문자열이어야 합니다.
첫 번째 줄에는 문자열 S가 주어지고, 두 번째 줄에는 문자열 T가 주어집니다.
S문자열의 길이는 최대 10,000을 넘지 않으며, T문자열의 길이는 S문자열의 길이보다 작거나 같습니다.
출력은 S단어 안에서 T문자열과 아나그램 관계가 되는 부분문자열의 총 개수를 출력해야 합니다.
예를 들어 S = “bacaAacba”, T = “abc”가 주어졌을 경우,
{bac}, {acb}, {cba} 총 3개의 부분문자열이 아나그램에 해당하므로 정답은 3이 됩니다.
해설

처음 이 문제를 접했을 때, 직접적인 풀이를 떠올리지 못하고 접근 방법에서 막혔습니다.
문자열을 하나하나 잘라 비교하는 방식은 떠올릴 수 있었지만, 시간 복잡도 측면에서 적절한지 판단하기 어려웠습니다.
만약 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이 된 문자를 삭제하지 않으면,
해시 크기 비교에서 오답이 발생할 수 있으므로 반드시 처리해야 합니다.
정리
이 문제는 아나그램이라는 개념보다, 연속된 부분 문자열을 어떻게 효율적으로 검사할 것인지가 핵심입니다.
해시와 슬라이딩 윈도우를 함께 사용하면 시간 복잡도를 크게 줄일 수 있으며,
문자열 관련 문제에서 매우 자주 활용되는 패턴입니다.
해시 빈도 집계 풀이를 더 짧게 점검하려면 JavaScript 해시 Map 학급 회장를 먼저 확인해보세요.
슬라이딩 윈도우 풀이 흐름을 더 연습하려면 JavaScript 최대 매출 슬라이딩 윈도우처럼 고정 길이 구간을 다루는 문제와 함께 보면 좋습니다.
QnA
Q. 왜 문자열을 정렬해서 비교하면 안 되나요?
정렬 방식은 구현은 쉽지만, 매번 정렬이 발생해 시간 복잡도가 크게 증가합니다. 이 문제의 입력 크기에서는 비효율적입니다.
Q. Map 대신 객체를 사용해도 되나요?
가능하지만, 키의 개수 관리와 비교 측면에서는 Map이 더 명확하고 안전합니다.
Q. 이 패턴은 어디에 또 사용되나요?
부분 문자열, 연속 구간 합, 빈도 비교 문제 등에서 거의 동일한 구조로 반복 등장합니다.
Q. 면접에서 이 문제를 설명하라고 하면 어떻게 말해야 하나요?
해시로 문자 빈도를 관리하고, 슬라이딩 윈도우로 연속 구간을 유지하며 비교했다고 설명하면 됩니다.
Q. 시간 복잡도는 어떻게 되나요?
문자열 S를 한 번 순회하므로 O(N)이며, 해시 비교는 문자 종류 수만큼만 수행됩니다.
Q. 대소문자 조건은 왜 중요한가요?
문자 자체를 키로 사용하기 때문에 대소문자 구분 여부가 결과에 직접적인 영향을 줍니다.
과정 마무리 실습
이미 푼 문제 한 개를 해설 없이 다시 풀고 경계 입력 세 개를 추가하세요.
완료 기준: 예상 출력과 실제 출력을 비교하고 시간 복잡도 및 다른 풀이의 장단점을 설명합니다.
이어서 공부할 과정: 자료구조 첫 글
풀이 전에 확인할 순서
- 입력값과 출력값을 한 문장으로 다시 적습니다.
- 반복할 대상과 비교·저장할 값을 정합니다.
- 필요한 자료구조와 시간복잡도를 예상합니다.
- 코드를 보기 전에 손으로 작은 예제를 계산합니다.
힌트: 코딩테스트 JS 해시 풀이: 모든 아나그램 찾기
정답 코드를 바로 따라 쓰기보다, 본문에서 값이 갱신되는 조건과 반복 범위를 먼저 찾으세요. 반복 한 번마다 반드시 유지되어야 하는 값이 무엇인지 적으면 풀이의 중심 변수를 고르기 쉽습니다.
테스트 확인
- 가능한 가장 작은 입력
- 같은 값이나 문자가 반복되는 입력
- 정답이 처음 또는 마지막 위치에서 결정되는 입력
- 입력 제한에 가까운 경우의 실행 시간
확인 결과: 본문의 예제뿐 아니라 위 경계 사례에서도 예상값과 실제 출력이 같아야 풀이가 완료됩니다.
이 글이 도움이 되었나요?
코딩테스트 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의 새 글을 확인할 수 있습니다.