이번 편의 결과물: coding-test/04-hash-map-set/에 문제 4개 풀이를 작성하고 node --test를 통과시킵니다. · 다루는 개념: Map/Set 기반 O(1) 조회, Two Sum류 패턴, 그룹핑·중복 제거
이 편에서 만드는 파일
coding-test/
└── 04-hash-map-set/
├── 01-two-sum/
│ ├── solution.js (+)
│ └── solution.test.js (+)
├── 02-group-anagrams/
│ ├── solution.js (+)
│ └── solution.test.js (+)
├── 03-dedupe-preserve-order/
│ ├── solution.js (+)
│ └── solution.test.js (+)
└── 04-array-intersection/
├── solution.js (+)
└── solution.test.js (+)개념 정리
02편의 자료구조 선택 지도에서 “값의 존재·중복 여부를 빠르게 확인”할 때 Map과 Set을 쓴다고 정리했습니다. 이 편은 그 원리를 네 문제로 직접 구현합니다.
| 자료구조 | 저장 형태 | 이 편에서 쓰는 곳 |
|---|---|---|
Map | 키-값 쌍 | ”값 -> 인덱스”, “키 -> 그룹” 처럼 값에 부가 정보를 함께 저장할 때 |
Set | 값만(중복 불가) | 중복 제거, “이미 나왔는지”만 확인할 때 |
일반 객체 {} | 키-값 쌍(문자열 키만) | 03편에서 이미 사용, Map과 API는 다르지만 원리는 비슷함 |
Map과 Set은 내부적으로 해시 테이블을 써서 get·has·add·delete가 평균 O(1)입니다. 배열의 includes나 indexOf는 매번 처음부터 끝까지 훑어야 해서 O(n)입니다. 반복 조회가 많은 문제일수록 Map·Set으로 바꾸는 효과가 커집니다.
| 메서드 | Map | Set |
|---|---|---|
| 추가 | set(key, value) | add(value) |
| 존재 확인 | has(key) | has(value) |
| 조회 | get(key) | (Set은 값 자체가 존재 여부만 표현) |
| 개수 | size | size |
실습
1. 합이 target인 두 인덱스 찾기
문제: 정수 배열 nums와 정수 target이 주어질 때, 합이 target이 되는 두 원소의 인덱스를 배열로 반환합니다. 같은 원소를 두 번 쓰지 않고, 정답은 항상 하나만 존재한다고 가정합니다.
입출력 예
| nums | target | 결과 |
|---|---|---|
[2, 7, 11, 15] | 9 | [0, 1] |
[-3, 4, 3, 90] | 0 | [0, 2] |
접근: 이중 반복문으로 모든 쌍을 확인하면 O(n^2)입니다. 배열을 순회하며 “지금까지 본 값 -> 인덱스”를 Map에 기록해두면, 각 원소마다 target - nums[i](complement)가 이미 Map에 있는지 O(1)로 확인할 수 있어 전체 O(n)입니다.
// coding-test/04-hash-map-set/01-two-sum/solution.js
// 시간복잡도: O(n) — 배열을 한 번 순회하며 Map에서 조회한다
// 공간복잡도: O(n) — 지금까지 본 값을 Map에 저장한다
export function solution(nums, target) {
const indexByValue = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (indexByValue.has(complement)) {
return [indexByValue.get(complement), i];
}
indexByValue.set(nums[i], i);
}
return [];
}// coding-test/04-hash-map-set/01-two-sum/solution.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { solution } from './solution.js';
test('합이 target인 두 인덱스를 찾는다', () => {
assert.deepStrictEqual(solution([2, 7, 11, 15], 9), [0, 1]);
});
test('음수가 섞여 있어도 정확하다', () => {
assert.deepStrictEqual(solution([-3, 4, 3, 90], 0), [0, 2]);
});함정: 현재 값을 Map에 저장하기 전에 complement부터 먼저 확인해야 합니다. 순서를 바꾸면 target이 nums[i]의 2배일 때 자기 자신을 자신의 짝으로 착각해 잘못된 쌍을 반환할 수 있습니다.
2. 그룹 애너그램 묶기
문제: 문자열 배열이 주어질 때, 구성 문자와 개수가 같은 문자열(애너그램)끼리 묶어 배열의 배열로 반환합니다.
입출력 예
| 입력 | 결과 |
|---|---|
[eat, tea, tan, ate, nat, bat] | [[eat, tea, ate], [tan, nat], [bat]] |
접근: 각 문자열의 글자를 정렬한 결과를 그룹 키로 씁니다. 같은 애너그램은 정렬하면 항상 같은 문자열이 되므로, Map에 “정렬된 키 -> 원본 문자열 목록”으로 모읍니다. 문자열 n개, 평균 길이 k일 때 O(n * k log k)입니다.
// coding-test/04-hash-map-set/02-group-anagrams/solution.js
// 시간복잡도: O(n * k log k) — 문자열 n개를 각각 정렬(O(k log k))해 키로 쓴다
// 공간복잡도: O(n * k) — 그룹별 문자열을 모두 저장한다
export function solution(words) {
const groups = new Map();
for (const word of words) {
const key = word.split('').sort().join('');
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(word);
}
return [...groups.values()];
}// coding-test/04-hash-map-set/02-group-anagrams/solution.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { solution } from './solution.js';
test('애너그램끼리 묶는다', () => {
const result = solution(['eat', 'tea', 'tan', 'ate', 'nat', 'bat']);
assert.deepStrictEqual(result, [
['eat', 'tea', 'ate'],
['tan', 'nat'],
['bat'],
]);
});
test('애너그램이 없으면 각자 자기 그룹이 된다', () => {
assert.deepStrictEqual(solution(['abc', 'def']), [['abc'], ['def']]);
});함정: 문자열 길이만으로 그룹을 나누면 길이는 같지만 구성 문자가 다른 단어까지 잘못 묶입니다(예: abc와 abd). 반드시 글자 자체를 정렬한 값을 키로 써야 정확히 구분됩니다.
3. 순서를 유지하며 배열 중복 제거하기
문제: 정수 배열에서 중복된 값을 제거하되, 처음 등장한 순서를 그대로 유지한 배열을 반환합니다.
입출력 예
| 입력 | 결과 |
|---|---|
[1, 2, 2, 3, 1, 4] | [1, 2, 3, 4] |
[5, 5, 5] | [5] |
접근: Set은 값을 추가한 순서를 그대로 기억하면서 중복은 자동으로 걸러줍니다. new Set(nums)로 중복을 없앤 뒤 배열로 펼치면 됩니다. O(n)입니다.
// coding-test/04-hash-map-set/03-dedupe-preserve-order/solution.js
// 시간복잡도: O(n) — 배열 원소를 한 번씩만 Set에 넣는다
// 공간복잡도: O(n) — 중복 없는 값을 저장하는 Set을 쓴다
export function solution(nums) {
return [...new Set(nums)];
}// coding-test/04-hash-map-set/03-dedupe-preserve-order/solution.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { solution } from './solution.js';
test('중복을 제거하되 처음 등장 순서를 유지한다', () => {
assert.deepStrictEqual(solution([1, 2, 2, 3, 1, 4]), [1, 2, 3, 4]);
});
test('모두 같은 값이면 하나만 남는다', () => {
assert.deepStrictEqual(solution([5, 5, 5]), [5]);
});함정: Set은 값 자체(원시값)를 기준으로 중복을 판단합니다. 배열이나 객체 같은 참조 타입은 내용이 같아도 참조가 다르면 다른 값으로 취급되어 중복 제거가 안 됩니다. 숫자·문자열 같은 원시값 배열에서만 이 방법을 그대로 쓸 수 있습니다.
4. 두 배열의 교집합 구하기
문제: 두 정수 배열이 주어질 때, 두 배열에 공통으로 들어있는 값(중복 없이)을 오름차순으로 정렬해 반환합니다.
입출력 예
| nums1 | nums2 | 결과 |
|---|---|---|
[1, 2, 2, 1] | [2, 2] | [2] |
[4, 9, 5] | [9, 4, 9, 8, 4] | [4, 9] |
접근: 한 배열을 Set으로 만들어두면, 다른 배열을 순회하며 각 값이 그 Set에 있는지 O(1)로 확인할 수 있습니다. 전체 O(n + m)입니다.
// coding-test/04-hash-map-set/04-array-intersection/solution.js
// 시간복잡도: O(n + m) — 두 배열 길이의 합에 비례한다
// 공간복잡도: O(n + m) — Set 두 개를 사용한다
export function solution(nums1, nums2) {
const firstSet = new Set(nums1);
const resultSet = new Set();
for (const num of nums2) {
if (firstSet.has(num)) {
resultSet.add(num);
}
}
return [...resultSet].sort((a, b) => a - b);
}// coding-test/04-hash-map-set/04-array-intersection/solution.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { solution } from './solution.js';
test('공통으로 들어있는 값만 중복 없이 반환한다', () => {
assert.deepStrictEqual(solution([1, 2, 2, 1], [2, 2]), [2]);
});
test('결과를 오름차순으로 정렬해 반환한다', () => {
assert.deepStrictEqual(solution([4, 9, 5], [9, 4, 9, 8, 4]), [4, 9]);
});함정: sort()를 비교 함수 없이 호출하면 숫자를 문자열로 바꿔 정렬해 [10, 2, 1]이 [1, 10, 2]처럼 사전 순서로 뒤섞입니다. 숫자를 정렬할 때는 항상 (a, b) => a - b 같은 비교 함수를 넘겨야 합니다.
직접 해보기
- Two Sum 문제를 응용해 “합이 target인 두 인덱스가 여러 쌍 존재할 수 있다”고 가정하고, 모든 쌍을 배열로 반환하도록 바꿔봅니다.
- 배열 교집합 문제를 응용해 “두 배열의 차집합”(
nums1에는 있지만nums2에는 없는 값)을 구하는 함수를 작성하고 테스트를 추가해봅니다.
정답 보기(2번)
export function solution(nums1, nums2) {
const secondSet = new Set(nums2);
const resultSet = new Set();
for (const num of nums1) {
if (!secondSet.has(num)) {
resultSet.add(num);
}
}
return [...resultSet].sort((a, b) => a - b);
}nums2를 Set으로 만들고, nums1을 순회하며 그 Set에 없는 값만 결과에 담으면 차집합이 됩니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| Two Sum에서 같은 원소를 자기 자신과 짝지음 | 현재 값을 Map에 저장한 뒤에 complement를 확인함 | 저장하기 전에 먼저 Map에서 complement를 확인 |
| 애너그램이 아닌 단어끼리 묶임 | 문자열 길이만으로 그룹을 나눔 | 글자를 정렬한 문자열을 키로 사용 |
| 숫자 배열 정렬 결과가 이상하게 뒤섞임 | sort()를 비교 함수 없이 호출해 문자열 기준으로 정렬됨 | (a, b) => a - b 비교 함수를 명시 |