순열과 조합 구현 (Permutation and Combination)
한눈에 보기
순서를 고려하는 순열과 무시하는 조합의 차이를 백트래킹 구현으로 설명한다. 시작 인덱스로 중복 셈을 막는 원리, 경우의 수와 복잡도, 중복 처리 요령을 정리한다.
최종 업데이트 2026.09.17
순열(Permutation)은 순서를 구분해서 뽑는 것, 조합(Combination)은 순서를 무시하고 뽑는 것이다. 완전 탐색 문제에서 "가능한 모든 경우"를 만들어야 할 때 둘 중 무엇인지 판단하는 것이 첫 단추다.
파이썬은 itertools가 다 해주지만, 면접에서는 직접 구현할 수 있어야 하고 코딩테스트에서도 가지치기(pruning)를 넣으려면 직접 짜야 한다. 이 글은 라이브러리 사용법과 백트래킹 구현을 함께 다룬다.
순열과 조합의 차이
| 구분 | 순열 nPr | 조합 nCr |
|---|---|---|
| 순서 | 구분한다 | 무시한다 |
| 개수 | n! / (n−r)! | n! / (r! × (n−r)!) |
| 3개 중 2개 | 6가지 | 3가지 |
| 구현 차이 | 이미 쓴 원소만 제외 | 현재 인덱스 뒤에서만 고른다 |
백트래킹으로 만들어 나가기
직접 구현할 때는 하나씩 골라 넣고, 끝까지 가보고, 다시 빼내는 방식을 쓴다. 이것이 백트래킹이다. 트리를 그려보면 무슨 일이 일어나는지 한눈에 보인다.
라이브러리로 하기
from itertools import permutations, combinations, product, combinations_with_replacement
arr = [1, 2, 3]
list(permutations(arr, 2))
# [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)] 순열 6가지
list(combinations(arr, 2))
# [(1,2), (1,3), (2,3)] 조합 3가지
list(product(arr, repeat=2))
# [(1,1), (1,2), ..., (3,3)] 중복 순열 9가지
list(combinations_with_replacement(arr, 2))
# [(1,1), (1,2), (1,3), (2,2), (2,3), (3,3)] 중복 조합 6가지
네 가지를 표로 정리하면 이렇다.
| 구분 | 순서 구분 | 같은 원소 재사용 | 함수 | 3개 중 2개 |
|---|---|---|---|---|
| 순열 | O | X | permutations | 6 |
| 조합 | X | X | combinations | 3 |
| 중복 순열 | O | O | product | 9 |
| 중복 조합 | X | O | combinations_with_replacement | 6 |
직접 구현하기
def permute(arr, r):
"""순열 — 이미 쓴 원소만 건너뛴다."""
result, path = [], []
used = [False] * len(arr)
def backtrack():
if len(path) == r:
result.append(path[:]) # 반드시 복사본. path는 계속 바뀐다
return
for i in range(len(arr)):
if used[i]:
continue
used[i] = True
path.append(arr[i])
backtrack()
path.pop() # 되돌리기
used[i] = False
backtrack()
return result
def combine(arr, r):
"""조합 — 현재 인덱스 다음부터만 고른다. used 배열이 필요 없다."""
result, path = [], []
def backtrack(start):
if len(path) == r:
result.append(path[:])
return
for i in range(start, len(arr)):
path.append(arr[i])
backtrack(i + 1) # i+1 — 앞으로만 간다
path.pop()
backtrack(0)
return result
두 함수의 차이는 딱 두 군데다. 순열은 used 배열로 "이미 쓴 것"을 막고 매번 range(len(arr)) 전체를 돈다. 조합은 start부터만 돌아서 인덱스가 항상 증가하도록 강제한다. 그러면 (1,2)는 만들어지지만 (2,1)은 애초에 생기지 않는다.
중복을 허용하려면 조합에서 backtrack(i + 1)을 backtrack(i)로 바꾸면 중복 조합이 되고, 순열에서 used 검사를 빼면 중복 순열이 된다.
가지치기 — 직접 구현하는 진짜 이유
itertools는 모든 경우를 다 만들어낸다. 하지만 "합이 10을 넘으면 볼 필요 없는" 문제라면, 다 만들어놓고 거르는 것보다 만드는 도중에 포기하는 편이 압도적으로 빠르다.
def combinations_with_limit(arr, r, limit):
"""합이 limit 이하인 조합만 찾는다."""
result, path = [], []
def backtrack(start, total):
if total > limit: # 여기서 끊으면 아래 가지를 통째로 건너뛴다
return
if len(path) == r:
result.append(path[:])
return
for i in range(start, len(arr)):
path.append(arr[i])
backtrack(i + 1, total + arr[i])
path.pop()
backtrack(0, 0)
return result
시간 복잡도
| 구분 | 경우의 수 | 현실적인 한계 |
|---|---|---|
| 전체 순열 (n!) | n = 10 → 362만 | n ≤ 10 정도 |
| 순열 nPr | n!/(n−r)! | r이 작으면 훨씬 여유 |
| 조합 nCr | n!/(r!(n−r)!) | 순열보다 r! 배 적다 |
| 중복 순열 (nʳ) | n = 5, r = 8 → 39만 | r이 커지면 급증 |
n! 은 무섭게 자란다. 10!은 362만이라 괜찮지만 13!은 62억으로 불가능하다. 문제에서 n이 10 이하로 제한되어 있으면 "전체 순열을 다 만들어봐도 된다"는 신호다.
자주 하는 실수
- 결과에
path를 그대로 넣는다.result.append(path)라고 쓰면 같은 리스트 객체를 참조하므로, 되돌리기가 일어날 때마다 저장된 결과도 함께 바뀐다. 마지막에 보면 전부 빈 리스트다. 반드시path[:]나list(path)로 복사한다. path.pop()을 빼먹는다. 되돌리기가 없으면 다음 가지를 탐색할 때 이전 흔적이 남아 엉뚱한 결과가 나온다.- 조합인데 순열로 푼다. 답의 개수가 r! 배로 부풀어 오답이거나 시간 초과가 난다. "순서가 결과를 바꾸는가"를 먼저 판단한다.
- 조합에서
backtrack(start + 1)로 쓴다.i + 1이어야 한다.start + 1이면 반복문이 도는 동안 다음 시작점이 고정되어 중복이 생긴다. - 중복 원소가 있는 입력을 그냥 돌린다.
[1, 1, 2]의 순열은 같은 결과가 두 번씩 나온다. 정렬한 뒤i > start and arr[i] == arr[i-1]이면 건너뛰는 처리가 필요하다. - n을 보지 않고 전체 순열을 만든다. n이 15만 돼도 1조가 넘는다. 제약 조건을 먼저 읽는다.
면접에서 이렇게 물어본다
- 순열과 조합의 차이를 예를 들어 설명해 보라.
- 백트래킹으로 조합을 구현할 때
start인덱스가 하는 역할은 무엇인가. - 결과에 리스트를 복사해서 넣어야 하는 이유는 무엇인가.
- 중복 원소가 있는 입력에서 중복 결과를 없애려면 어떻게 하겠는가.
- 가지치기가 왜 효과적인지 설명해 보라.
- n! 의 증가 속도를 고려할 때 완전 탐색이 가능한 n의 범위는 어느 정도인가.
알고리즘에서 같이 읽기
- 이분 탐색 (Binary Search)정렬된 배열에서 탐색 범위를 절반씩 줄여 O(log n)에 값을 찾는 이분 탐색의 원리와 동작 과정을 그림으로 정리했다. lower bound와 upper bound, 코딩테스트 단골인 파라메트릭 서치와 회전 정렬 배열, 구현 코드와 자주 하는 실수까지 다룬다.
- 비트마스크 (Bitmask)정수의 각 비트로 집합의 포함 여부를 표현하는 비트마스크 기법을 다룬다. 기본 비트 연산 정리, 부분집합 열거, 외판원 순회 동적 계획법 구현과 주의점을 정리한다.
- DFS와 BFS (Depth First Search, Breadth First Search)그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.
- 다익스트라 알고리즘 (Dijkstra's Algorithm)음수 간선이 없는 그래프에서 한 정점으로부터의 최단 거리를 구하는 다익스트라 알고리즘의 그리디 원리, 우선순위 큐 구현, 복잡도 비교와 한계를 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기