Skip to Content
WebJavaScriptJavaScript 코딩테스트04. 해시(Map/Set)로 조회 최적화

이번 편의 결과물: 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는 다르지만 원리는 비슷함

MapSet은 내부적으로 해시 테이블을 써서 get·has·add·delete가 평균 O(1)입니다. 배열의 includesindexOf는 매번 처음부터 끝까지 훑어야 해서 O(n)입니다. 반복 조회가 많은 문제일수록 Map·Set으로 바꾸는 효과가 커집니다.

메서드MapSet
추가set(key, value)add(value)
존재 확인has(key)has(value)
조회get(key)(Set은 값 자체가 존재 여부만 표현)
개수sizesize

실습

1. 합이 target인 두 인덱스 찾기

문제: 정수 배열 nums와 정수 target이 주어질 때, 합이 target이 되는 두 원소의 인덱스를 배열로 반환합니다. 같은 원소를 두 번 쓰지 않고, 정답은 항상 하나만 존재한다고 가정합니다.

입출력 예

numstarget결과
[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부터 먼저 확인해야 합니다. 순서를 바꾸면 targetnums[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']]); });

함정: 문자열 길이만으로 그룹을 나누면 길이는 같지만 구성 문자가 다른 단어까지 잘못 묶입니다(예: abcabd). 반드시 글자 자체를 정렬한 값을 키로 써야 정확히 구분됩니다.

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. 두 배열의 교집합 구하기

문제: 두 정수 배열이 주어질 때, 두 배열에 공통으로 들어있는 값(중복 없이)을 오름차순으로 정렬해 반환합니다.

입출력 예

nums1nums2결과
[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 같은 비교 함수를 넘겨야 합니다.

직접 해보기

  1. Two Sum 문제를 응용해 “합이 target인 두 인덱스가 여러 쌍 존재할 수 있다”고 가정하고, 모든 쌍을 배열로 반환하도록 바꿔봅니다.
  2. 배열 교집합 문제를 응용해 “두 배열의 차집합”(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); }

nums2Set으로 만들고, nums1을 순회하며 그 Set에 없는 값만 결과에 담으면 차집합이 됩니다.

자주 하는 실수

증상원인고치는 법
Two Sum에서 같은 원소를 자기 자신과 짝지음현재 값을 Map에 저장한 뒤에 complement를 확인함저장하기 전에 먼저 Map에서 complement를 확인
애너그램이 아닌 단어끼리 묶임문자열 길이만으로 그룹을 나눔글자를 정렬한 문자열을 키로 사용
숫자 배열 정렬 결과가 이상하게 뒤섞임sort()를 비교 함수 없이 호출해 문자열 기준으로 정렬됨(a, b) => a - b 비교 함수를 명시

확인 문제

문제 14지선다
Two Sum 문제를 이중 반복문 대신 Map으로 풀면 시간복잡도가 어떻게 바뀌는가?
문제 24지선다
그룹 애너그램 문제에서 문자열을 정렬한 값을 그룹 키로 쓰는 이유는?
문제 34지선다
Set으로 배열의 중복을 제거했을 때 원래 순서가 유지되는 이유는?
문제 44지선다
[10, 2, 1].sort()의 결과가 [1, 10, 2]가 되는 이유는?

참고 자료

Last updated on