시간복잡도란 특정 함수나 알고리즘이 얼마나 효율적인지, 특히 입력 크기가 커질 때 성능이 어떻게 변하는지 예측하는데 중요한 요소이다. 실제로 시간을 측정하는 것이 아니라, 입력 크기(N)에 비례하여 연산의 개수가 얼마나 증가하는지를 대략적으로 파악하는 것을 말한다.
빅-O 표기법은 알고리즘의 최악의 경우(Worst-case scenario) 시간 복잡도를 나타낸다.
상수항이나 중요하지 않은 계수는 무시하고, 입력 크기 N이 커질 때 가장 큰 영향을 미치는 향만 남겨서 표기하는 방식이다.
arr[0]for i in arr { ... }
Swift 코드에서 시간 복잡도를 따져볼 땐 다음 원칙들을 적용한다.
할당, 산술 연산, 특정 인덱스 접근 등 대부분의 기본 연산은 상수 시간(O(1))으로 간주한다.
func basicOperation(value: Int) -> Int {
let result = value * 2 + 5 // O(1)
return result
}
반복문은 입력 크기(N)에 따라 반복되는 횟수가 결정되므로, 일반적으로 선형 시간 이상이 나온다.
// O(N)
func sumArray(arr: [Int]) -> Int {
var sum = 0
for element in arr { // N번 반복
sum += element // O(1)
}
return sum
}
// O(N^2)
func printPairs(arr: [Int]) {
for i in arr { // N번 반복
for j in arr { // N번 반복
print("\(i), \(j)") // O(1)
}
}
}
반복문 안에 또 다른 반복문이 있으면 각 반복문의 시간 복잡도를 곱한다.
for i (N번) 안에 for j (N번) => O(N times N) = O(N²)for i (N번) 안에 for j (M번) => O(N times M)재귀 함수의 시간 복잡도는 주로 재귀 호출의 깊이와 각 호출에서 수행되는 연산에 따라 결정된다.
// O(N) - N번 호출
func factorial(n: Int) -> Int {
if n <= 1 {
return 1
}
return n * factorial(n: n - 1)
}
// O(2^N) - Memoization 없는 경우
func fibonacci(n: Int) -> Int {
if n <= 1 {
return n
}
return fibonacci(n: n - 1) + fibonacci(n: n - 2)
}
Swift의 Array, Dictionary, Set 등 컬렉션 타입의 특정 연산은 내부 구현에 따라 시간 복잡도가 달라진다.
Array:arr[index] = O(1)arr.append(element)는 보통 O(1)이지만, 용량 재할당 시 O(N)arr.insert(at:), arr.remove(at:) = O(N)arr.contains(element)는 O(N) (선형 탐색)Dictionary, Set:알고리즘에 여러 부분의 시간 복잡도가 있을 때, 가장 큰 영향을 미치는 향(dominant term)만 남긴다.
func exampleComplexity(arr: [Int]) {
// Part 1: O(N^2)
for i in arr {
for j in arr {
print(i + j)
}
}
// Part 2: O(N)
for k in arr {
print(k)
}
}
// 전체 시간 복잡도는 O(N^2 + N) -> O(N^2)
결론적으로, Swift에서 알고리즘의 시간 복잡도를 계산하는 핵심은 코드 내의 각 연산이 입력 크기 N에 비례하여 몇 번 수행되는지 분석하고, 가장 큰 영향을 미치는 항을 빅-O 표기법으로 나타내는 것이다.
앞으로는 알고리즘을 풀 때 이러한 점도 고려하여 풀며, 특히 반복문과 컬렉션 타입의 연산을 할 때 주의를 해야할 것 같다.