득이공간

[알고리즘] 7장. Sorting 본문

PS/알고리즘

[알고리즘] 7장. Sorting

쟁득 2024. 2. 27. 14:22
해당 게시물은 한치근 교수님의 '알고리즘' 강의를 수강하며
학습한 내용을 개인적으로 정리한 글입니다.

📌 7-1

* 계산복잡도(Computational Complexity)
- 얼마나 많은 계산을 해야지 문제가 풀릴 것인가


* 알고리즘의 분석
- 어떤 특정 알고리즘의 효율(efficiency)을 측정
- 시간복잡도(time complexity)
- 공간복잡도(space/memory complexity)


* 문제풀이 접근하는 2가지 방법
- 문제를 푸는 더 효율적인 알고리즘을 개발
- 더 효율적인 알고리즘 개발이 불가능함을 증명


* 문제의 분석
- 일반적으로 "계산복잡도 분석"이란 "문제의 분석"을 지칭
- 어떤 문제에 대해서 그 문제를 풀 수 있는 모든 알고리즘의 효율의 하한(lower-bound)을 결정한다.
- 복잡도 하한이 Ω(f(n))인 문제에 대해서 복잡도가 Θ(f(n))인 알고리즘을 만들어 내는 것이 목표다.


* 정렬문제(sorting) : Ω(nlgn)
- 교환정렬(Exchange sort) : Θ(n^2)
- 합병정렬(Merge sort) : Θ(nlgn)


* 안정성(Stability)
- 같은 키값을 갖는 데이터 간의 정렬 전 순서가 정렬 후에도 유지되는 성질

 

* stable
- 삽입정렬 (insertion sort)
- 합병정렬 (merge sort)
- 거품정렬 (bubble sort)

 

* not stable
- 빠른정렬 (quick sort)
- 힙정렬 (heap sort)
- 선택정렬 (selection sort)
- 교환정렬 (exchange sort)


📌 7-2

* <A> 삽입정렬 (Insertion Sort)

cpp
void insertionsort(int n, keytype S[]) { ‌index i, j; ‌keytype x; for (i = 2; i <= n; i++) { ‌‌x = S[i]; ‌‌j = i - 1; ‌‌while (j > 0 && S[j] > x) { ‌‌‌S[j + 1] = S[j]; ‌‌‌j--; ‌‌} ‌‌S[j + 1] = x; } }


* <A> 선택정렬 (Selection Sort)

cpp
void selectionsort(int n, keytype S[]) { ‌index i, j, smallest; for (i = 1; i <= n-1; i++) { ‌‌smallest = i; ‌‌for (j = i+1; j <= n; j++) ‌‌‌if (S[j] < S[smallest]) ‌‌‌‌smallest = j; ‌‌exchange S[i] and S[smallest]; } }


* <A> 교환정렬 (Exchange Sort)

cpp
void exchangesort(int n, keytype S[]) { ‌index i, j; for (i = 1; i <= n-1; i++) ‌‌for (j = i+1; j <= n; j++) ‌‌‌if (S[j] < S[i]) ‌‌‌‌exchange S[i] and S[j] }


* <A> 거품정렬 (Bubble Sort)

cpp
void bubblesort(int n, keytype S[]) { ‌index i, j; for (i=n; i>=1; i--) ‌‌for (j=2; j<=i; j++) ‌‌‌if (S[j-1] > S[j]) ‌‌‌‌exchange S[j-1] amd S[j] }


* Θ(n^2) 알고리즘 비교

  비교횟수 지정횟수
삽입정렬 W(n)=n^2/2
A(n)=n^2/4
W(n)=n^2/2
A(n)=n^2/4
선택정렬 T(n)=n^2/2 T(n)=3n
교환정렬 T(n)=n^2/2 W(n)=3n^2/2
A(n)=3n^2/4
거품정렬 T(n)=n^2/2 W(n)=3n^2/2
A(n)=3n^2/4


* 정렬 알고리즘 비교
- 삽입정렬은 어느정도 정렬된 데이터에 대해서는 빠르게 수행된다.
- 삽입정렬은 교환정렬 보다는 항상 최소한 빠르게 수행된다고 할 수 있다.
- 선택정렬이 교환정렬 보다 일반적으로 빠르다.
- 입력이 이미 정렬되어 있는 경우, 선택정렬은 지정이 이루어지지만(자신의 위치에서) 교환정렬은 지정이 이루어지지 않으므로 교환정렬이 빠르다.
- n의 크기가 크고, 키의 크기가 큰 자료구조일 때는 지정하는 시간이 많이 걸리므로 선택정렬 알고리즘이 삽입정렬 알고리즘보다 더 빠르다.


📌 7-3

* 한번 비교하는데 최대한 하나의 역을 제거하는 알고리즘의 하한선
- 역 : 순서쌍 중에 앞에있는 것이 뒤에있는 것보다 큰 상태


* 합병정렬은 비교마다 하나 이상의 역을 제거하므로 교환, 삽입, 선택 정렬보다 효율적이다.


* <A> 빠른정렬 (Quick Sort)

cpp
void quicksort(index low, index high) { ‌index pivotpoint; if (high > low) { ‌‌partition(low, high, pivotpoint); ‌‌quicksort(low, pivotpoint - 1); ‌‌quicksort(pivotpoint + 1, high); } }

📌 7-4

* binary tree의 종류
- 완전이진트리(complete(perfect) binary tree)
- 실질적인 완전이진트리(essentially complete binary tree)
- full binary tree(proper binary tree or 2-tree)


* 힙 (heap)
- 힙의 성질(heap property) : 어떤 마디에 저장된 값은 그 마디의 자식마디에 저장된 값보다 크거나 같다. - max heap
- 힙(heap) : 힙의 성질을 만족하는 실질적인 완전이진트리


* 힙 구조의 특성
- 최대값 확인 - O(1)
- 최대값 제거 및 재구성 - O(lg n)
- 데이터 추가, 삭제, 변경 - O(lg n)
- 최대값을 항상 유지해야하는 Queue를 구현하는데 적합하다. - priority queue


* <A> Siftdown
- 힙 성질을 만족하도록 재구성하는 방법. sift: 채로 치다.

cpp
void siftdown(heap& H) { ‌node parent, largerchild; ‌parent = root of H; ‌largerchild = parent's child containing larger key; while(key at parent is smaller than key at largerchild) { ‌‌exchange key at parent and key at largerchild; ‌‌parent = largerchild; ‌‌largerchild = parent's child containing larger key; } } - 루트에서 키를 추출하고 힙 성질을 회복하는 의사코드 keytype root(heap& H) { ‌keytype keyout; ‌keyout = key at the root; ‌move the key at the bottom node to the root; delete the bottom node; siftdown(H); return keyout; }


* <A> 힙정렬 (Heap Sort)
1. n개의 키를 이용하여 힙을 구성한다.
2. 루트에 있는 제일 큰 값을 제거한다. -> 힙 재구성
3. step2를 n-1번 반복한다.

cpp
void heapsort(int n, heap H, keytype S[]) { makeheap(n, H); removekeys(n, H, S); } void makeheap(int n, heap& H) { ‌index i; ‌heap Hsub; for (i = d - 1; i >= 0; i--) ‌‌for (all subtree Hsub whose roots have depth i) ‌‌‌siftdown(Hsub); } void removekeys(int n, heap H, keytype S[]) { ‌index i; for (i = n; i >= 1; i--) ‌‌S[i] = root(H); }


* make heap 방법
- 방법 1 : 데이터가 입력되는 순서대로 heap을 매번 구성
- 방법 2 : 모든 데이터를 트리에 넣은 상태에서 heap 구성


* Θ(nlgn) 알고리즘 비교

  비교횟수 지정횟수 저장공간
합병정렬 W(n)=A(n)=nlgn T(n)=2nlgn Θ(n)
빠른정렬 W(n)=n^2/2
A(n)=1.38nlgn
A(n)=0.69nlgn Θ(lgn)
힙정렬 W(n)=A(n)=2nlgn W(n)=A(n)=nlgn Θ(1)

📌 7-5

* 키의 비교만으로 정렬하는 경우 하한
- 정렬알고리즘에 대한 결정트리(decision tree)


* 가지친 결정트리(pruned decision tree)

- 일관성 있는 순서로 결정을 내림으로써 뿌리마디로부터 모든 잎마디에 도달할 수 있는 경우, 가지친(pruned) 결정트리다.


* 정리 7.3

- n개의 서로 다른 키를 비교함으로써 만 정렬하는 결정적 알고리즘은 최악의 경우 최소한 「nlgn - 1.45nㄱ번의 비교를 수행한다.


* 키 값의 비교를 통한 정렬은 Ω(nlgn)의 복잡도를 갖는다. 즉, nlgn보다 더 빠른 알고리즘을 개발할 수는 없다.
* 합병정렬의 평균의 경우 성능인 nlgn - 1.26n은 키를 비교만 하여 정렬하는 알고리즘으로는 거의 최적이다.


📌 7-6

* <A> 기수정렬 (Radix Sort)
- 분배에 의한 정렬
- 키에 대해서 아무런 정보가 없는 경우 : 불가능
- 키에 대한 어느 정도의 정보를 알고 있는 경우 : 기수(radix, base)를 사용한다.

cpp
void radixsort(node_pointer& masterlist, int numdigits) { ‌index i; ‌node_pointer list[0..9]; for (i = 1; i <= numdigits; i++) { ‌‌distribute(masterlist, i); ‌‌coalesce(masterlist); } } void distribute(node_pointer& masterlist, index i) { ‌index j; ‌node_pointer p; for (j = 0; j <= 9; j++) ‌‌list[j] = NULL; ‌p = masterlist; while (p != NULL) { ‌‌j = p->key에서 (오른쪽에서) i번째 숫자의 값; ‌‌p를 list[j]의 끝에 링크; ‌‌p = p->link; } } void coalesce(node_pointer& masterlist) { ‌index j; ‌masterlist = NULL; for (j = 0; j <= 9; j++) ‌‌list[j]에 있는 마디들을 masterlist의 끝에 링크 }


* Topological Sort
- 정의 : i에서 j로 가는 arc가 있을 때 i가 j보다 먼저 오는 정렬 방법


'PS > 알고리즘' 카테고리의 다른 글

[알고리즘] 8장. Searching  (1) 2024.02.28
[알고리즘] 6장. Branch and Bound  (1) 2024.02.27
[알고리즘] 5장. Backtracking  (0) 2024.02.27
[알고리즘] 4장. Greedy Algorthm  (1) 2024.02.27
[알고리즘] 3장. Dynamic Programming  (2) 2024.02.27