본문 바로가기

공부기록/알고리즘

영리한 프로그래밍을 위한 알고리즘 강좌 - Ch1.정렬 #1~7

섹션 1. 정렬

3. 기본적인 정렬 알고리즘

 

정렬 알고리즘
- Bubble sort
- Insertion sort
- Selection sort
simple, slow
- Quicksort
- Merge sort
- Heap sort
fast
- Radix sort O(N)

 

– Selection Sort

각 루프마다

- 최대 원소를 찾는다

- 최대 원소와 맨 오른쪽 원소를 교환한다

- 맨 오른쪽 원소를 제외한다

하나의 원소만 남을 때까지 위 루프를 반복

selectionSort(A[], n) // 배열 A[1...n]을 정렬한다.
{
  for last <- n downto 2 { ---1
    A[1...last] 중 가장 큰 수 A[k]를 찾는다; ---2
    A[k] <-> A[last]; -> A[k] 와 A[last]의 값을 교환 ---3
  }
}

실행시간 :

①의 for 루프는 n-1번 반복

②에서 가장 큰 수를 찾기 위한 비교 횟수 : n-1, n-2, ..., 2.1

③의 교환은 상수시간 작업

시간복잡도 T(n) = (n-1) + (n-2) + ... + 2+1 = O(n^2)

 

그림으로 이해하는 게 더 좋을 것 같다(Selection Sort)

 

 


 

– Bubble Sort

bubbleSort(A[], n) // 배열 A[1...n]을 정렬한다.
{
  for last <- n downto 2 { ---1
    for i <- 1 to last -1  ---2
      if (A[i] > A[i+1]) then A[i] <-> A[i+1]; -> 교환 ---3
    }
}

수행시간:

①의 for 루프는 n-1번 반복

②의 for 루프는 각각 n-1, n-2,...,2,1번 반복

③의 교환은 상수시간 작업

시간복잡도 T(n) = (n-1) + (n-2) + ... + 2+1 = O(n^2)

 

Bubble Sort

 

 


 

– Insertion Sort

insertionSort(A[], n) // 배열 A[1...n]을 정렬한다.
{
  for i <- 2 to n{ --- 1
    A[1...n]의 적당한 자리에 A[i]를 삽입한다 --- 2
  }
}

수행시간:

①의 for 루프는 n-1번 반복

②의 삽입은 최악의 경우 i-1번 비교

최악의 경우 :  T(n) = (n-1) + (n-2) + ... + 2+1 = O(n^2)

- 이건 최악의 경우를 계산한 것이기 때문에 평균적으로는 bubble sort와 selection sort의 절반정도 된다.

 

Insertion Sort

 

 

4. 합병정렬(merge sort)

merge sort 와 quick sort 는 분할정복법을 사용한다 (Divide and Conquer)

 

분할 : 해결하고자 하는 문제를 작은 크기의 동일한 문제들로 분할

정복 : 각각의 작은 문제를 순환적으로 해결

합병 : 작은 문제의 해를 합하여(merge) 원래 문제에 대한 해를 구함

 

 


– merge sort

데이터가 저장된 배열을 절반으로 나눔

각각을 순환적으로 정렬

정렬된 두 개의 배열을 합쳐 전체를 정렬

merge sort

 

 

mergeSort(A[], p, r)  // A[p ... r]을 정렬한다.  
{
  if (p < r) then {
    q <- (p+r) / 2;       --- 1. p, r의 중간 지점 계산
    mergeSort(A, p, q);   --- 2. 전반부 정렬
    mergeSort(A, q+1, r); --- 3. 후반부 정렬
    merge(A, p, q, r);    --- 4. 합병
  }
}

merge(A[], p, q, r)
{
  정렬되어 있는 두 배열 A[p...q]와 A[q+1...r]을 합하여 
  정렬된 하나의 배열 A[p....r]을 만든다. 
}

 

최종 코드 :

void merge( int datap[], int p, int q, int r) {
  int i = p; j = q+1; k = p;
  int tmp[data.length()];
  while (i <= q && j <= r) {
    if (data[i] <= data[j])
      tmp[k++] = data[i++];
    else
      tmp[k++] = data[j++]; 
  }
  while (i <= q)
    tmp[k++] = data[i++];
  while (j <= r)
    tmp[k++] = data[j++];
  for (int i = p; i <= r; i++)
    data[i] = tmp[i];
}

- 한쪽이 끝날 때까지 반복한다.

 

mergesort의 시간복잡도 : O(nlogn)

 

5. 빠른정렬(quick sort)

 

분할 : 배열을 다음과 같은 조건이 만족되도록 두 부분으로 나눈다.

elements in lower parts <= elements in upper parts

정복 : 각 부분을 순환적으로 정렬한다.

합병 : nothing to do

 

 


 

– Quicksort

정렬할 배열이 주어짐. 마지막 수를 기준(pivot)으로 삼는다

기준보다 작은 수는 기준의 왼쪽에 나머지는 기준의 오른쪽에 오도록 재배치(분할)한다 (partition)

기준의 왼쪽과 오른쪽을 각각 순환적으로 정렬한다(정렬 완료)

 

quickSort(A[], p, r) // A[p...r]을 정렬한다
{
  if (p<r) then {
	// q : pivot의 위치
    q = partition(A, p, r);     // 분할
    quickSort(A, p, q-1);       // 왼쪽 부분배열 정렬
    quickSort(A, q+1, r);       // 오른쪽 부분배열 정렬
  }
}

partition(A[], p, r)
{
  // 배열 A[p...r]의 원소들을 A[r]을 기준으로 양쪽으로 재배치하고 A[r]이 자리한 위치를 return 한다;
  x <- A[r];
  i <- p-1;
  for j <- p to r-1 {
    if A[j] <= x then
      i <- i + 1;
      exchange A[i] and A[j];
  }
  exchange A[i+1] and A[r];
  return i+1;
}

 

더보기

어떻게 partition 할거냐?

if A[j] >= x
  j <- j+1;
else
  i <- i+1;
  exchange A[i] and A[j];
  j <- j+1;

 

 

quick sort 의 시간 복잡도

- 최악의 경우

   항상 한 쪽은 0개, 다른 쪽은 n-1개로 분할되는 경우 O(n^2) 

   이미 정렬된 입력 데이터(마지막 원소를 피봇으로 선택하는 경우)

- 최선의 경우

   항상 절반으로 분할되는 경우 O(nlogn)

확률의 기댓값을 이용하여 평균시간복잡도를 구해보면 O(nlogn) 이 된다.

 

 

 

pivot의 선택

- 첫번째 값이나 마지막 값을 피봇으로 선택

   이미 정렬된 데이터 혹은 거꾸로 정렬된 데이터가 최악의 경우

   현실의 데이터는 랜덤하지 않으므로 (거꾸로) 정렬된 데이터가 입력으로 들어올 가능성은 매우 높음

   따라서 좋은 방법이라고 할 수 없음

- median of three

   첫번째 값과 마지막 값, 그리고 가운데 값 중에서 중간값(median)을 피봇으로 선택

   최악의 경우 시간복잡도가 달라지지는 않음

- Randomized Quicksort

   피봇을 랜덤하게 선택

   no worst case instance, but worst case execution

   평균 시간복잡도 O(NlogN)

 

 

 

 

6. 힙 정렬(heap sort)

 

최악의 경우 시간 복잡도 O(nlogn)

sorts in place - 추가 배열 불필요

이진 힙(binary heap) 자료구조를 사용

 


 

– Full vs Complete Binary Trees

full binary tree : 모든 레벨에 노드들이 꽉 차있는 형태

complete binary tree : 마지막 레벨을 제외하면 완전히 꽉 차있고, 마지막 레벨에는 가장 오른쪽부터 연속된 몇 개의 노드가 비어있을 수 있음

 

Heap은
1. complete binary tree이면서
2. heap property를 만족해야함
    (max heap property : 부모는 자식보다 크거나 같다 / min heap property : 부모는 자식보다 작거나 같다)

힙은 유일하지 않다.

 

힙은 일차원 배열로 표현 가능 : A[1..n]
   루트 노드 A[1]
   A[i]의 부모 = A[i / 2]
   A[i]의 왼쪽 자식 = A[2i]
   A[i]의 오른쪽 자식 = A[2i+1]

 

- Max Heapify : recursive version

// 노드 i를 루트로하는 서브트리를 heapify 한다.
Max-Heapify(A, i)
{
  if there is no child of A[i]
    return;
  k <- index of the diggest child of i;
  if A[i] >= A[k]
    return;
  exchange A[i] and A[k];
  Max-Heapify(A, k);
}

 

- Max Heapify : iterative version

Max-Heapify(A, i)
{
  while A[i] has a child do
    k <- index of the biggest child of i;
    if A[i] >= A[k]
  	  return;
    exchange A[i] and A[k];
    i = k;
  end.
}

 

 

BUILD-MAX-HEAP(A)
   heap-size[A] <- length[A]
   for i <- length[A] / 2 down to 1 
      do Max-Heapify(A,i)

시간복잡도 : O(n)

 

 

– Heapsort

1. 주어진 데이터로 힙을 만든다.

2. 힙에서 최댓값(루트)을 가장 마지막 값과 바꾼다.

3. 힙의 크기가 1 줄어든 것으로 간주한다. 즉, 가장 마지막 값은 힙의 일부가 아닌 것으로 간주한다.

4. 루트노드에 대해서 HEAPIFY(1)한다

5. 2~4번을 반복한다

 

 

HEAPSORT(A)
  BUILD-Max-Heap(A) // O(n)
  for i <- heap_size down to 2 do // n-1 times
    exchange A[1] <-> A[i] // O(1)
    heap_size <- heap_size - 1 // O(1)
    Max-Heapify(A, 1) // O(logn)
    
// Total time = O(nlogn)

 

6-4. 힙의 다른 응용 : 우선순위 큐(priority queue)

- 최대 우선순위 큐 (maximum priority queue)는 다음의 두 가지 연산을 지원하는 자료구조

      INSERT(x) : 새로운 원소 x를 삽입

      EXTRACT_MAX() : 최댓값을 삭제하고 반환

- 최소 우선순위 큐 (minimum priority queue)는 EXTRACT-MAX 대신 EXTRACT-MIN을 지원하는 자료구조

- MAX HEAP을 이용하여 최대 우선순위 큐를 구현

 

 

- INSERT

MAX-HEAP-INSERT(A, key){
  heap_size = heap_size + 1;
  A[heap_size] = key;
  i = heap_size;
  while (i>1 and A[PARENT(i) < A[i]){
    exchange A[i] and A[PARENT(i)];
    i = PARENT(i);
  }
}

시간복잡도 O(logn)

 

 

- EXTRACT_MAX()

HEAP-EXTRACT-MAX(A)
  if heap-size[A] < 1
    then error "heap underflow"
  max <-A[1]
  A[1] <- A[heap-size[A]]
  heap-size[A] <- heap-size[A] - 1
  MAX-HEAPIFY(A,1)
  return max

시간복잡도 O(logn)

 

 

더보기

그래도 1학기 때 자료구조 때 배웠던 내용이라 익숙했고, 다시 한 번 들으니까 옛날에 들었을 때보다 이해가 더 빠릿빠릿 되었던 것 같다. 나중에 또 복습하고, 아이패드로 그려서 복습하자. 코드를 보고 이해하는 것에 더 나아가 내가 직접 코드를 구현할 수 있을 때까지 발전하고 싶다.