본문 바로가기

iOS/알고리즘

[Swift : 알고리즘] 순열과 조합 (Permutation and Combination)

300x250

코딩 테스트에서 빈번하게 사용되는 순열과 조합 알고리즘에 대해 공부해보자.

직접적으로 몇 개의 조합을 찾아라!라는 문제보다는 순열 혹은 조합을 적절히 사용해서 경우의 수를 찾는 문제들이 빈번히 출제되므로 대비해두는 것이 좋다.


1. 순열 (Permutation)

'서로 다른 n 개의 원소 중에서 r 개를 골라 순서를 구분하여 나열한 경우의 수'를 의미한다.

순열은 nPr 로 표현한다.

순열의 개수를 구하는 공식은  n! / (n-r)! 이다.

순열에서 중요한 점은 순서를 구분한다는 점이다.

 

예를 들어 [1, 2, 3, 4]의 배열에서 순서를 구분하여 3가지 원소를 경우의 수를 나열하면 아래와 같이 24개의 경우의 수가 나온다.

[1, 2, 3]
[1, 2, 4]
[1, 3, 2] //[1, 2, 3] 과 같은 값들이 들어갔지만 순서가 다르므로 다른 경우의 수로 본다.
[1, 3, 4]
[1, 4, 2]
[1, 4, 3]

[2, 1, 3]
[2, 1, 4]
[2, 3, 1]
[2, 3, 4]
[2, 4, 1]
[2, 4, 3]

[3, 1, 2]
[3, 1, 4]
[3, 2, 1]
[3, 2, 4]
[3, 4, 1]
[3, 4, 2]

[4, 1, 2]
[4, 1, 3]
[4, 2, 1]
[4, 2, 3]
[4, 3, 1]
[4, 3, 2]

 

이를 구현하는 가장 간단한 방법은 for loop를 r번 중첩하여 경우의 수를 구하는 방법이다.

import Foundation

let numberArray: [Int] = [1, 2, 3, 4]

func permutationWithForLoop(array: [Int]) {
    var permutaionArray: [[Int]] = []
    for i in 0..<array.count {
        for j in 0..<array.count {
            for k in 0..<array.count {
                if i==j || j == k || k == i { //중복 제거
                    continue
                }
                //순열 어레이에 i, j, k 번째의 값을 append 시킨다.
                permutaionArray.append([array[i], array[j], array[k]])
            }
        }
    }
    print("4개의 값에서 3개를 고르는 순열의 수: \(permutaionArray.count)개") //24개
    print(permutaionArray)
    /*
    [[1, 2, 3], [1, 2, 4], [1, 3, 2], [1, 3, 4], [1, 4, 2], [1, 4, 3], 
    [2, 1, 3], [2, 1, 4], [2, 3, 1], [2, 3, 4], [2, 4, 1], [2, 4, 3], 
    [3, 1, 2], [3, 1, 4], [3, 2, 1], [3, 2, 4], [3, 4, 1], [3, 4, 2], 
    [4, 1, 2], [4, 1, 3], [4, 2, 1], [4, 2, 3], [4, 3, 1], [4, 3, 2]]
    */
}

permutationWithForLoop(array: numberArray)

 

for loop를 사용해서 순열을 구할 때 문제점은 무엇일까?

n개중 r 개를 선택할 때 r의 개수만큼 for loop를 중첩해야 한다는 점이다.

따라서 r이 변경될 때마다 for loop의 중첩을 변경해야 하므로 dynamic 하게 r개를 선택하는 용도로 사용하기 어렵다.

 

이런 경우, 재귀함수를 사용해서 r에 대응할 수 있게 순열을 구할 수 있다.

let stringArray: [String] = ["A", "B", "C", "D"]
var permutaionArray: [[String]] = [] //찾은 순열을 저장할 배열
var numberOfPick: Int = 2 //몇개로 구성된 순열을 찾을지

func permutaionWithRecursion(array: [String], pickCount: Int, permutationArray: inout [[String]], index: Int = 0) {
    if index == pickCount {
        permutationArray.append(Array(array[0..<index]))
        return
    }
    
    var arr = array
    
    for i in index..<arr.count {
        arr.swapAt(index, i)
        permutaionWithRecursion(array: arr, pickCount: pickCount, permutationArray: &permutationArray, index: index + 1)
        arr.swapAt(index, i)
    }
}

permutaionWithRecursion(array: stringArray, pickCount: numberOfPick, permutationArray: &permutaionArray)
print("4개의 값에서 2개를 고르는 순열의 수: \(permutaionArray.count)개") //12개
print(permutaionArray)
/*
[["A", "B"], ["A", "C"], ["A", "D"],
 ["B", "A"], ["B", "C"], ["B", "D"],
 ["C", "B"], ["C", "A"], ["C", "D"],
 ["D", "B"], ["D", "C"], ["D", "A"]]
 */

위 알고리즘의 동작방식은 다음과 같다.

1. permutaionWithRecursive 함수에서는 순열을 수행할 배열, 선택할 개수, 찾은 순열을 저장할 배열, 인덱스(default = 0)를 파라미터로 받는다.

* 이때 찾은 순열을 저장할 배열은 파라미터로 전달되지만 값의 변화를 저장시키기 위해 inout을 사용해서 넘깁니다.

2. for loop를 돌며 i번째 원소와 index번째 원소를 swap 후 재귀 호출한다.

3. 호출 이후 기존 배열의 원소 배열이 달라졌으므로 다시 원상태로 되돌린다.

4. index == pickCount, 즉 선택할 개수만큼 재귀 호출되었을 경우 pickCount만큼 원소를 확정하였다는 의미이므로 permutaionArray에 값을 추가한 후 함수를 종료한다.

 

예를 들어 ["A", "B", "C", "D"] 배열이 넘어왔을 때

index 번째 원소인 A를 다른 i번째 원소와 스왑하고 재귀 호출한다.

 

index와 numberOfPick이 같아질 때까지 for loop를 돌며 index번째 원소와 i번째 원소를 바꾼 뒤 재귀 호출한다.

index와 numberOfPick이 같아지면 0번째부터 index번째까지의 원소를 찾은 순열을 저장할 배열에 append 시킨다.


2. 조합 (Combination)

'서로 다른 n 개의 원소 중에서 r 개를 골라 순서를 구분하지 않고 나열한 경우의 수'를 의미한다.

조합은 nCr 로 표현한다.

조합의 개수를 구하는 공식은  n! / r!(n-r)! 이다.

조합에서 중요한 점은 순서를 구분하지 않는다는 점이다.

 

예를 들어 [1, 2, 3, 4]의 배열에서 순서를 구분하지않고 3가지 원소를 경우의 수를 나열하면 아래와 같이 4개의 경우의 수가 나온다.

[1, 2, 3]
[1, 2, 4]
[1, 3, 4]
[2, 3, 4]

 

순열과 마찬가지로 for loop를 r번 중첩하여 구현한 조합의 개수를 찾는 알고리즘은 다음과 같다.

let numberArray: [Int] = [1, 2, 3, 4]

func combinationWithForLoop(array: [Int]) -> [[Int]] {
    var combinationArray: [[Int]] = []
    
    for i in 0..<array.count {
        for j in (i+1)..<array.count {
            for k in (j+1)..<array.count {
                combinationArray.append([array[i], array[j], array[k]])
            }
        }
    }
    return combinationArray
}
print("4개의 값에서 3개를 고르는 조합의 수 : \(combinationWithForLoop(array: numberArray).count)")
print(combinationWithForLoop(array: numberArray))
/*
 [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]
 */

순열과 다른점은 for loop를 중첩시킬 때 반복문을 0에서 시작하는 것이 아닌 이전 for loop의 index + 1 에서 시작하는 점이다.

이미 이전 루프에서 뽑혀진 index는 더 이상 고려대상이 아니므로 index 이후의 경우만 생각하면 된다.

 

for loop를 사용할 경우 순열에서 for loop를 사용했을 때 문제점과 마찬가지로 dynamic하게 변하는 r에 대응할 수 없다는 문제점이 있다.

 

순열과 마찬가지로 재귀함수를 사용해서 r에 대응할 수 있게 조합을 구할 수 있다.

let intArray: [Int] = [1, 2, 3, 4]
var combinationArray: [[Int]] = []
let numberOfPick: Int = 2

func combinationWithRecursion(array: [Int], pickCount: Int, index: Int = 0, tempArray: [Int], combsArray: inout [[Int]]) -> [[Int]] {
    if tempArray.count == pickCount {
        combsArray.append(tempArray)
        return []
    }

    for i in index..<array.count {
        combinationWithRecursion(array: array, pickCount: pickCount, index: i + 1, tempArray: tempArray + [array[i]], combsArray: &combsArray)
    }

    return combsArray
}

combinationWithRecursion(array: intArray, pickCount: numberOfPick, tempArray: [], combsArray: &combinationArray)
print("4개의 값에서 2개를 고르는 조합의 수: \(combinationArray.count)개") //6개
print(combinationArray)
/*
 [[1, 2], [1, 3], [1, 4],
 [2, 3], [2, 4],
 [3, 4]]
 */

위 알고리즘의 동작방식은 다음과 같다.

1. combinationWithRecursion 함수에서는 조합을 수행할 배열, 선택할 개수, 인덱스(default = 0), 임시로 조합을 저장할 배열, 찾은 조합을 저장할 배열을 파라미터로 받는다.

* 이때 찾은 조합을 저장할 배열은 파라미터로 전달되지만 값의 변화를 저장시키기 위해 inout을 사용해서 넘깁니다.

2. index부터 for loop 를 돌며 조합을 탐색한다.

* 순열과 달리 순서를 구분하지 않기 때문에 한번 사용된 조합은 다시 사용되지 않으므로 다시 방문하지 않기 위해 재귀 호출 시 index +1 을 넣어준다.

3. tempArray.count == pickCount, 즉 선택할 개수만큼 재귀 호출되었을 경우 pickCount만큼 원소를 확정하였다는 의미이므로 체combinationArray에 값을 추가한 후 함수를 종료한다.

 

예를 들어, [1, 2, 3, 4] 배열이 넘어왔을 때

index 번째 부터 for loop를 돌며 재귀 호출한다.

루프를 돌며 루프마다 각 1, 2, 3, 4의 원소값이 tempArray에 append 된다.

 

index와 pickCount가 같아질 때까지 index에 i +1 을 넣어주면서 targetArray의 count (위의 경우에는 4) 번째 까지 for loop를 돌며 재귀호출하고, tempArray에 값을 append 한다.

tempArray의 count와 pickCount가 같아지면 tempArray 찾은 조합을 저장할 배열에 append 시킨다.

 

 

 

 

☞ 순열(Permutaion)알고리즘 예제 소스코드 github

☞ 조합(Combination)알고리즘 예제 소스코드 github

320x100

'iOS > 알고리즘' 카테고리의 다른 글

[Swift : 알고리즘] 선택정렬(Selection Sort)  (0) 2020.09.03