주요 포인트 한눈에 보기
includes()로 교집합은 구했지만 입력이 3만 개까지 커질 때 시간 초과가 걱정되는 JavaScript 학습자를 위한 풀이입니다. 두 배열을 숫자 기준으로 정렬한 뒤 투 포인터로 한 번만 훑는 이유, 정렬 비용까지 포함한 복잡도, 원본 배열 변경과 중복값 가정을 예제로 검증합니다.
문제
A, B 두 개의 집합이 주어지면 두 집합의 공통 원소를 추출하여 오름차순으로 출력하는 프로그램을 작성하세요.
입력 설명
첫 번째 줄에 집합 A의 크기 N(1 ≤ N ≤ 30,000)이 주어집니다.
두 번째 줄에 N개의 원소가 주어집니다. 원소는 중복되지 않습니다.
세 번째 줄에 집합 B의 크기 M(1 ≤ M ≤ 30,000)이 주어집니다.
네 번째 줄에 M개의 원소가 주어집니다. 원소는 중복되지 않습니다.
각 집합의 원소는 1,000,000,000 이하의 자연수입니다.
출력 설명
두 집합의 공통 원소를 오름차순으로 정렬하여 출력합니다.
입력 예제
5
1 3 9 5 2
5
3 2 5 7 8
출력 예제
2 3 5
내가 푼 방식

한 배열을 순회하면서 다른 배열에 포함되어 있는지 includes()로 확인하는 방식입니다.
공통 원소를 배열에 모은 뒤, 마지막에 정렬합니다.
function solution(arr1, arr2) {
let arr = [];
arr1.forEach((item) => {
if (arr2.includes(item)) {
arr.push(item);
}
});
return arr.sort((a, b) => a - b);
}
왜 이 방식이 아닌가
includes()는 내부적으로 배열을 처음부터 끝까지 탐색합니다.
따라서 이 방식은 배열 크기가 커질수록 성능이 급격히 떨어집니다.
이 문제에서는 배열의 길이가 최대 30,000까지 가능하므로,
최악의 경우 O(N × M)에 가까운 시간이 걸릴 수 있습니다.
코딩 테스트에서는 이런 방식이 시간 초과로 이어질 가능성이 높습니다.
정답

두 배열을 먼저 오름차순으로 정렬한 뒤, 투 포인터 방식으로 공통 원소를 찾습니다.
function solution(arr1, arr2) {
let answer = [];
arr1.sort((a, b) => a - b);
arr2.sort((a, b) => a - b);
let p1 = 0;
let p2 = 0;
while (p1 < arr1.length && p2 < arr2.length) {
if (arr1[p1] === arr2[p2]) {
answer.push(arr1[p1]);
p1++;
p2++;
} else if (arr1[p1] < arr2[p2]) {
p1++;
} else {
p2++;
}
}
return answer;
}
정답 해설
두 배열을 각각 오름차순으로 정렬하면, 가장 작은 값부터 차례대로 비교할 수 있는 상태가 됩니다.
이 상태에서 두 포인터를 사용하면 불필요한 비교 없이 공통 원소만 골라낼 수 있습니다.
arr1[p1]과 arr2[p2]가 같으면 공통 원소이므로 결과에 추가하고,
두 포인터를 모두 한 칸씩 이동합니다.
값이 다를 경우에는 더 작은 값을 가진 쪽 포인터만 이동합니다.
이렇게 하면 이미 비교가 끝난 값은 다시 볼 필요가 없습니다.
공통 원소 추출 전에 투 포인터 기본 이동 방식을 먼저 잡고 싶다면 JS 투 포인터 배열 합치기를 함께 복습하면 좋습니다.
두 배열 공통 원소 문제의 핵심이 되는 포인터 이동 방식은 JS 연속 구간 합 투 포인터에서 기본 패턴으로 정리됩니다.
예제 디테일
기본 예제
console.log(solution([1, 3, 9, 5, 2], [3, 2, 5, 7, 8]));
// [2, 3, 5]
동작 설명: 두 배열을 정렬한 뒤, 작은 값부터 하나씩 비교합니다.
값이 같은 경우에만 결과 배열에 추가됩니다.
메커니즘 해설: 포인터는 한 방향으로만 이동하고 각 비교에서 적어도 하나가 전진합니다. 같은 원소가 여러 번 비교될 수는 있지만 총 포인터 이동 횟수는 두 배열 길이의 합 이하입니다.
실수 포인트: 값이 같으면 두 포인터를 함께 이동해 이미 처리한 두 원소를 건너뜁니다. 반복문에서 어느 포인터도 움직이지 않는 분기가 있으면 무한 루프가 발생할 수 있습니다.
응용 예제
console.log(solution([1, 2, 4, 6, 8], [2, 4, 6, 8, 10]));
// [2, 4, 6, 8]
동작 설명: 두 배열 모두 정렬되어 있으므로
포인터는 같은 값에서만 동시에 이동합니다.
메커니즘 해설: 중복이 없는 집합 조건 덕분에
결과에는 동일한 값이 한 번씩만 추가됩니다.
실수 포인트: 문제 조건에 “중복 없음”이 없다면
중복 처리 로직을 별도로 고려해야 합니다.
오류 예제
console.log(solution([3, 1, 5], [2, 3]));
// 정렬되지 않은 입력
동작 설명: 입력이 정렬되어 있지 않으면
포인터 비교 순서가 의미를 잃습니다.
메커니즘 해설: 투 포인터는 “앞에 있는 값이 더 작다”는
전제가 반드시 필요합니다.
실수 포인트: 이 문제는 정렬이 보장되지 않으므로
반드시 정렬 후에 투 포인터를 적용해야 합니다.
복잡도·입력 조건·검증 체크리스트
includes()를 각 원소마다 호출하는 첫 풀이는 최악의 경우 N×M번에 가까운 비교가 필요합니다. 정답 풀이는 두 배열 정렬에 O(N log N + M log M), 두 포인터 순회에 O(N+M)이 듭니다. 이미 정렬된 입력이라면 순회 부분만 O(N+M)이지만, 이 문제의 원본 입력은 정렬을 보장하지 않으므로 전체 복잡도에서 정렬 비용을 빼면 안 됩니다.
Array.prototype.sort()는 원본 배열을 제자리에서 바꾸며, 숫자를 오름차순으로 정렬하려면 (a, b) => a - b 비교 함수를 전달해야 합니다. 호출자가 원본을 계속 써야 한다면 [...arr1].sort(...)처럼 복사본을 정렬하세요. 자세한 동작은 MDN Array.prototype.sort() 문서에서 확인할 수 있습니다.
console.assert(JSON.stringify(solution([1, 3, 9, 5, 2], [3, 2, 5, 7, 8])) === '[2,3,5]');
console.assert(JSON.stringify(solution([], [1, 2])) === '[]');
console.assert(JSON.stringify(solution([4], [4])) === '[4]');
문제는 각 집합 안의 원소가 중복되지 않는다고 명시합니다. 중복을 허용하면 현재 코드는 두 배열에 존재하는 횟수의 최솟값만큼 결과에 넣는 다중집합 교집합처럼 동작하므로, ‘고유값만 한 번’이 목표라면 별도의 중복 제거 규칙이 필요합니다.
결과 원소 수를 K라고 하면 정답 배열에는 O(K) 공간이 필요합니다. 원본 보존을 위해 두 배열을 복사해 정렬하면 여기에 O(N+M) 복사 공간이 추가됩니다. 반대로 현재 코드처럼 제자리 정렬하면 복사 공간은 줄지만 호출 뒤 입력 순서가 달라집니다. 어느 선택이 맞는지는 함수 계약에 포함해야 합니다.
정리: 먼저 입력의 정렬 여부와 중복 정책을 확인하고, 정렬 비용·원본 변경 여부까지 설명할 수 있어야 완성된 풀이입니다. 이 조건을 적은 뒤 포인터가 한 방향으로만 이동한다는 불변식을 제시하면 시간 복잡도 설명도 명확해집니다.
풀이 전에 확인할 순서
- 입력값과 출력값을 한 문장으로 다시 적습니다.
- 반복할 대상과 비교·저장할 값을 정합니다.
- 필요한 자료구조와 시간복잡도를 예상합니다.
- 코드를 보기 전에 손으로 작은 예제를 계산합니다.
힌트: 코딩테스트 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의 새 글을 확인할 수 있습니다.