[두서없음 주의] 고민의 흐름대로 작성했어요
[스압 주의] 내용이 정말 길어요: 화면 우측의 목차를 이용해주세요!
배열 속 세 정수의 합이 0인 모든 경우의 수 (Github)
https://school.programmers.co.kr/learn/courses/30/lessons/147355 |
---|
import Foundation
func solution(_ number:[Int]) -> Int {
return 0
}
처음에 짠 코드는 이렇다:
import Foundation
func solution(_ numbers: [Int]) -> Int {
guard (3...13).contains(numbers.count), numbers.allSatisfy({ (-1000...1000).contains($0) }) else {
print("3 ≤ 입력값의 길이 ≤ 13, -1,000 ≤ 입력값의 각 원소 ≤ 1,000 이어야 합니다.")
return -1
}
var possibleCaseCount = 0
for indexA in numbers.startIndex...numbers.endIndex {
if indexA + 3 <= numbers.count {
let numberA = numbers[indexA]
for indexB in (indexA + 1)...numbers.endIndex {
if indexB + 2 <= numbers.count {
let numberB = numbers[indexB]
for indexC in (indexB + 1)...numbers.endIndex {
if indexC + 1 <= numbers.count {
let numberC = numbers[indexC]
if numberA + numberB + numberC == 0 {
possibleCaseCount += 1
}
}
}
}
}
}
}
return possibleCaseCount
}
이대로 답안을 제출하고 끝내도 되지만..
보다시피 상당히 복잡해보이고, 가독성이 매우 나쁘다.
이 방향성을 유지한다면, 개선할 수 있는 점은 크게 세 가지로 보인다:
numbers[indexA] + numbers[indexB] + numbers[indexC]
이렇게 더해준다.for
문과 if
문 구조를 간략화한다.1번은 문장 안에 썼듯이 간단하게 해결 가능하다.
2번은..
for indexA in numbers.startIndex...number.endIndex {
if indexA + 3 <= numbers.count {
// ...
}
}
이런 식으로 반복되니까 for
문과 if
문을 함께 묶어줄 수 있겠다.
indexA + 3 <= numbers.count
를 치환하면 indexA <= numbers.count - 3
이지 않은가?
그러니 밑의 코드에서 ???에 들어갈 수만 찾으면 될 것 같다.
for indexA in numbers.startIndex...(number.endIndex - ???)
그런데 endIndex
와 count
는 정확히 어떻게 다른걸까?
당연히 Index는 0부터 시작하니까, endIndex + 1 = count
인 줄 알았다.
하지만 찾아보니 endIndex
는 마지막 요소의 인덱스 + 1
이었다.
Swift의 컬렉션 설계 철학과 범위 연산의 일관성 때문이라고 한다.
이를 바탕으로, 1번 내용도 포함하여 for
문과 if
문을 간략하게 정리해보았다.
var possibleCaseCount = 0
for i in 0...numbers.endIndex - 3 {
for j in i+1...numbers.endIndex - 2 {
for k in j+1...numbers.endIndex - 1 {
if numbers[i] + numbers[j] + numbers[k] == 0 {
possibleCaseCount += 1
}
}
}
}
return possibleCaseCount
3번 내용을 적용하지 않은 지금도 썩 나쁘지 않지만, 삼중 루프가 그대로 nested되어 재사용성이 낮다. 도전하는 마음으로 재귀함수를 구현해보기로 했다.
재귀함수(recursive function)를 짜는 것은 처음이라 덜컥 겁부터 났지만, "while
문을 사용할 때 조심하는 것이랑 뭐가 다르겠어" 하구 용기를 가졌다 >:D
var possibleCaseCount = 0
func recursive(_ index: Int, _ current: [Int] ) {
// 종료 조건: 3개의 숫자를 선택한 경우
if current.count == 3 {
if current.reduce(0, +) == 0 {
possibleCaseCount += 1
}
return
}
// 재귀 단계; 다음 숫자를 선택
for i in index..<numbers.count {
recursive(i + 1, current + [numbers[i]])
}
}
recursive(0, [])
return possibleCaseCount
위 재귀함수에서 Swift 컴파일러는 아래와 같은 과정을 거친다:
각 단계에서 current + [numbers[i]]
를 통해 current
배열에 숫자가 하나씩 추가되고, current
배열의 숫자가 3개가 되면 합을 계산한 후 새로운 조합을 위해 return한다. 각 깊이(첫번째 숫자, 두번째 숫자, 세번째 숫자)에서 return을 해도 아직 바깥 단계의 for
문 안에 있기 때문에 단계적으로 반복된다. 만약 배열의 숫자가 3개가 아닌데 다음으로 추가할 숫자가 없다면, 그보다 바깥에 있는 for
루프에 의해 다음 경우로 넘어가게 된다. 탐색하는 순서를 예시를 통해 시각화하면 아래와 같다.
current = [-2]
(index 0)current = [-2, 3]
(index 0 → 1)current = [-2, 3, 0]
→ 합: 1
(index 0 → 1 → 2)current = [-2, 3, 2]
→ 합: 3
(index 0 → 1 → 3)current = [-2, 3, -5]
→ 합: -4
(index 0 → 1 → 4)current = [-2, 0]
(index 0 → 2)current = [-2, 0, 2]
→ 합: 0
(유효 케이스) (index 0 → 2 → 3)current = [-2, 0, -5]
→ 합: -7
(index 0 → 2 → 4)current = [-2, 2]
(index 0 → 3)current = [-2, 2, -5]
→ 합: -5
(index 0 → 3 → 4)current = [-2, -5]
(index 0 → 4)current = [3]
(index 1)current = [3, 0]
(index 1 → 2)current = [3, 0, 2]
→ 합: 5
(index 1 → 2 → 3)current = [3, 0, -5]
→ 합: -2
(index 1 → 2 → 4)current = [3, 2]
(index 1 → 3)current = [3, 2, -5]
→ 합: 0
(유효 케이스) (index 1 → 3 → 4)current = [3, -5]
(index 1 → 4) current = [0]
(index 2)current = [0, 2]
(index 2 → 3)current = [0, 2, -5]
→ 합: -3
(index 2 → 3 → 4)current = [0, -5]
(index 2 → 4)current = [2]
(index 3)current = [2, -5]
(index 3 → 4)current = [-5]
(index 4)조잘조잘:
for
루프 구조 안에 for
루프 구조가 있다보니 헷갈리는데, 위 경우의 수로부터 예를 들어 current = [-2, -5] (index 0 → 4)
으로부터 current = [3] (index 1)
에 넘어가는 경우를 보자.(index 0 → 1~4)
루프 구조 안에는 (index 0 → 4)
루프 실행이 있다. 그런데 (index 0 → 4)
루프 실행 안에 있는 세번째 숫자를 채택하는 루프 구조는 for i in 5..<5
가 되고, 이는 빈 범위이기에 실행되지 않고 종료된다. 또한, 이 함수는 안에 있는 루프 구조가 끝나면 생명 주기가 끝나는 함수이다. 따라서 (index 0 → 4)
루프 실행은 끝난다. (index 0 → 1~4)
에서 마지막 실행이었던 (index 0 → 4)
루프 실행이 종료되어버렸으니, 마찬가지로 (index 0)
안의 루프 구조(index 0 → 1~4)
도 종료된다. 그러면 (index 0)
루프 실행이 끝이 난다.(index 0)
루프 실행마저도 (index 0~4)
루프 구조 안에 있는 하나의 루프 실행이다. (index 0)
루프 실행이 종료되면 다음 루프 실행은? (index 1)
루프 실행이다. 이런 식으로 반복된다..재귀함수를 열심히 이해해서 만든 건 좋다. 근데 한 가지 마음에 걸리는 게 있다.
단순한for
문 반복에서는 각 범위에서 제한을 주니 필요없는 조합들이 발생하지 않는다.
그런데 재귀함수를 사용했더니 생각하지 않아도 될 경우의 수들을 훑게 된다: (index 0 → 4), (index 1 → 4), (index 2 → 4), (index 3), (index 4)
그뿐만이 아니고 current
라는 배열은 재귀함수 스택에서 매번 만들어지니 메모리 낭비가 이루어질 것 같다.
var possibleCaseCount = 0
var combination = [Int](repeating: 0, count: 3) // 중복된 배열 생성 방지
func recursive(_ depth: Int, _ start: Int) {
if depth == 3 { // 종료 조건
if combination.reduce(0, +) == 0 {
possibleCaseCount += 1
}
return
}
for index in start..<numbers.count { // 재귀 단계
combination[depth] = numbers[index]
recursive(depth + 1, index + 1)
}
}
recursive(0, 0)
return possibleCaseCount
combination
이란 배열을 미리 선언하고,depth
매개변수로 재귀 깊이를 추적하게 하였다. depth
가 3이 되는 것은 combination[0]
, combination[1]
, combination[2]
모두 값이 변경되었음을 의미하기에, 이 시점에서 합을 계산한 후 return
하게 된다. 따라서 depth
가 3보다 작은 경우에만 루프가 실행되며, 3에 도달할 시 더 이상 combination[depth]
에 접근하지 않는다. return
한 후에는 재귀 호출의 이전 단계로 돌아간다.
예시로 나타내면 이렇다:
numbers = [1, -2, -1, 0]
combination = [0, 0, 0]
1. recursive(0, 0) 호출 (index 0 → )
combination[0] = 1
2. recursive(1, 1) 호출 (index 0 → 1 → )
combination[1] = -2
3. recursive(2, 2) 호출 (index 0 → 1 → 2 → )
combination[2] = -1
4. recursive(3, 3) 호출 (index 0 → 1 → 2 계산)
depth == 3이므로 return
3. recursive(2, 3) 호출 // 루프의 다음 반복 (index 0 → 1 → )
combination[2] = 0
4. recursive(3, 4) 호출 (index 0 → 1 → 3 계산)
depth == 3이므로 return
// 3. recursive(2, 4) 호출 (index 0 → 1 → ?)
// 빈 범위라서 루프 종료
2. recursive(1, 2) 호출 // 루프의 다음 반복 (index 0 → )
combination[1] = -1
3. recursive(2, 3) 호출 // 루프의 다음 반복 (index 0 → 2 → )
combination[2] = 0
4. recursive(3, 4) 호출 (index 0 → 2 → 3 계산)
depth == 3이므로 return
// 3. recursive(2, 4) 호출 (index 0 → 2 → ?)
// 빈 범위라서 루프 종료
// 2. recursive(1, 3) 호출 (index 0 → 3 → ?)
// 빈 범위라서 루프 종료
1. recursive(0, 1) 호출 (index 1 → )
combination[0] = -2
2. recursive(1, 2) 호출 (index 1 → 2 → )
combination[1] = -1
3. recursive(2, 3) 호출 (index 1 → 2 → 3 → )
combination[2] = 0
4. recursive(3, 4) 호출 (index 1 → 2 → 3 계산)
depth == 3이므로 return
// 3. recursive(2, 4) 호출 (index 1 → 2 → ?)
// 빈 범위라서 루프 종료
// 2. recursive(1, 3) 호출 (index 1 → 3 → ?)
// 빈 범위라서 루프 종료
1. recursive(0, 2) 호출 (index 2 → )
combination[0] = -1
2. recursive(1, 3) 호출 (index 2 → 3 → )
combination[1] = 0
3. recursive(2, 4) 호출 (index 2 → 3 → ?)
// 빈 범위라서 루프 종료
// 2. recursive(1, 4) 호출 (index 2 → 3 → ?)
// 빈 범위라서 루프 종료
1. recursive(0, 3) (index 3 → )
combination[0] = 0
2. recursive(1, 4) 호출 (index 3 → ?)
// 빈 범위라서 루프 종료
// 모든 루프가 끝남
유효하지 않은 범위는 위에서 표시한 것처럼 호출이 되어도 바로 루프가 종료되어 뛰어넘게 된다.
로직 자체를 얘기할 때는 이 단계들을 일일히 설명하는 것이 비효율적이지만, 이렇게 직접 표시해보면 깊이를 단계단계 바꿔가며 루프구조를 통해 탐색하는 것을 알 수 있다! 마치 파일 탐색기로 폴더를 탐색하는 것 같다.
// 종료 조건
if start >= numbers.count { // 빈 범위 방지 조건
return
}
// 재귀 단계
가운데에 빈 범위 방지 조건을 넣음으로써 필요없는 범위에 대해 함수 호출을 막고 싶었다. 하지만 자세히 들여다보니 무의미하다고 판단했다. 왜냐하면 결국엔 for
문의 범위 연산에서 해당 조건은 걸러지게 되고, if
문과 for
문의 연산 비용은 별다를게 없기 때문이다. 그나마 가독성이나 논리 명확성은 높일 수 있을 것 같다.
대신에 단순히 start
를 비교하는 것보다, depth
에 따라 함께 비교하면 필요없는 경우의 수를 더 넘길 수 있을 것 같다.
이마저도 입력값이 작다면 큰 의미가 없는 최적화지만, 없는 것보단 좋을 것 같다.
// 종료 조건
if depth == 1 && start >= numbers.count - 1 {
// depth 1에서 남은 요소가 충분하지 않으면 조합 생성 불가
return
}
// 재귀 단계
import Foundation
func solution(_ numbers: [Int]) -> Int {
guard (3...13).contains(numbers.count), numbers.allSatisfy({ (-1000...1000).contains($0) }) else {
print("3 ≤ 입력값의 길이 ≤ 13, -1,000 ≤ 입력값의 각 원소 ≤ 1,000 이어야 합니다.")
return -1
}
var possibleCaseCount = 0
var combination = [Int](repeating: 0, count: 3) // 중복된 배열 생성 방지
func recursive(_ depth: Int, _ start: Int) {
if depth == 3 { // 종료 조건
if combination.reduce(0, +) == 0 {
possibleCaseCount += 1
}
return
}
// depth 1에서 남은 요소가 충분하지 않으면 건너뜀
if depth == 1 && start >= numbers.count - 1 {
return
}
for index in start..<numbers.count { // 재귀 단계
combination[depth] = numbers[index]
recursive(depth + 1, index + 1)
}
}
recursive(0, 0)
return possibleCaseCount
}
endIndex
는count
와 그 값이 같지만 쓰임새가 전혀 다르며, 여기엔 Swift의 철학도 내재되어있다.실제로 성능에 차이가 많이 난다 |
---|
endIndex
는 마지막 요소의 인덱스 + 1
로 정의되어있다. 따라서 count
와 같은 값을 갖는다.
Swift에서는 반열림 범위 (..<
)를 자주 사용한다. 이 범위는 시작점은 포함하되, 끝점은 포함하지 않는 방식이다. 이를 통해 코드를 더 읽기 쉽게 만들고, 에러를 방지할 수 있다.
let array = [10, 20, 30, 40]
for i in array.startIndex..<array.endIndex {
print(array[i])
}
여기서 startIndex..<endIndex
는 0..<4
로 해석되어 모든 유효한 인덱스를 포함한다.
endIndex
가 배열의 유효한 마지막 인덱스를 초과하는 값을 가지도록 설계된 이유는 범위 기반 접근에서 초과 오류를 방지하기 위해서이다.
let array = [10, 20, 30, 40]
let slice = array[1..<array.endIndex] // [20, 30, 40]
이렇게 하면 마지막 요소까지 포함하는 범위를 안전하게 지정할 수 있다. 만약 endIndex
가 마지막 유효 인덱스였더라면, 범위를 설정할 때 1..<array.count
와 같은 별도 처리가 필요했을 것이다.
전통적인 C 스타일 루프에서는 범위를 i = 0; i < count; i++
로 지정하곤 했다. Swift는 이를 보다 명시적이고 안전하게 표현하기 위해 endIndex
를 포함하여 범위의 상한을 유효 범위 바깥 값으로 설정한 것이다.
endIndex
는 유효한 마지막 인덱스의 다음 위치를 가리킨다...<
)를 일관되게 사용하고, 안전한 배열 접근을 보장하기 위한 설계이다.count
와 endIndex
가 같은 값을 가지면서도 따로 제공되는 이유는, 각각의 역할이 다르고, 특정 상황에서 더 명확하고 안전하게 코드를 작성할 수 있도록 하기 위함이다.
count
와 endIndex
가 구분되지 않았을 때의 문제만약 count
만 제공된다면, 다음과 같은 문제 상황이 발생할 수 있다.
let array = [10, 20, 30, 40]
// 마지막 요소까지 포함하려는 범위:
let slice = array[0..<array.count] // [10, 20, 30, 40]
// 반복문
for i in 0..<array.count {
print(array[i]) // 출력: 10, 20, 30, 40
}
위 코드는 잘 작동하지만, 이때 count
의 의미는 "마지막 인덱스 + 1"과 유사하게 해석된다. 범위를 설정하는 코드와 요소 개수를 확인하는 코드가 개념적으로 다른 역할을 수행하는데, 둘이 같은 이름이라면 혼란이 생길 수 있다.
만약 count
가 단순히 유효한 마지막 인덱스를 가리켰다면, 다음과 같은 범위 연산이 안전하지 않을 수 있다:
for i in 0...array.count - 1 { // 만약 실수로 `...`을 사용하면?
print(array[i]) // 마지막 인덱스 초과로 런타임 에러 발생
}
이런 경우를 막기 위해 Swift는 endIndex
라는 별도의 속성을 제공하여 범위의 끝을 명확히 정의한다.
Array
외에도 Swift의 컬렉션 타입은 매우 다양하다. 특히 Slice
, String
, Dictionary
등의 경우, 인덱스와 요소 개수가 동일한 의미를 갖지 않을 수 있다. 이러한 상황에서 endIndex
와 count
의 분리가 더욱 중요해진다.
String
에서 Character
접근let text = "Swift"
print(text.count) // 5 (문자 개수)
print(text.endIndex) // Index(5) (유효한 범위의 끝)
for index in text.startIndex..<text.endIndex {
print(text[index]) // S, w, i, f, t
}
count
는 문자열의 문자 개수(5)를 나타낸다.endIndex
는 마지막 문자 다음 위치를 나타낸다.startIndex..<endIndex
로 설정하여 모든 문자를 안전하게 반복할 수 있다.Slice
Slice
는 원래 배열의 일부를 잘라낸 서브컬렉션이다. 이 경우 인덱스가 원래 배열의 인덱스 범위를 유지하기 때문에, count
와 endIndex
가 다르게 작동한다.
let array = [10, 20, 30, 40, 50]
let slice = array[1..<4] // [20, 30, 40]
print(slice.count) // 3 (요소 개수)
print(slice.endIndex) // 4 (원래 배열의 인덱스 기준)
slice.count
는 슬라이스된 요소의 개수 (3개)이다.slice.endIndex
는 원래 배열의 인덱스 기준으로 마지막 요소 이후의 위치이다.Swift는 안전성을 중시한다. 따라서:
count
는 요소의 개수라는 개념을 명확히 전달합니다.endIndex
는 범위 끝을 설정할 때 사용하는 안전한 값으로 설계되었다.이런 분리 덕분에, 컬렉션의 크기와 유효 인덱스 범위를 각각 명확히 구분할 수 있다.
count
는 요소의 개수를 나타내며, 크기를 파악할 때 사용된다.endIndex
는 마지막 유효 인덱스 다음 위치를 나타내며, 반복 및 범위 연산에서 안전성을 제공한다.startIndex
가 항상 0인 것은 아니다.
Swift에서 startIndex
는 컬렉션(예: 배열, 문자열, 슬라이스 등)의 첫 번째 유효 인덱스를 나타내며, 0일 수도 있고 아닐 수도 있다. 특정 컬렉션 타입과 상황에 따라 다르게 동작합니다.
배열의 경우, startIndex
는 항상 0이다.
let array = [10, 20, 30]
print(array.startIndex) // 출력: 0
문자열의 경우, startIndex
는 항상 0이 아니다. Swift의 문자열은 유니코드 스칼라를 기반으로 동작하며, 인덱스는 단순한 정수가 아니라 문자열 내 특정 위치를 나타내는 구조이다.
let text = "Swift"
print(text.startIndex) // 문자열의 첫 번째 위치 (0처럼 보이지만 실제로는 Character의 위치)
문자열에서는 startIndex
가 유니코드 스칼라에 따라 위치를 정의하므로, 0처럼 보이지만 내부적으로는 다르게 동작할 수 있다.
슬라이스(Slice)는 기존 컬렉션의 부분 집합을 나타낸다. startIndex
는 원래 컬렉션의 인덱스를 유지하기 때문에, 0이 아닐 수 있다.
let array = [10, 20, 30, 40, 50]
let slice = array[1..<4] // [20, 30, 40]
print(slice.startIndex) // 출력: 1 (원래 배열의 인덱스 유지)
print(slice[1]) // 20
슬라이스는 원래 컬렉션의 인덱스를 유지하므로, startIndex
는 슬라이스의 첫 번째 요소가 원래 배열에서 가리키던 위치를 나타낸다.
딕셔너리와 집합의 경우, 인덱스는 요소의 추가 순서에 따라 달라질 수 있으며 0이 아니다.
let dict = ["a": 1, "b": 2, "c": 3]
print(dict.startIndex) // 0처럼 보이지 않음. Dictionary.Index 타입 값
딕셔너리의 startIndex
는 내부 구현에 따라 첫 번째 요소의 인덱스를 가리키며, 일반적인 정수 기반 인덱스와는 다르다.
startIndex
는 항상 0.startIndex
는 첫 번째 문자의 위치로, 0처럼 보이지만 내부적으로는 다름.startIndex
는 원래 컬렉션의 인덱스를 유지.startIndex
는 정수형 0이 아니며, 내부 구조에 따라 달라짐.Swift의 startIndex
는 데이터 타입과 상황에 따라 달라질 수 있도록 설계되었습니다. 이를 통해 더 유연하고 안전한 범위 연산을 제공한다.
Swift에서 반열림 범위(..<
)를 자주 사용하는 이유는 안전성, 일관성, 그리고 코드 가독성을 높이기 위해서이다. 이 방식은 범위 기반 반복과 인덱스 접근을 보다 명확하고 오류 없이 수행할 수 있도록 돕는다.
반열림 범위는 Swift에서 배열, 문자열, 슬라이스 등 다양한 컬렉션 타입과 함께 일관되게 동작한다. 이 일관성은 개발자가 코드를 예측 가능하게 작성하는 데 큰 도움을 준다.
let array = [10, 20, 30, 40]
let slice = array[1..<3] // [20, 30]
let text = "Swift"
let substring = text[text.startIndex..<text.index(text.startIndex, offsetBy: 3)]
print(substring) // Swi
반열림 범위는 마지막 인덱스를 포함하지 않으므로 반복문의 직관성을 높여준다. 일반적인 C 스타일 루프와 비슷한 동작을 하기 때문에 직관적이다:
// C 스타일 루프
for (int i = 0; i < n; i++) {
// ...
}
// Swift의 for-in 루프
for i in 0..<n {
// ...
}
i < n
조건을 만족하므로, 루프 범위를 초과하지 않는 안전한 반복이 가능하다.반열림 범위는 종료 조건 변경이 쉬워서, 유연하게 코드 작성이 가능하다.
let array = [10, 20, 30, 40, 50]
// 첫 3개의 요소만 출력
for i in 0..<3 {
print(array[i]) // 10, 20, 30
}
// 마지막 3개의 요소만 출력
for i in 2..<array.count {
print(array[i]) // 30, 40, 50
}
반열림 범위를 사용하면 종료 지점만 쉽게 조정하여 원하는 서브 범위를 추출할 수 있다.
Swift는 안전성과 간결성을 핵심 설계 목표로 한다. 반열림 범위는:
-1
또는 +1
계산 없이 간결하게 범위를 설정할 수 있도록 한다...<
)는 안전성과 일관성을 제공한다.재귀함수는 자기 자신을 호출하는 함수이다.
재귀함수는 종료 조건(Base Case)과 재귀 단계(Recursive Case)로 구성된다.
func recursiveFunction(_ parameter: Int) {
// 종료 조건(Base Case)
if parameter <= 0 {
return
}
print("재귀 호출: \(parameter)")
// 재귀 단계(Recursive Case)
recursiveFunction(parameter - 1)
}
parameter
값을 감소시켜 종료 조건에 가까워지도록 만든다.팩토리얼 n!
은 n * (n-1) * (n-2) * ... * 1
로 정의된다. 팩토리얼은 재귀적으로 계산할 수 있다.
func factorial(_ n: Int) -> Int {
if n == 0 {
return 1 // 종료 조건: 0! = 1
}
return n * factorial(n - 1) // 재귀 단계
}
// 사용 예시
print(factorial(5)) // 출력: 120 (5 * 4 * 3 * 2 * 1)
피보나치 수열의 각 항은 앞의 두 항의 합으로 정의된다.
수열: 0, 1, 1, 2, 3, 5, 8, 13, ...
func fibonacci(_ n: Int) -> Int {
if n == 0 {
return 0 // 종료 조건
} else if n == 1 {
return 1 // 종료 조건
}
return fibonacci(n - 1) + fibonacci(n - 2) // 재귀 단계
}
// 사용 예시
print(fibonacci(7)) // 출력: 13
재귀함수는 계층적 데이터 구조(예: 디렉토리, 트리) 탐색에 유용하다.
func exploreDirectory(_ directory: String) {
print("Exploring \(directory)")
// 가정: 하위 디렉토리 목록 가져오기 (가상 함수)
let subdirectories = getSubdirectories(of: directory)
for subdirectory in subdirectories {
exploreDirectory(subdirectory) // 하위 디렉토리 탐색
}
}
// 가상 데이터
func getSubdirectories(of directory: String) -> [String] {
return ["subdir1", "subdir2"] // 가상의 하위 디렉토리 목록
}
// 사용 예시
exploreDirectory("/root")
종료 조건을 반드시 정의해야 한다.
재귀 호출의 깊이를 고려해야 한다.
대체 알고리즘(반복문) 고려
재귀함수는 이러한 문제들을 해결할 때 직관적이고 간결한 코드를 작성하는 데 매우 유용하다.
append
메서드배열의 끝에 단일 요소를 추가한다.
var array = [1, 2, 3]
array.append(4)
print(array) // 출력: [1, 2, 3, 4]
append(contentsOf:)
메서드다른 배열이나 시퀀스의 모든 요소를 추가한다.
var array = [1, 2]
array.append(contentsOf: [3, 4, 5])
print(array) // 출력: [1, 2, 3, 4, 5]
insert
메서드배열의 특정 위치에 요소를 삽입한다.
var array = [1, 3, 4]
array.insert(2, at: 1) // 인덱스 1 위치에 2 삽입
print(array) // 출력: [1, 2, 3, 4]
insert(contentsOf:at:)
메서드배열의 특정 위치에 다른 배열이나 시퀀스의 모든 요소를 삽입한다.
var array = [1, 4, 5]
array.insert(contentsOf: [2, 3], at: 1) // 인덱스 1 위치에 [2, 3] 삽입
print(array) // 출력: [1, 2, 3, 4, 5]
replaceSubrange
메서드배열의 특정 범위를 대체하거나, 범위에 새 요소를 삽입한다.
var array = [1, 2, 3, 4, 5]
array.replaceSubrange(1...3, with: [10, 20])
print(array) // 출력: [1, 10, 20, 5]
var array = [1, 2, 5, 6]
array.replaceSubrange(2..<2, with: [3, 4]) // 인덱스 2 위치에 [3, 4] 삽입
print(array) // 출력: [1, 2, 3, 4, 5, 6]
var array = [1, 2, 3, 4, 5]
array.replaceSubrange(1...4, with: [10])
print(array) // 출력: [1, 10]
var array = [1, 2, 3, 4, 5]
array.replaceSubrange(2..<2, with: [10, 20])
print(array) // 출력: [1, 2, 10, 20, 3, 4, 5]
+=
연산자배열의 끝에 하나 이상의 요소를 추가할 때 사용한다.
단일 요소 추가 시에도 가능하지만, 그럴 땐 명확성을 위해 append
를 쓰자.
var array = [1, 2, 3]
array += [4, 5]
print(array) // 출력: [1, 2, 3, 4, 5]
+
연산자두 배열을 합쳐서 새로운 배열을 반환한다. 기존 배열은 변경되지 않는다.
let array1 = [1, 2]
let array2 = [3, 4]
let combinedArray = array1 + array2
print(combinedArray) // 출력: [1, 2, 3, 4]
reserveCapacity
와 append
를 조합배열의 용량을 미리 확보하여 요소를 추가할 때 성능을 최적화한다.
var array: [Int] = []
array.reserveCapacity(10) // 용량 미리 확보
array.append(1)
array.append(2)
print(array) // 출력: [1, 2]
메서드/연산자 | 설명 | 예시 | 결과 |
---|---|---|---|
append | 배열의 끝에 단일 요소를 추가 | array.append(4) | [1, 2, 3, 4] |
append(contentsOf:) | 배열이나 시퀀스의 모든 요소를 추가 | array.append(contentsOf: [3, 4, 5]) | [1, 2, 3, 4, 5] |
insert | 배열의 특정 위치에 단일 요소 삽입 | array.insert(2, at: 1) | [1, 2, 3, 4] |
insert(contentsOf:at:) | 배열의 특정 위치에 다른 배열이나 시퀀스의 모든 요소 삽입 | array.insert(contentsOf: [2, 3], at: 1) | [1, 2, 3, 4, 5] |
replaceSubrange | 특정 범위를 대체하거나 삽입 | - array.replaceSubrange(1...3, with: [10, 20]) - array.replaceSubrange(2..<2, with: [3, 4]) | [1, 10, 20, 5] [1, 2, 3, 4, 5, 6] |
+= 연산자 | 배열의 끝에 하나 이상의 요소 추가 | array += [4, 5] | [1, 2, 3, 4, 5] |
+ 연산자 | 두 배열을 합쳐서 새로운 배열 반환 | let combinedArray = array1 + array2 | [1, 2, 3, 4] |
reserveCapacity + append | 용량을 미리 확보한 후 요소 추가로 성능 최적화 | array.reserveCapacity(10) array.append(1) | [1, 2] |
append
사용.append(contentsOf:)
또는 +=
사용.insert
(단일 요소), insert(contentsOf:at:)
(여러 요소).replaceSubrange
사용.reserveCapacity
로 용량 확보 후 추가.
스압주의는 이럴 때 쓰는 거군요