+ 항공대학교 이인복 교수님의 알고리즘 해석 및 설계 과목 내용를 정리한 글입니다.
정렬 문제
=> n개의 서로 다른 수가 주어졌을 때, 이들을 이동하여 점점 커지게(오름차순), 또는 점점 작아지게(내림차순) 만드는 문제
ex) 9, 3, 5, 7 => 3, 5, 7, 9
- 한 문제를 풀기 위해서 여러가지 방법이 가능하다.
- 방법마다 특징이 다르다.
- 기본적인 문제이면서, 실생활에 자주 쓰인다.
- 시간 복잡도, 최적성에 대해서 증명이 쉽다.
정렬할 데이터가 담긴 배열의 각 원소를 O(1) 시간에 접근 가능하다고 가정한다.
(데이터가 메인 메모리에 저장되어 있다고 가정한다.)
버블 정렬
1. 맨 왼쪽 원소부터 바로 이웃한 원소와 비교해 가면서, 큰 수가 오른쪽으로 가도록 교환한다.
2. 맨 끝까지 가면 가장 큰 원소를 찾은 것이므로, 이 과정을 다시 나머지 n-1개 수에 대해서 반복한다.
ex) 5 3 1 4 2를 버블 정렬하라.
5 3 1 4 2
3 5 1 4 2
3 1 5 4 2
3 1 4 5 2
3 1 4 2 5
3 1 4 2 5
1 3 4 2 5
1 3 4 2 5
1 3 2 4 5
1 3 2 4 5
1 3 2 4 5
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
< 정확성 >
- 제일 큰 원소가 제일 뒤에 존재하고, 그 다음 큰 원소가 그 앞, 그 다음 큰 원소가 그앞, ...
< 시간 복잡도 >
처음 가장 큰 원소를 구할 때 n -1번 비교하고, 두 번째 큰 원소를 구할 때 n-2번, ...
=> (n-1) + (n-2) + ... + 1 = n(n-1) / 2 => Θ(n^2)
즉, 입력과 무관하게, n개의 값을 정렬할 때 항상 똑같은 횟수의 비교를 수행한다.
ex) 1 2 3 4 5와 5 4 3 2 1의 비교 횟수가 같다.
import random
def bubblesort(arr):
for i in range(len(arr) - 1):
for j in range(len(arr) - i + 1):
if j + 1 < len(arr) and arr[j] > arr[j + 1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
List = list(range(20))
random.shuffle(List)
print(List)
print(bubblesort(List))
j + 1이 리스트 길이보다 큰 경우 에러가 발생하므로 주의!
삽입 정렬 (insertion sort)
1. 정렬 대상이 될 원소를 두 부분으로 나눈다.
- 앞은 이미 정렬이 된 부분 (오름차순이 만족하는 부분)
- 뒤는 정렬할 부분
2. 매번 정렬할 부분의 가장 첫번째 원소를 정렬이 된 부분에 삽입한다.
- 삽입은 정렬된 부분의 가장 큰 원소(= 가장 오른쪽 원소)부터 왼쪽으로 비교해나가면서 진행한다.
ex) 5 3 1 4 2를 삽입 정렬하라
5 3 1 4 2 (처음 원소 5는 그 자체로 정렬된 수)
5 3 1 4 2
3 5 1 4 2
3 5 1 4 2
3 1 5 4 2
1 3 5 4 2
1 3 5 4 2
1 3 4 5 2
1 3 4 5 2
1 3 4 5 2
1 3 4 2 5
1 3 2 4 5
1 2 3 4 5
1 2 3 4 5
import random
def insertsort(arr):
result = [] # 정렬한 부분
while len(arr) > 0:
num = arr.pop(0)
result.append(num)
for i in range(len(result) - 1, 0, -1):
if result[i] < result[i-1]:
result[i], result[i-1] = result[i-1], result[i]
else:
break
return result
List = list(range(20))
random.shuffle(List)
print(List)
print(insertsort(List))
< 정확성 >
1. 이미 정렬된 부분은 항상 정렬이 되어 있다.
2. 처음에는 정렬된 부분에 원소 1개 있다. (원소 1개는 그 자체가 정렬되어 있다.)
3. 매번 정렬할 부분의 원소 1개를 정렬된 부분으로 옮긴다.
- 정렬할 부분은 처음 n-1개 원소를 갖고 있다.
- 매번 1개씩 감소하므로, 최종적으로 정렬된 부분에 n개 정렬할 부분에 0개의 원소가 있다.
- 정렬된 부분은 정렬이 되어 있으므로 최종적인 정답은 정렬된 상태이다.
< 시간 복잡도 >
최선의 경우: 이미 오름차순으로 정렬되어 있을 때
- 정렬된 부분에 원소를 넣을 때마다 비교를 한 번만 하면 된다.
=> 총 n-1번 비교 => Θ(n)
최악의 경우: 역순으로 정렬된 수들을 정렬할 때
- i번째 위치의 원소를 정렬된 부분에 추가할 때 i-1개의 원소와 모두 비교해야한다.
(이미 정렬된 부분에 i-1개의 원소가 있을 때)
=> i = 2, 3, 4, ..., n 이므로 1 + 2 + ... + n-1 = n(n-1) / 2 => Θ(n^2)
평균 시간 복잡도: i개의 정렬된 부분에 i+1번째 원소를 삽입할 때
- 삽입을 마치면 정렬된 부분에는 i+1개의 원소가 있고 1등부터 i+1등이 가능하다.
- i+1등할 확률은 1 / (i+1)이고 비교는 1번
- i등할 확률은 1 / (i+1)이고 비교는 2번
- ...
- 2등할 확률은 1 / (i+1)이고 비교는 i번 (마지막 원소와 비교하여 크면 2등)
- 1등할 확률은 1 / (i+1)이고 비교는 i번 (마지막 원소와 비교하여 작으면 1등)
기대값의 선형성: E[x+y] = E[x] + E[y]
i+1번째 원소를 추가할 때 평균적인 비교횟수
=> 2등일 때부터 i+1등일때까지 확률의 합 + 1등일 때의 합
=> i가 1~n-1까지 합은 n(n-1) / 4 + (n-1) - {(1 / 2) + (1 / 3) + ... + (1 / n)}
n(n-1) / 4 + (n-1)
=> Θ(n^2)
{(1 / 2) + (1 / 3) + ... + (1 / n)}
=> {(1 / n) + (1 / n) + ... (1 / n)} ≤ {(1 / 2) + (1 / 3) + ... + (1 / n)} ≤ {1 + 1 + ... + 1}
=> 1 ≤ {(1 / 2) + (1 / 3) + ... + (1 / n)} ≤ n-1
따라서, n^2 / 4 ≤ n(n-1) / 4 + (n-1) - {(1 / 2) + (1 / 3) + ... + (1 / n)} ≤ 5n^2 / 4
=> 평균적인 시간 복잡도는 Θ(n^2)이다.
+ 수열의 합의 크기를 추정할 때, 반복되는 횟수만큼 가장 큰 값을 더한 것보다는 작고, 가장 작은 값을 더한 것보다는 크다는 성질을 이용하여 범위를 구할 수 있다.
O(n^2)보다 빠르게 정렬할 수 있는가?
인접한 두 원소를 교환하는 방식의 정렬 알고리즘은 O(n^2)보다 빨리 정렬할 수 없다.
< 증명 >
- n개의 서로 다른 값을 가지는 원소를 나열하는 방법은 n! 가지가 있지만, 오름차순/내림차순으로 정렬된 결과는 한 가지이다.
- n개의 원소 중 둘을 뽑는 방법은 (n, 2)가지가 있다.
- 정렬되지 않았다면 순서가 틀린 쌍(= 둘을 뽑았을 때 앞의 원소가 뒤의 원소보다 큰 쌍)이 있다.
- 정렬되었다면, 순서가 틀린 쌍이 없다.
- 인접한 두 원소를 교환하면 이 중 오로지 한 쌍이 제거된다.
ex) 1 2 5 4 3 => 1 2 5 3 4 (단 한 쌍만 수정)
ex) 1 2 5 4 3 => 1 2 3 4 5 (멀리 떨어진 쌍을 바꾸면 한 번에 모두 수정)
- 최악의 경우에는 모든 쌍의 순서가 잘못되어있다. (역순 정렬)
- 따라서, (n, 2) = O(n^2)번 교환이 필요하다.
'Study > Algorithm Study' 카테고리의 다른 글
[Algorithm] 정렬: 힙 정렬 (1) | 2023.09.24 |
---|---|
[Algorithm] 정렬: 병합 정렬, 퀵 정렬 (1) | 2023.09.17 |
[Algorithm] 알고리즘 기초 (3) (0) | 2023.09.16 |
[Algorithm] 알고리즘 기초 (2) (0) | 2023.09.05 |
[Algorithm] 알고리즘 기초 (1) (2) | 2023.08.30 |