Swift의 sort 메서드는 어떻게 동작하는지, 얼마나 효율적인지 궁금할 때가 있다. 이를 알아보기 위해 대표적인 정렬 알고리즘 몇 가지를 알아보고, swift에서는 어떻게 구현되어 있는지 알아보자!
0. 알아둬야 할 것들
0-1. Stable vs Unstable
- Stable
- 정렬의 결과가 항상 일정(= 기존의 정렬 순서가 유지됨)
- Unstable
- 정렬의 결과가 매번 다를 수 있음(=기존의 정렬순서가 유지되지 않음)
- id: 1 / name: Hue / power: 7777 / date: 2024.04.25 10:00
- id: 2 / name: Jack / power : 8888 / date: 2024.04.25 10:02
- id: 3 / name: Den / power: 9999 / date: 2024.04.25 10:10
- id: 4 / name: Bren / power: 8888 / date: 2024.04.25 11:32
- id: 5 / name: Lon / power: 8888 / date: 2024.04.25 12:34
위 데이터가 있다고 가정했을 때, power을 기준으로 정렬을 한다고 하자. 그럼 어떻게 될까?
- id: 1 / name: Hue / power: 7777 / date: 2024.04.25 10:00
- id: 2 / name: Jack / power : 8888 / date: 2024.04.25 10:02
- id: 4 / name: Bren / power: 8888 / date: 2024.04.25 11:32
- id: 5 / name: Lon / power: 8888 / date: 2024.04.25 12:34
- id: 3 / name: Den / power: 9999 / date: 2024.04.25 10:10
위처럼 같은 값을 가진 데이터일 땐, 기존 순서를 유지한 채 정렬을 한다. 이를 Stable이라 한다. 반면 기존 순서를 유지하지 않고 바뀌는 경우를 Unstanble이라고 알아두면 된다.
0-2. In-Place vs Not In-Place
- In-Place
- 원소의 개수에 비해 충분히 무시할 만한 공간을 더 사용하는 알고리즘
- 기존 데이터의 메모리 공간에서 정렬이 처리됨
- 추가적인 메모리 공간 필요 X (Cache Hit Rate 높음)
- Not In-Place
- 원소의 개수에 비례하여 저장 공간을 더 사용하는 정렬 알고리즘
- 기존 데이터 이외에 별도의 메모리 공간을 할당함
- ex) tmp 변수를 이용해 정렬할 때, 추가적인 메모리를 사용해야 함
- 추가적인 메모리 공간 필요 O (Cache Hit Rate 낮음)
1. 버블정렬
두 개의 인접한 원소를 비교해 순서에 맞지 않으면 서로 교환하는 정렬 방식
1-1. 구현코드
func bubbleSort<T: Comparable>(_ arr: inout [T]) {
let size = arr.count
// i = 정렬된 데이터의 개수
for i in 0..<size {
// j = 데이터 순회 Index
for j in 1..<size-i {
if arr[j-1] > arr[j] {
arr.swapAt(j - 1, j)
}
}
}
}
var scores = [100, 32, 40, 1, 5, 7, 9, 14, 14]
bubbleSort(&scores)
print(scores) // 결과값 [1, 5, 7, 9, 14, 14, 32, 40, 100]
1-2. 특징
시간복잡도: O(N^2)
Stable (기존의 정렬 순서가 유지됨)
In-Place (추가적인 메모리 공간 필요 X)
1-3. 추가적인 키워드
inout
기존 메서드에서 파라미터로 값을 넘겨줄 땐 값을 복사한다.(Call-by-value) 하지만 inout 키워드를 사용하면 주소값을 참조하게 된다.(Call-by-Reference) 따라서 복사해서 값을 넘길 땐 O(N) 시간복잡도가 들지만, 주소값을 참조하는 경우에는 O(1)의 시간복잡도가 필요하다. 그런데 여기서 Swift만의 특징이 있는데, inout 키워드를 사용할 땐, Copy-in Copy-out을 한다고 한다. 풀어서 설명해 보자면 복사해서 값을 가져와서, 값을 처리하고 다시 원본에 복사해서 붙여 넣는 형태라고 한다. 그러나 최적화될 땐, Call-by-Reference 형태로 주소값을 참조한다고 한다.
2. 삽입정렬
두 개의 인접한 원소를 비교해 순서에 맞지 않으면 서로 교환하는 정렬 방식
2-1. 구현코드
func insertionSort<T: Comparable>(_ arr: inout [T]) {
let size = arr.count
// i = 정렬된 데이터 개수(=정렬된 영역의 데이터 개수)
for i in 1..<size {
// j = 데이터 순회 Index
for j in stride(from: i, to: 0, by: -1) {
if arr[j-1] > arr[j] {
arr.swapAt(j - 1, j)
} else{ break }
}
}
}
var scores = [100, 32, 40, 1, 5, 7, 9, 14, 14]
insertionSort(&scores)
print(scores) // 결과 [1, 5, 7, 9, 14, 14, 32, 40, 100]
2-2. 특징
시간복잡도: O(N^2) // 어느 정도 정렬이 되어있는 경우엔 O(N)
Stable (기존의 정렬 순서가 유지됨)
In-Place (추가적인 메모리 공간 필요 X)
2-3. 추가적인 키워드
for j in stride(from: 10, to: 0, by: -1) {
print(j)
}
// 출력 10 9 8 7 6 5 4 3 2 1
for j in stride(from: 10, through: 0, by: -1) {
print(j)
}
// 출력 10 9 8 7 6 5 4 3 2 1 0
여기선 배열의 맨 뒤 index부터 돌아주기 위해 stride라는 메서드를 활용했다. python에서는 사실 stride가 필요 없고 단순히 range(10,0,-1)과 같은 방식으로 구현이 가능하다. 하지만 swift에선 10.. <0 식으로 구현을 하면 런타임에러가 발생한다. 따라서 stride를 활용해야 한다.
3. Merge정렬
데이터들을 분할하고 다시 병합하는 정렬방식
3-1. 구현코드
func mergeSort<T: Comparable>(_ arr: inout [T], _ start: Int, _ end: Int) {
if end == start + 1 { return } // base condition(개수 1)
let mid = (start + end) / 2
mergeSort(&arr, start, mid)
mergeSort(&arr, mid, end)
merge(arr: &arr, start, end)
/// 정렬된 두 배열을 합쳐서 정렬하는 과정
func merge(arr: inout [T], _ start: Int, _ end: Int) {
let mid = (start + end) / 2
var lIndex = start, rIndex = mid
var temp: [T] = []
temp.reserveCapacity(end - start)
for _ in start..<end {
if lIndex == mid {
temp.append(arr[rIndex])
rIndex += 1
} else if rIndex == end {
temp.append(arr[lIndex])
lIndex += 1
} else if arr[lIndex] <= arr[rIndex] { // stable
temp.append(arr[lIndex])
lIndex += 1
} else {
temp.append(arr[rIndex])
rIndex += 1
}
}
for i in start..<end {
arr[i] = temp[i - start]
}
}
}
var scores = [100, 32, 40, 1, 5, 7, 9, 14, 14]
mergeSort(&scores, 0, scores.count)
print(scores) // 결과값 [1, 5, 7, 9, 14, 14, 32, 40, 100]
3-2. 특징
시간복잡도: O(NlogN)
Stable (기존의 정렬 순서가 유지됨)
Not In-Place (추가적인 메모리 공간 필요 O)
3-3. 추가적인 키워드
reserveCapacity는 배열의 들어갈 원소의 개수를 미리 지정해 두는 메서드이다. 예를 들어 10이라고 지정을 한다면 그 배열은 10개의 요소를 가지고 있을 것이다. 만약 이때 append를 한다면 다시 10개가 더 들어가 원소의 개수가 20인 배열이 된다.
4. Quick 정렬
pivot을 기준으로 작거나 같은 값, 큰 값으로 데이터를 재귀적으로 분리하는 정렬방식
4-1. 구현코드
func mergeSort<T: Comparable>(_ arr: inout [T], _ start: Int, _ end: Int) {
if end == start + 1 { return } // base condition(개수 1)
let mid = (start + end) / 2
mergeSort(&arr, start, mid)
mergeSort(&arr, mid, end)
merge(arr: &arr, start, end)
/// 정렬된 두 배열을 합쳐서 정렬하는 과정
func merge(arr: inout [T], _ start: Int, _ end: Int) {
let mid = (start + end) / 2
var lIndex = start, rIndex = mid
var temp: [T] = []
temp.reserveCapacity(end - start)
for _ in start..<end {
if lIndex == mid {
temp.append(arr[rIndex])
rIndex += 1
} else if rIndex == end {
temp.append(arr[lIndex])
lIndex += 1
} else if arr[lIndex] <= arr[rIndex] { // stable
temp.append(arr[lIndex])
lIndex += 1
} else {
temp.append(arr[rIndex])
rIndex += 1
}
}
for i in start..<end {
arr[i] = temp[i - start]
}
}
}
var scores = [100, 32, 40, 1, 5, 7, 9, 14, 14]
mergeSort(&scores, 0, scores.count)
print(scores) // 결과값 [1, 5, 7, 9, 14, 14, 32, 40, 100]
4-2. 특징
시간복잡도: O(NlogN)
Stable (기존의 정렬 순서가 유지되지 않음)
In-Place (추가적인 메모리 공간 필요 X)
4-3. 추가적인 키워드
quickSort는 In-Place로만 구현이 돼야 해당 정렬의 장점을 살릴 수 있다고 한다.(속도의 빠름) 따라서 따로 배열을 만들어서 Not In-Place 하게 구현하면 안 된다.
5. Swift의 sort
2018년도까지 swift의 sort는 Intro Sort 방식을 사용하고 있었다고 한다. 그러나 이후에는 Tim Sort를 사용하고 있다고 밝혔다. (swift github에)
Intro Sort | Tim Sort | |
구성 | Quick / Heap / Insertion | Merge / Insertion |
Time Complexity | O(NlogN) | O(N) ~ O(NlogN) |
6. 정리
Bubble | Insertion | Merge | Quick | |
Time Complexity | O(N^2) | O(N)~O(N^2) | O(NlogN) | O(NlogN) ~ O(N^2) |
Stable | O | O | O | X |
In-Place | O | O | X | O |
'# 개발 > 알고리즘' 카테고리의 다른 글
[알고리즘] Swift - 토마토(백준7576) (0) | 2024.04.21 |
---|---|
[알고리즘] Swift - 카드2(백준2164) (0) | 2024.04.16 |
[알고리즘] Swift - 올바른 괄호(백준9012) (0) | 2024.04.16 |