득이공간

[알고리즘] 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)

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)

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)

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)

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)

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: 채로 치다.

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번 반복한다.

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)를 사용한다.

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