정렬
- 알고리즘의 기본 정렬에 대해 알아보자
- 특정한 기준(오름차순 or 내림차순 ...etc)으로 수열을 배열하는 것
- 여러 정렬 알고리즘에 대해 알아보자
- 단순 but 비효율적: 삽입, 선택, 버블
- 복잡 but 효율적: 퀵, 힙, 합병, 기수
1. 삽입 정렬(Insertion Sort)
- 제자리 정렬 알고리즘 : 정렬 시, 추가 메모리를 요구하지 않음
- 손 안의 카드를 정렬하는 방법
- 정해진 카드 하나(key index)와 앞에 카드들를 비교한다 생각해봐라
- 알고리즘
- 카드를 오름차순으로 정렬하게 된다면,
- key index의 카드가 왼쪽보다 작으면 swap
- key index의 카드가 왼쪽보다 크면 중단할 것
- ...
- 모든 카드에 대해 위 방식을 진행
- 삽입 정렬 특징
- [장점1]: 안정 정렬 : 값이 같은 두 원소의 순서가 바뀌지 않음
- [장점2]: 레코드의 수가 적음 or 정렬이 대부분 되어있는 경우 효율적
- [단점1]: 레코드의 수가 많을 경우 비효율적
- 시간 복잡도
- 최선: O(n)
- 평균[default]: O(n2)
- 최악[default]: O(n2)

- 삽입 정렬 코드
# include <stdio.h>
# define MAX_SIZE 5
void insertion_sort(int list[], int n){
int i, j, key;
for(i=1; i<n; i++){
key = list[i];
for(j=i-1; j>=0 && list[j]>key; j--){
list[j+1] = list[j];
}
list[j+1] = key;
}
}
void main(){
int i;
int n = MAX_SIZE;
int list[n] = {8, 5, 6, 2, 4};
insertion_sort(list, n);
for(i=0; i<n; i++){
printf("%d\n", list[i]);
}
}
2. 선택 정렬(Selection Sort)
- 제자리 정렬 알고리즘 : 정렬 시, 추가 메모리를 요구하지 않음
- 배열 전체를 보고, 특정 기준에 맞는 값을 첫번째 값부터 선택해나감
- 알고리즘
- 카드를 오름차순으로 정렬하게 된다면,
- 0번째 인덱스에 들어갈 가장 작은 원소를 전체 배열에서 찾음
- 1번째 인덱스에 들어갈 가장 작은 원소를 전체 배열 - 1(선택된 원소 제외)에서 찾음
- 2번째 인덱스에 들어갈 가장 작은 원소를 전체 배열 - 2(선택된 원소 제외)에서 찾음
- ...
- 모든 카드에 대해 위 방식을 진행
- 선택 정렬 특징
- [장점1]: 구현이 쉽다.
- *[장점2]: 메모리 소비가 작다.
- [단점1]: 불안정 정렬
- 시간 복잡도
- 최선: O(n2)
- 평균[default]: O(n2)
- 최악: O(n2)

- 선택 정렬 코드
# include <stdio.h>
#define SWAP(x, y, temp)((temp) = (x), (x) = (y), (y) = (temp))
# define MAX_SIZE 5
void selection_sort(int list[], int n){
int i, j, least, temp;
for(i=0; i<n-1; i++){
least = i;
for(j=i+1; j<n; j++){
if(list[j]<list[least])
least = j;
}
if(i != least){
SWAP(list[i], list[least], temp);
}
}
}
void main(){
int i;
int n = MAX_SIZE;
int list[n] = {9, 6, 7, 3, 5};
selection_sort(list, n);
for(i=0; i<n; i++){
printf("%d\n", list[i]);
}
}
3. 버블 정렬(Bubble Sort)
- 거품(버블)이 올라오듯 정렬되는 알고리즘
- 서로 인접한 두 원소를 검사해 정렬
- 선택정렬과 알고리즘이 유사(선택: 앞부터 / 버블: 뒤부터)
- 알고리즘
- 카드를 오름차순으로 정렬하게 된다면,
- k번째 인덱스와 k+1번째 인덱스 중 큰것을 비교
- SWAP을 통해 큰 원소가 뒤 인덱스에 위치하도록 변경
- 마지막 인덱스에 가장 큰 원소 배치
- 마지막 - 1 인덱스에 두번째로 큰 원소 배치
- ...
- 모든 카드에 대해 위 방식을 진행
- 버블 정렬 특징
- [장점1]: 구현이 쉽다.
- [단점1]: 불안정 정렬
- [단점2]: 인접한 요소와 교환(SWAP)만을 수행
- 교환작업(SWAP) 비용 > 이동작업(MOVE) 비용이라 버블정렬은 거의 사용하지 않음
- 즉 비효율적
- 시간 복잡도
- 최선: O(n2)
- 평균[default]: O(n2)
- 최악: O(n2)

- 버블 정렬 코드
# include <stdio.h>
# define MAX_SIZE 5
void bubble_sort(int list[], int n){
int i, j, temp;
for(i=n-1; i>0; i--){
for(j=0; j<i; j++){
if(list[j]<list[j+1]){
temp = list[j];
list[j] = list[j+1];
list[j+1] = temp;
}
}
}
}
void main(){
int i;
int n = MAX_SIZE;
int list[n] = {7, 4, 5, 1, 3};
bubble_sort(list, n);
for(i=0; i<n; i++){
printf("%d\n", list[i]);
}
}
4. 셸 정렬(Shell Sort)
- 삽입 정렬 업그레이드 버전
- 삽입 정렬 문제점인 인접 원소간 교환(SWAP)의 문제점을 보완
- 삽입 정렬이 어느정도 정렬된 배열에 대해 대단히 빠른 것에서 착안
- 셸 정렬은 전체 배열을 한 번에 정렬하지 않음
- 전체 배열을 부분 리스트로 나눠 삽입 정렬 수행
- 알고리즘
- 카드를 오름차순으로 정렬하게 된다면,
- 전체 배열의 k(간격 = gap)번째 요소를 추출해 부분 리스트를 만든다.
- 간격의 초깃값 : (전체 배열의 크기) / 2
- 생성된 부분 리스트의 개수 = k
- 각 회전마다 k를 절반씩 줄인다.
- 즉, 각 부분 리스트의 개수는 감소하고, 원소의 개수는 증가한다.
- 간격(k)은 홀수로 설정하는 것이 좋다.
- 간격(k)이 짝수로 나온다면 +1을 수행한다.
- 위 방식을 간격(k) = 1이 될 때까지 진행

- 셸 정렬 특징
- [장점1]: 부분 리스트 사용 -> 교환되는 요소들이 삽입 정렬보다 최종 위치에 있을 가능성이 높아진다. -> 평균 이동 거리 감소
- [장점2]: 삽입 정렬보다 더욱 빠르게 수행된다.
- [장점3]: 간단한 알고리즘으로 쉬운 구현 가능
- 시간 복잡도
- 최선: O(n)
- 평균[default]: O(n1.5)
- 최악: O(n2)

- 셸 정렬 코드
# include <stdio.h>
# define MAX_SIZE 10
void inc_insertion_sort(int list[], int first, int last, int gap){
int i, j, key;
for(i=first+gap; i<=last; i=i+gap){
key = list[i];
for(j=i-gap; j>=first && list[j]>key; j=j-gap){
list[j+gap] = list[j];
}
list[j+gap] = key;
}
}
void shell_sort(int list[], int n){
int i, gap;
for(gap=n/2; gap>0; gap=gap/2){
if((gap%2) == 0)(
gap++;
)
for(i=0; i<gap; i++){
inc_insertion_sort(list, i, n-1, gap);
}
}
}
void main(){
int i;
int n = MAX_SIZE;
int list[n] = {10, 8, 6, 20, 4, 3, 22, 1, 0, 15, 16};
shell_sort(list, n);
for(i=0; i<n; i++){
printf("%d\n", list[i]);
}
}
5. 합병 정렬(Merge Sort)
- 분할 정복 알고리즘을 활용한 정렬
- 작은 두개의 문제로 전체 문제를 분리하고 해결한 각각의 답을 모아 전체 문제를 해결
- 제자리 정렬X: 추가 메모리 공간(리스트) 필요
- 안정 정렬
- 알고리즘
- 리스트의 길이가 1 or 0이면 정렬된 것으로 본다.
- 그렇지 않은 경우
분할: 입력 배열을 같은 크기의 2개의 부분 배열로 분할한다.
합병 정렬: 부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 순환 호출 을 이용하여 다시 분할 정복
병합: 부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 순환 호출 을 이용해 다시 분할 정복
실제 정렬이 이루어지는 과정

- 합병 정렬 특징
- [장점1]: 안정 정렬
- 데이터 분포에 영향을 덜 받는다.
- 입력 데이터와 상관없이 정렬 시간이 동일하다 => O(nlog2n)
- [단점1]: 임시 배열(메모리) 필요
- [단점2]: 각 레코드들의 크기가 큰 경우 이동 횟수가 많아 매우 큰 시간적 낭비 초래
- [단점3]: 힙정렬보다 다소 메모리가 많이 필요함(단, 상대적으론 빠름)
- 시간 복잡도
- 최선: nO(nlog2n)
- 평균[default]: nO(nlog2n)
- 최악: nO(nlog2n)

- 합병 정렬 코드
# include <stdio.h>
# define MAX_SIZE 8
int sorted[MAX_SIZE]
void merge(int list[], int left, int mid, int right){
int i, j, k, l;
i = left;
j = mid+1;
k = left;
while(i<=mid && j<=right){
if(list[i]<=list[j])
sorted[k++] = list[i++];
else
sorted[k++] = list[j++];
}
if(i>mid){
for(l=j; l<=right; l++)
sorted[k++] = list[l];
}
else{
for(l=i; l<=mid; l++)
sorted[k++] = list[l];
}
for(l=left; l<=right; l++){
list[l] = sorted[l];
}
}
void merge_sort(int list[], int left, int right){
int mid;
if(left<right){
mid = (left+right)/2
merge_sort(list, left, mid);
merge_sort(list, mid+1, right);
merge(list, left, mid, right);
}
}
void main(){
int i;
int n = MAX_SIZE;
int list[n] = {21, 10, 12, 20, 25, 13, 15, 22};
merge_sort(list, 0, n-1);
for(i=0; i<n; i++){
printf("%d\n", list[i]);
}
}
6. 퀵 정렬(Quick Sort)
- 분할 정복 알고리즘을 활용한 정렬
- 작은 두개의 문제로 전체 문제를 분리하고 해결한 각각의 답을 모아 전체 문제를 해결
- 불 안정 정렬
- 알고리즘
- 부분 리스트들이 더 이상 분할 불가능할 때(리스트의 크기 = 0 or 1)까지 아래 과정을 반복한다.
- 분할(Devide): 입력 요소들을 피봇(Pivot)을 기준으로 비균등하게 2개의 부분 배열로 분할한다.
- 정복(Conquer): 부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 재귀 호출을 통해 분할한다.
- 결합(Combine): 정렬된 부분 배열들을 하나의 배열에 합병한다.
- 순환 호출이 이뤄질 때마다 최소한 하나(피봇 위치)의 원소 위치가 결정되어 반드시 끝남을 보장
- 리스트 안에 한 요소 피봇을 선택
- 피봇보다 작은 원소는 왼쪽으로 이동
- 피봇보다 큰 원소는 오른쪽으로 이동
- 피봇을 제외한 왼쪽 리스트와 오른쪽 리스트 재귀 호출로 재정렬


- 퀵 정렬 특징
- [장점1]: Quick(빠르다)
- STL에서 사용하는 정렬 코드가 퀵정렬 = 그만큼 효율적이라는 의미
- [장점2]: 제자리 정렬
- [단점1]: 정렬된 상태의 리스트의 경우 불균형 분할의 특성 상 수행 시간이 많이 걸린다.
- 보완: 피봇 선택 방법에 따라 퀵 정렬의 불 균형 분할 방지 or 완화
midian of three
: 중간값(medium)을 피봇으로 선택
- 시간 복잡도
- 최선: nO(nlog2n)
- 평균[default]: nO(nlog2n)
- 최악: nO(n2)

- 퀵 정렬 코드
# include <stdio.h>
# define MAX_SIZE 9
# define SWAP(x, y, temp) ( (temp)=(x), (x)=(y), (y)=(temp) )
int partition(int list[], int left, int right){
int pivot, temp;
int low, high;
low = left;
high = right + 1;
pivot = list[left];
do{
do {
low++;
} while (low<=right && list[low]<pivot);
do {
high--;
} while (high>=left && list[high]>pivot);
if(low<high){
SWAP(list[low], list[high], temp);
}
} while (low<high);
SWAP(list[left], list[high], temp);
return high;
}
void quick_sort(int list[], int left, int right){
if(left<right){
int q = partition(list, left, right);
quick_sort(list, left, q-1);
quick_sort(list, q+1, right);
}
}
void main(){
int i;
int n = MAX_SIZE;
int list[n] = {5, 3, 8, 4, 9, 1, 6, 2, 7};
quick_sort(list, 0, n-1);
for(i=0; i<n; i++){
printf("%d\n", list[i]);
}
}
7. 힙 정렬(Heap Sort)
- 자료구조 힙(Heap)을 사용한 정렬
- 힙(Heap): 완전 이진 트리의 일종으로 우선순위 큐를 위해 만들어진 자료구조
- 최댓값, 최솟값을 쉽게 추출[루트]할 수 있는 자료구조
- 최대 힙 트리나 최소 힙 트리를 구성해 정렬
- 최대 힙: 내림차순 정렬
- 최소 힙: 오름차순 정렬
- 알고리즘
- 1. 힙 구성: 정렬할 N개 원소를 정렬 조건에 따라 최대힙 or 최소힙 구성해 정렬을 수행
- 2. 루트 원소 삭제: 루트 원소를 배열에 저장
- 3. 재배치: 삭제된 루트 원소를 채우기 위해 힙 재구성
- 최대 힙 삭제

element delete_max_heap(HeapType *h){
int parent, child;
element item, temp;
item = h->heap[1];
temp = h->heap[(h->heap_size)--];
parent = 1;
child = 2;
while(child <= h->heap_size){
if( (child < h->heap_size) && ((h->heap[child].key) < h->heap[child+1].key) ){
child++;
}
if( temp.key >= h->heap[child].key ){
break;
}
h->heap[parent] = h->heap[child];
parent = child;
child *= 2;
}
h->heap[parent] = temp;
return item;
}