[Data Structures & Algorithms] Sorting (1)
Sorting
: 컴퓨터 관련 전공에서 매우 중요한 고전 문제. Sorting을 이용하면 많은 문제들을 효율적으로 풀 수 있음.
- 이진 탐색(Binary Search) : O(log2n) 보장
- 가장 큰 값 (혹은 가장 작은 값) 찾기 : O(1) 보장
- 중복되는 같은 값들이 있을 때 이들을 제거 : O(n) 보장
- 최소 신장 트리(Minimal Spanning Tree) : O(n) 보장
- SQL: ORDER BY, GROUP BY
- 대용량 데이터 탐색(B Tree)
- 문제를 푸는 데 전체 수행 시간의 상당 비중 (약 25% - 50%)이 sorting 시간에 소비됨. 따라서 다양한 sorting 알고리즘들의 기법, 효율성, 장/단점들을 습득하는 것이 매우 중요함.
- Sorting의 대상이 되는 값들을 각각 key 라고 지칭함. 다음의 2 가지 유형의 key들이 있음.
- 숫자(Numeric) : 학번, 사번, 점수 등
- 문자열(String) : 이름, 단어, 용어 등
고려사항
Time 성능
- 비교(comparison) 연산을 총 몇 번하나?
- 즉, O(n2) 혹은 O(n*log2n) 이냐?
- cf) 탐색에서는, O(n) vs O(log2n)
- Swap 연산을 많이 (혹은 적게) 하나?
Space 성능
- In-Place
- Extra Space (즉, space(예: stack)가 여분으로 더 필요하나?)
유형
Internal Sort (내부 정렬)
- Sort small or medium volume data in main memory
- selection sort, bubble sort, insertion sort, quick sort, heap sort, merge sort, etc.
- 성능 척도 : 비교 회수 (CPU cost)
- O(n2) or O(n*log2n)
- 자료구조 과목에서 취급
External Sort (외부 정렬)
- Sort very large volume data in disk storage
- Can not directly use internal sort algorithms
- Divide a file into several sorted subfiles (= run)
- Use internal sort and merge sorted subfiles
- 성능 척도 : disk 접근 회수 (I/O cost)
- 데이터베이스 과목에서 취급
Selection Sort (선택 정렬)
- 전체 list가 2 개의 sublist 들로 분할됨 : sorted / unsorted
- 전체 list가 모두 sort 될 때까지 각 단계(pass)에서 다음 과정을 반복;
- (1) Unsorted list에서 smallest key를 탐색해서 선택.
- (2) 이 smallest key를 unsorted list의 맨 앞에 위치한 key와 swap.
Selection Sort (int list[ ], int n)
int i = 0, j = 0;
int smallest = 0; int temp = 0;
// list에서 smallest key를 찾음/
for (i = 0; i < n - 1; i ++) {
smallest = i;
for (j = i + 1; j < n; j++)
if (list[j] < list[smallest])
smallest = j;
// Smallest key와 맨 앞의 key와 교환 //
temp = list [i];
list [i] = list [smallest];
list [smallest] = temp;
- 총 비교 회수 :
- = (n-1) + (n-2) + . . . + 1
- = (n-1)n/2
- = O(n^2)
- Duplex(이중) Selection Sorting
- Unsorted List를 탐색하는 동안 smallest 와 largest key를 함께 동시에 찾음.
- Smallest key와 list의 맨 앞에 위치한 key와 swap
- Largest key를 list의 맨 뒤에 위치한 key와 swap
- 결과적으로 한 단계에서 smallest와 largest 값 2 개가 제자리를 찾아 감.
- 총 비교 회수의 성능은 조금 향상됨.
- 그러나 Big Oh 표기의 관점에서 여전히 O(n^2)의 시간 범주에 속함
Bubble Sort
- 전체 list가 2 개의 sublist 들로 분할됨 : sorted / unsorted
- 전체 list가 모두 sort 될 때까지 각 단계(pass)에서 다음 과정을 반복;
- (1) unsorted list의 오른쪽 끝에서 왼쪽으로 이동하면서 각 쌍(pair)의 2 개의 key 값들을 서로 비교함.
- (2) 만약 비교 후 key들의 크기가 뒤바뀌었으면 서로 swap함.
- 각 단계에서 smallest key가 filtering되어 sorted list로 이동됨.
- 총 비교 회수 :
- = (n-1) + (n-2) + . . . + 1 = (n-1)n/2 = O(n^2)
비교 : Selection Sort와 Bubble Sort
- Selection Sort는 각 단계에서 swap이 단 1 번만 발생
- : 즉, Smallest key와 맨 앞의 key와의 swap 때만 발생
- Bubble Sort는 각 단계에서 swap 횟수가 많음
- : Smallest key가 한 칸마다 왼 쪽 방향으로 계속 이동하므로;
- Swap 측면에서, Key size가 큰 경우 (예: 문자열)에는, Selection Sort가 더 효율적임.
- Key들이 거의 sort가 되는 과정인 경우, Bubble Sort가 매우 효율적임.
- 즉, 만일 어떤 단계에서 swap이 전혀 없으면 sort가 완료 된 것이므로 그 다음 단계를 수행할 필요 없고, 여기서 종료됨.
- Key들이 이미 sort가 된 list 인 경우, Bubble Sort는 첫번째 단계에서 종료됨: 따라서 O(n)으로 향상됨. 반면에 Selection Sort는 여전히 O(n^2).
Insertion Sort (삽입 정렬)
- 전체 list가 2 개의 sublist 들로 분할됨 : sorted / unsorted
- 전체 list가 모두 sort 될 때까지 각 단계(pass)에서 다음 과정을 반복;
- (1) 각 unsorted list의 맨 앞의 key를 pivot이라는 변수에 이 값을 저장함.
- (2) pivot과 pivot 이전에 위치한 각 key들을 왼쪽 방향으로 계속 이동하면서 비교함.
- (3) pivot 보다 큰 key를 만날 때 마다, 이 큰 key를 오른 쪽으로 한 칸 이동함. 최종적으로 (즉, pivot보다 작은 key 다음 위치) 남은 빈 공간에 pivot을 insert함.
-
i 번째 단계 수행 후에는, list의 첫번째에서 i 번째 까지의 모든 key들은 (즉 pivot 이전의 모든 key들) 이미 sort가 완료가 된 상태임.
- Worst case의 총 비교회수
- Input list의 Key들이 내림 차순으로 이미 sort 된 경우
- 예: {80, 70, 60, 50, 40, 30, 20, 10}
- 이 경우 각 단계에서 pivot 이전의 모든 key들과 비교를 해야 함.
- 1 + 2 + . . . + (n – 1) = n (n-1)/2 = O(n2)
- Input list의 Key들이 내림 차순으로 이미 sort 된 경우
- Best case의 총 비교회수
- Input list의 Key들이 오름 차순으로 이미 sort 된 경우
- 예: {10, 20, 30, 40, 50, 60, 70, 80}
- 이 경우 각 단계에서 pivot 바로 이전의 key 한 개만 비교하면 충분.
- 1 + 1 + . . . + 1 = O(n)
- Input list의 Key들이 오름 차순으로 이미 sort 된 경우
비교 : Selection, Bubble, Insertion
- Worst case에 모든 알고리즘이 O(n^2)으로서 동일
- Extra Space가 필요 없음. Array 1 개면 충분.
- 알고리즘들이 비교적 단순하므로 사용하기 편함.
- Key들의 개수(n)가 적은 경우에는 사용해도 무방. 단, 많은 경우에는 매우 느림. 예: n = 100,000이상
- Key들이 내림 차순으로 이미 정렬된 경우에는 insertion sort 를 사용하면 매우 비효율적.
- Key들이 오름 차순으로 이미 정렬된 경우에는 bubble과 insertion sort 를 사용하면 매우 효율적
- Swap 연산은 selection sort 를 사용하면 매우 효율적.
연습: Selection, Bubble, Insertion
- 1) 다음의 각 list를 Selection, Bubble, Insertion Sort를 이용하여 정렬 과정을 단계별로 보여라.
- 2) 각 방법에 대해 비교 회수와 swap 회수를 계산하라.
- list 1 = {7, 5, 10, 4, 3, 6, 8, 9, 2, 1}
- list 2 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
- list 3 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Quick Sort
(One of Top 10 algorithms in 20th Century)
- 전체 list가 모두 sort 될 때까지 각 단계(pass)에서 다음 과정을 반복;
- (1) Pivot 을 선택 (단, pivot 을 list의 맨 앞의 key로 선택한다고 가정)
- (2) List를 2 개의 sublist로 다음과 같이 분할(Split)함;
- 왼 쪽 sublist의 모든 key들은 pivot 보다 작음.
- 오른 쪽 sublist의 모든 key들은 pivot 보다 큼
- Divide and Conquer 알고리즘 기법은 주어진 문제를 계속하여 더 작은 크기의 각각의 문제들로 분할한 후 각각의 답을 합치는 방식임.
- Quick sort는 위의 방식을 기반으로, split 과정을 계속 거쳐 여러 개의 작은 sublist로 분할. 각 sublist가 더 이상 분할이 안 되면 정렬이 완료됨.
- 이제 왼 쪽 sublist를 split을 더 분할이 안 될 때 까지 계속 수행함;
- 다음에 오른 쪽 sublist를 split을 더 분할이 안 될 때 까지 계속 수행한 후 종료함.
성능 분석
- Quick sort의 시간 성능은 list를 어떠한 형태로 split 하는지에 따라 결정됨.
-
즉, split 된 2 개의 sublist가 균형 있게 거의 같은 크기(balance)를 갖는지, 혹은 한 쪽으로만 크기가 편중(biased)되었는지?
- Worst Case Analysis
- Split 된 sublist들이 completely unbalanced (= biased) 일 때 발생
- 즉, 각 split 단계에서 pivot을 smallest 혹은 largest key를 매번 선택할 때
- 예 : {9, 8, 7, . . . ,2, 1} : Decreasingly sorted
- 예 : {1, 2, 3, . . . ,8, 9} : Increasingly sorted
- 각 split 단계 후에, sublist들이 다음의 형태를 갖음;
- list [n - 1], list [n - 2], . . . , list [1];
- 총 비교 회수 : (n - 1) + (n - 2), . . . + 1 = O(n^2)
- Average Case Analysis
- Split 된 sublist들이 roughly qual size 일 때 발생;
- 즉 key들의 개수가 대략 균등하게 2 개의 sublist에 배분되는 경우.
- 매번 split 후에 각 sublist의 크기는 다음과 같이 ½ 씩 감소됨;
- n, n/2, n/4, n/8, . . . n/2k
- 이제 총 split의 회수(k)는 다음과 같음;
- n/2k = 1; k = log2n
- 각 split에서 비교 연산을 n 번 수행함.
- 따라서 quick sort의 총 비교 연산의 회수는 다음과 같음.
- 각 split에서 비교 회수 * 총 split 회수
- = n * log2n
- = O(nlog2n)
- Split 된 sublist들이 roughly qual size 일 때 발생;
- 다음은 Quick Sort의 Average Case를 Formal한 방식으로 분석하는 과정;
- 여기서 T(n)은 n 개의 keys들을 정렬하기 위한 총 비교 회수;
- Quick sort has the best average time among all the sorting methods
Quick Sort의 성능 향상
- Pivot을 list의 맨 앞 혹은 맨 뒤의 key로 정한 경우;
- 그 list가 오름 차순 혹은 내림 차순으로 정렬이 되었는지를 검토;
- 만약 그렇다면, pivot을 임의로 random 값으로 재선정해서 worst case를 피할 것.
- 분할 된 sublist가 점차적으로 중간에 small size (예: key 개수가 10개)가 되는 경우;
- 이 경우 split을 중지하고 다른 알고리즘(예: Insertion sort)를 사용하여 종료하는 것이 더 효율적
- Quick sort는 small size인 경우에는 비효율적
- 좋은 Pivot의 선택이 중요
- 분할된 각 sublist가 거의 균등한 size가 되도록 pivot을 선택
Choice of Pivot : Median 값
- Pivot으로 전체 list에서 “Median (중간값)” 을 선택
- 모든 key 값들을 고려하여 median 값을 계산하는 것은 비효율적;
- 다른 방법: Sampling 방법
- Median of Three : first, middle, last
- 맨 앞, 맨 뒤, 가운데 key들 중에서, 중간 값(median)을 pivot으로 선택
- 예: list[6, 20]인 경우, list[6], list[13]와 list[20] 중에서
- 만약 list[6] = 30, list[13] = 2, list[20] = 10이면, pivot은 list[20] = 10;
- 만약 list[6] = 3, list[13] = 2, list[20] = 10이면, pivot은 list[6] = 3;
- Quick Sort using median of three as pivot
- 위는 각 sublist가 거의 equal size (not biased)로 split되는 경우의 예
댓글남기기