순열과 조합 구현 (Permutation and Combination)

한눈에 보기

순서를 고려하는 순열과 무시하는 조합의 차이를 백트래킹 구현으로 설명한다. 시작 인덱스로 중복 셈을 막는 원리, 경우의 수와 복잡도, 중복 처리 요령을 정리한다.

최종 업데이트 2026.09.17

순열(Permutation)은 순서를 구분해서 뽑는 것, 조합(Combination)은 순서를 무시하고 뽑는 것이다. 완전 탐색 문제에서 "가능한 모든 경우"를 만들어야 할 때 둘 중 무엇인지 판단하는 것이 첫 단추다.

파이썬은 itertools가 다 해주지만, 면접에서는 직접 구현할 수 있어야 하고 코딩테스트에서도 가지치기(pruning)를 넣으려면 직접 짜야 한다. 이 글은 라이브러리 사용법과 백트래킹 구현을 함께 다룬다.

순열과 조합의 차이

순열과 조합: A B C 중 두 개를 뽑을 때 순열은 여섯 가지, 조합은 세 가지가 되는 비교
"팀장과 부팀장을 뽑는다"면 순열(누가 팀장인지가 다르다), "대표 2명을 뽑는다"면 조합(순서가 의미 없다)이다. 문제의 문장에서 순서가 결과를 바꾸는지 먼저 확인한다.
구분순열 nPr조합 nCr
순서구분한다무시한다
개수n! / (n−r)!n! / (r! × (n−r)!)
3개 중 2개6가지3가지
구현 차이이미 쓴 원소만 제외현재 인덱스 뒤에서만 고른다

백트래킹으로 만들어 나가기

직접 구현할 때는 하나씩 골라 넣고, 끝까지 가보고, 다시 빼내는 방식을 쓴다. 이것이 백트래킹이다. 트리를 그려보면 무슨 일이 일어나는지 한눈에 보인다.

순열과 조합: 1 2 3의 모든 순열을 백트래킹으로 만들어 나가는 결정 트리
깊이가 뽑을 개수 r, 잎사귀가 완성된 순열이다. 조합이라면 이 트리에서 "이미 지나온 인덱스는 다시 안 본다"는 규칙을 넣어 절반 이상의 가지가 잘려 나간다.

라이브러리로 하기

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개
순열OXpermutations6
조합XXcombinations3
중복 순열OOproduct9
중복 조합XOcombinations_with_replacement6

직접 구현하기

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 정도
순열 nPrn!/(n−r)!r이 작으면 훨씬 여유
조합 nCrn!/(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의 범위는 어느 정도인가.
알고리즘 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기