코딩테스트란?
코딩테스트는 개발자 채용 과정에서 지원자의 알고리즘 문제 해결 능력과 코딩 실력을 평가하기 위한 시험입니다. 카카오, 네이버, 라인, 쿠팡 등 국내 주요 IT 기업과 구글, 메타, 아마존 등 글로벌 빅테크 기업 모두 채용 과정에 코딩테스트를 포함하고 있습니다.
핵심 포인트: 코딩테스트는 단순히 코드를 작성하는 능력만 평가하는 것이 아니라, 주어진 문제를 분석하고 효율적인 알고리즘을 설계하며 이를 정확한 코드로 구현하는 종합적인 능력을 측정합니다.
코딩테스트의 주요 평가 요소
코딩테스트에서 평가하는 핵심 요소는 다음과 같습니다.
| 평가 요소 | 설명 |
|---|---|
| 정확성 | 주어진 모든 테스트 케이스를 통과하는가 |
| 효율성 | 시간 복잡도와 공간 복잡도가 최적인가 |
| 코드 품질 | 코드가 읽기 쉽고 유지보수 가능한가 |
| 엣지 케이스 처리 | 경계 조건과 예외 상황을 올바르게 처리하는가 |
코딩테스트 주요 알고리즘 유형
코딩테스트에서 자주 출제되는 알고리즘 유형을 난이도별로 정리했습니다.
기초 단계 (쉬움)
- 구현/시뮬레이션: 문제에서 요구하는 대로 정확히 구현
- 완전탐색(Brute Force): 모든 경우의 수를 확인
- 문자열 처리: 문자열 파싱, 변환, 검증
- 정렬: 내장 정렬 함수 활용 또는 정렬 알고리즘 구현
- 해시(Hash): 딕셔너리를 활용한 빠른 탐색
중급 단계 (보통)
- 탐욕법(Greedy): 매 순간 최선의 선택
- 이진탐색(Binary Search): 정렬된 데이터에서 탐색
- 스택/큐: LIFO/FIFO 자료구조 활용
- DFS/BFS: 그래프 탐색 알고리즘
- 투 포인터(Two Pointers): 두 개의 포인터로 효율적 탐색
고급 단계 (어려움)
- 동적계획법(DP): 중복 부분문제를 메모이제이션으로 해결
- 그래프 이론: 최단경로(Dijkstra, Floyd-Warshall), 최소신장트리(Kruskal, Prim)
- 분할정복(Divide and Conquer): 문제를 작은 단위로 분할
- 세그먼트 트리: 구간 쿼리를 에 처리
- 트라이(Trie): 문자열 검색에 특화된 트리 구조
실전 예제: 두 수의 합 찾기
가장 기본적이면서도 중요한 패턴인 “두 수의 합” 문제를 통해 난이도별 접근법을 비교해보겠습니다.
문제 설명
정수 배열 nums와 목표값 target이 주어졌을 때, 배열에서 두 수를 선택하여 합이 target이 되는 두 수의 인덱스를 반환하세요.
제약 조건:
– nums.length
– nums[i]
– target
– 정확히 하나의 해답만 존재
예제:
입력: nums = [2, 7, 11, 15], target = 9
출력: [0, 1]
설명: nums[0] + nums[1] = 2 + 7 = 9
접근법 1: 완전탐색 (쉬움)
핵심 원리: 모든 가능한 두 수의 조합을 확인합니다.
def two_sum_brute_force(nums, target):
"""
완전탐색 방식: 이중 루프로 모든 조합 확인
"""
n = len(nums)
# 첫 번째 수 선택
for i in range(n):
# 두 번째 수 선택 (i 이후부터 시작하여 중복 방지)
for j in range(i + 1, n):
# 두 수의 합이 target과 같으면 인덱스 반환
if nums[i] + nums[j] == target:
return [i, j]
return [] # 해답이 없는 경우 (문제 조건상 발생하지 않음)
# 테스트
nums = [2, 7, 11, 15]
target = 9
print(two_sum_brute_force(nums, target)) # [0, 1]
복잡도 분석:
– 시간 복잡도: – 이중 루프로 모든 조합 확인
– 공간 복잡도: – 추가 메모리 사용 없음
장단점:
– ✅ 구현이 간단하고 직관적
– ❌ 데이터가 많을 때() 시간 초과 발생 가능
접근법 2: 해시 테이블 (보통) ⭐ 최적 풀이
핵심 원리: 각 원소를 순회하면서 target - nums[i]가 이미 딕셔너리에 존재하는지 확인합니다.
def two_sum_hash(nums, target):
"""
해시 테이블 방식: 딕셔너리로 O(1) 탐색
"""
# 값 -> 인덱스 매핑을 저장할 딕셔너리
num_to_index = {}
for i, num in enumerate(nums):
# 현재 수와 더해서 target이 되는 수 계산
complement = target - num
# complement가 이미 딕셔너리에 있으면 답 찾음
if complement in num_to_index:
return [num_to_index[complement], i]
# 현재 수를 딕셔너리에 저장 (반드시 확인 후 저장)
num_to_index[num] = i
return []
# 테스트
nums = [2, 7, 11, 15]
target = 9
print(two_sum_hash(nums, target)) # [0, 1]
복잡도 분석:
– 시간 복잡도: – 배열을 한 번만 순회
– 공간 복잡도: – 최악의 경우 모든 원소를 딕셔너리에 저장
장단점:
– ✅ 시간 복잡도가 선형으로 매우 효율적
– ✅ 대부분의 코딩테스트에서 통과 가능
– ✅ 이 문제의 최적 풀이
– ❌ 추가 메모리 사용 (하지만 은 허용 범위)
접근법 3: 투 포인터 (보통)
핵심 원리: 배열을 정렬한 후 양 끝에서 포인터를 움직이며 합을 조정합니다.
주의: 이 문제에서는 원본 인덱스를 반환해야 하므로 정렬 시 인덱스 정보를 함께 저장해야 합니다. 이 때문에 해시 방식보다 복잡하고 느립니다. 투 포인터는 “정렬된 배열”이 주어지거나 “인덱스가 아닌 값 자체”를 반환하는 문제에 더 적합합니다.
def two_sum_two_pointers(nums, target):
"""
투 포인터 방식: 정렬 후 양 끝에서 탐색
주의: 원본 인덱스를 유지해야 하므로 (값, 인덱스) 튜플로 저장
"""
# 원본 인덱스를 유지하기 위해 (값, 원본 인덱스) 튜플 생성
nums_with_index = [(num, i) for i, num in enumerate(nums)]
# 값 기준으로 정렬
nums_with_index.sort()
# 양 끝에 포인터 배치
left = 0
right = len(nums) - 1
while left < right:
current_sum = nums_with_index[left][0] + nums_with_index[right][0]
if current_sum == target:
# 원본 인덱스 반환 (작은 인덱스를 먼저)
idx1 = nums_with_index[left][1]
idx2 = nums_with_index[right][1]
return [min(idx1, idx2), max(idx1, idx2)]
elif current_sum < target:
# 합이 작으면 왼쪽 포인터를 오른쪽으로
left += 1
else:
# 합이 크면 오른쪽 포인터를 왼쪽으로
right -= 1
return []
# 테스트
nums = [2, 7, 11, 15]
target = 9
print(two_sum_two_pointers(nums, target)) # [0, 1]
복잡도 분석:
– 시간 복잡도: – 정렬에 의해 결정됨
– 공간 복잡도: – 정렬을 위한 추가 배열 생성
장단점:
– ✅ 정렬된 배열이 주어지면 시간, 공간으로 해결 가능
– ❌ 이 문제(원본 인덱스 반환)에서는 해시 방식보다 느리고 복잡함
– ✅ 다른 변형 문제(세 수의 합, 네 수의 합 등)로 확장 가능
동작 과정 시각화
해시 테이블 방식을 예제로 단계별 동작을 살펴보겠습니다.
입력: nums = [2, 7, 11, 15], target = 9
단계 1: i=0, num=2
complement = 9 - 2 = 7
딕셔너리: {}
7이 딕셔너리에 없음 → 2를 저장
딕셔너리: {2: 0}
단계 2: i=1, num=7
complement = 9 - 7 = 2
딕셔너리: {2: 0}
2가 딕셔너리에 있음! → [0, 1] 반환 ✓
난이도별 접근법 비교표
| 접근법 | 난이도 | 시간 복잡도 | 공간 복잡도 | 추천 상황 |
|---|---|---|---|---|
| 완전탐색 | 쉬움 | 작은 데이터 | ||
| 해시 테이블 | 보통 | 대부분의 경우 최적 ⭐ | ||
| 투 포인터 | 보통 | 정렬된 배열 / 변형 문제 |
자주 하는 실수와 엣지 케이스
실수 1: 같은 인덱스를 두 번 사용
# 잘못된 코드
def wrong_two_sum(nums, target):
num_to_index = {}
for i, num in enumerate(nums):
num_to_index[num] = i # ❌ 먼저 저장하면 안 됨!
complement = target - num
if complement in num_to_index: # 같은 인덱스를 두 번 사용할 수 있음
return [num_to_index[complement], i]
return []
# 문제 발생 예시
print(wrong_two_sum([3, 2, 4], 6)) # [1, 2] (정답)
print(wrong_two_sum([5], 10)) # [0, 0] (오답! 5를 두 번 사용)
해결: 딕셔너리에 추가하기 전에 complement를 먼저 확인합니다.
# 올바른 순서
if complement in num_to_index: # 1. 먼저 확인
return [num_to_index[complement], i]
num_to_index[num] = i # 2. 그 다음 저장
실수 2: 인덱스 순서
문제에서 “작은 인덱스를 먼저 반환”을 요구할 수 있습니다. 해시 방식은 자동으로 순서가 보장되지만, 투 포인터 방식에서는 정렬 후 인덱스가 섞이므로 [min(idx1, idx2), max(idx1, idx2)]로 처리해야 합니다.
엣지 케이스
- 음수 포함:
nums = [-1, -2, -3, -4], target = -6→[1, 2](음수도 정상 처리) - 중복 값:
nums = [3, 3], target = 6→[0, 1](다른 인덱스면 사용 가능) - 큰 수: 범위의 정수 → Python은 자동으로 처리 (int overflow 없음)
- 최소 크기:
nums = [1, 2], target = 3→[0, 1](배열 크기 2도 정상 동작)
관련 문제와 응용
“두 수의 합” 패턴은 다양한 변형 문제로 확장됩니다.
변형 1: 세 수의 합 (3Sum)
문제: 배열에서 합이 0이 되는 서로 다른 세 수의 조합을 모두 찾기 (중복 제거)
def three_sum(nums):
"""
세 수의 합: 첫 번째 수를 고정하고 나머지는 투 포인터
"""
nums.sort() # 정렬 필수
result = []
n = len(nums)
for i in range(n - 2):
# 중복 제거: 같은 값은 건너뛰기
if i > 0 and nums[i] == nums[i - 1]:
continue
# 최적화: 가장 작은 수가 0보다 크면 더 이상 합이 0이 될 수 없음
if nums[i] > 0:
break
# 나머지 두 수를 투 포인터로 찾기
left, right = i + 1, n - 1
target = -nums[i] # 세 수의 합이 0이므로 나머지 두 수의 합은 -nums[i]
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
result.append([nums[i], nums[left], nums[right]])
left += 1
right -= 1
# 중복 제거
while left < right and nums[left] == nums[left - 1]:
left += 1
while left < right and nums[right] == nums[right + 1]:
right -= 1
elif current_sum < target:
left += 1
else:
right -= 1
return result
# 테스트
print(three_sum([-1, 0, 1, 2, -1, -4]))
# [[-1, -1, 2], [-1, 0, 1]]
시간 복잡도: – 고정 1개 + 투 포인터 =
핵심 포인트:
– 정렬이 필수 (투 포인터 사용 위해)
– 중복 제거를 위해 같은 값은 건너뜀
– 해시 방식으로는 3Sum을 보다 빠르게 풀 수 없음 (투 포인터가 최적)
변형 2: 두 수의 합 II (정렬된 배열)
문제: 이미 오름차순 정렬된 배열에서 두 수의 합 찾기
배열이 이미 정렬되어 있으면 투 포인터가 최적입니다. 별도 정렬이 필요 없고 공간 복잡도도 입니다.
def two_sum_sorted(numbers, target):
"""
정렬된 배열에서 투 포인터 사용 (공간 복잡도 O(1))
LeetCode 167번 기준으로 1-indexed 반환
"""
left, right = 0, len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left + 1, right + 1] # 1-indexed로 반환
elif current_sum < target:
left += 1
else:
right -= 1
return []
# 테스트
print(two_sum_sorted([2, 7, 11, 15], 9)) # [1, 2]
시간 복잡도: , 공간 복잡도: ⭐
언제 투 포인터를 쓸까?
– 배열이 이미 정렬되어 있거나
– 정렬해도 되고 (원본 인덱스가 필요 없고)
– 값 자체를 반환하는 경우
코딩테스트 준비 전략
1. 단계별 학습 로드맵
| 단계 | 기간 | 학습 내용 | 추천 문제 수 |
|---|---|---|---|
| 기초 | 1-2주 | 배열, 문자열, 해시, 정렬 | 30-50문제 |
| 중급 | 2-3주 | 스택/큐, DFS/BFS, 이진탐색, 그리디 | 50-70문제 |
| 고급 | 3-4주 | DP, 그래프, 분할정복 | 40-60문제 |
| 실전 | 1-2주 | 모의 테스트, 약점 보완 | 20-30문제 |
2. 효과적인 문제 풀이 프로세스
1. 문제 이해 (5분)
- 입력/출력 형식 확인
- 제약 조건 파악 (시간 제한, 데이터 크기)
- 예제로 문제 동작 이해
2. 접근법 설계 (10분)
- 브루트포스로 가능한지 판단
- 최적 알고리즘 선택
- 시간/공간 복잡도 계산
3. 코드 구현 (20분)
- 변수명을 명확하게
- 주석으로 의도 설명
- 단계별로 구현
4. 테스트 (5분)
- 예제 입력 확인
- 엣지 케이스 테스트
- 경계 조건 확인
3. 시간 복잡도 판단 기준
입력 크기 에 따른 적합한 알고리즘:
| 입력 크기 | 허용 복잡도 | 알고리즘 예시 |
|---|---|---|
| 순열 완전탐색 (실제 8! = 40,320) | ||
| 비트마스킹, 백트래킹 | ||
| 3중 루프, Floyd-Warshall | ||
| 이중 루프, 버블 정렬 | ||
| 정렬, 우선순위 큐 | ||
| 선형 탐색, 해시 | ||
| n > 10^8 | 또는 | 이진탐색, 수학 공식 |
팁: 시간 제한이 1초일 때, 일반적으로 약 1억 번()의 연산이 가능합니다. 따라서 일 때 는 시간 초과가 발생합니다.
4. 추천 연습 플랫폼
| 플랫폼 | 특징 | 추천 대상 |
|---|---|---|
| 백준 | 한국어, 다양한 난이도, 단계별 학습 | 초보자 ~ 고급 |
| 프로그래머스 | 기업 기출, 레벨별 분류, SQL 포함 | 취업 준비생 |
| LeetCode | 글로벌 표준, 기업 태그, Discussion | 해외 취업, 고급 |
| 코드포스 | 대회 중심, 레이팅 시스템 | 알고리즘 경시 |
5. 실전 팁
- 시간 배분: 쉬운 문제부터 풀고 어려운 문제는 나중에
- 부분 점수: 완벽한 해답이 안 나와도 부분 점수 노리기
- 디버깅:
print()활용해서 중간 과정 확인 - 예외 처리: 빈 배열, 0, 음수 등 엣지 케이스 체크
- 변수명:
i,j보다는left,right,count등 의미 있는 이름 - 입력 속도: Python에서 입력이 많을 때
sys.stdin.readline()사용 - 재귀 깊이: DFS 사용 시
sys.setrecursionlimit(10**6)설정
언어별 유용한 내장 함수
Python
# 정렬
nums.sort() # 제자리 정렬 O(n log n)
sorted(nums) # 새 리스트 반환
nums.sort(reverse=True) # 내림차순
# 이진탐색
import bisect
bisect.bisect_left(nums, target) # target 삽입 위치 (왼쪽)
bisect.bisect_right(nums, target) # target 삽입 위치 (오른쪽)
# 카운터
from collections import Counter
Counter([1, 1, 2, 3]).most_common(2) # [(1, 2), (2, 1)]
# 힙 (우선순위 큐)
import heapq
heapq.heappush(heap, item) # 최소 힙
heapq.heappop(heap)
heapq.heappush(heap, -item) # 최대 힙 (음수로 저장)
# deque (양방향 큐)
from collections import deque
q = deque([1, 2, 3])
q.appendleft(0) # O(1) 왼쪽 삽입
q.popleft() # O(1) 왼쪽 제거
# 순열/조합
from itertools import permutations, combinations
list(permutations([1, 2, 3], 2)) # [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]
list(combinations([1, 2, 3], 2)) # [(1,2), (1,3), (2,3)]
# defaultdict (기본값 있는 딕셔너리)
from collections import defaultdict
graph = defaultdict(list) # 자동으로 빈 리스트 생성
graph[1].append(2) # KeyError 없음
# 입력 속도 개선 (백준)
import sys
input = sys.stdin.readline
n = int(input()) # 개행 문자 포함되므로 int() 필수
마무리
코딩테스트는 알고리즘 지식 + 구현 능력 + 시간 관리의 종합입니다. 이 글에서 다룬 핵심 내용을 정리하면:
핵심 요약
- 알고리즘 유형 파악: 문제를 보고 어떤 알고리즘이 필요한지 빠르게 판단
- 복잡도 분석: 시간/공간 복잡도를 계산하여 효율적인 방법 선택
- 단계별 구현: 브루트포스 → 최적화 순서로 접근
- 엣지 케이스: 빈 배열, 최솟값/최댓값, 중복 등 경계 조건 체크
- 꾸준한 연습: 하루 1-2문제씩 꾸준히 풀기
두 수의 합 패턴 정리
| 상황 | 최적 풀이 | 복잡도 |
|---|---|---|
| 일반 배열, 인덱스 반환 | 해시 테이블 | 시간, 공간 |
| 정렬된 배열, 값 반환 가능 | 투 포인터 | 시간, 공간 |
| 세 수 이상의 합 | 정렬 + 투 포인터 | 시간 |
최종 조언: 처음에는 시간이 오래 걸리더라도 왜 이 알고리즘을 사용하는지, 시간 복잡도가 어떻게 되는지를 명확히 이해하며 풀어야 합니다. 단순히 코드를 외우는 것이 아니라 원리를 이해하면 변형 문제도 쉽게 해결할 수 있습니다.
코딩테스트는 결국 패턴 인식입니다. 다양한 유형의 문제를 풀다 보면 새로운 문제를 봤을 때 “아, 이건 저번에 풀었던 문제랑 비슷한데?”라는 직관이 생깁니다. 꾸준히 연습하면 누구나 합격할 수 있습니다. 화이팅!
Did you find this helpful?
☕ Buy me a coffee
Leave a Reply