공부/자료구조

[자료구조] 내부 정렬 Internal Sorting

knhoo 2024. 5. 31. 13:50
728x90

정렬 (Sorting)

정의

  • n개의 요소를 오름차순으로 재배열하는 것
  • 입력: 7, 3, 6, 2, 1
  • 정렬 후 출력: 1, 2, 3, 6, 7

정렬 방법

  • 내부 정렬 (Internal Sorting)
    • PQ 기반 정렬 (PQ based sorting)
    • 선택 정렬 (Selection sort)
    • 삽입 정렬 (Insertion sort)
    • 힙 정렬 (Heap sort)
  • 외부 정렬 (External Sorting)

Priority Queue(PQ)

정의

  • 우선 순위가 부여된 요소들을 저장하는 추상 데이터 타입으로, 임의의 요소 삽입을 지원하지만 요소 제거는 우선 순위에 따라 이루어진다.

특징

  • 스택, 큐, 리스트, 트리와 같은 위치 기반 데이터 구조와 근본적으로 다름.
  • 기존의 데이터 구조는 요소를 특정 위치에 저장하며, 이는 삽입 및 삭제 방법에 의해 결정됨
  • 우선 순위 큐 ADT는 요소들을 그들의 우선 순위에 따라 저장하며, 사용자에게 "위치" 개념을 노출하지 않는다.
  • 우선 순위 큐에서는 우선 순위가 가장 높은 요소가 언제든지 제거될 수 있다.

Sorting with a Priority Queue

우선 순위 큐를 사용하여 비교 가능한 요소 집합을 정렬할 수 있다.

  1. 삽입 연산을 통해 요소들을 하나씩 삽입
  2. removeMin 연산을 통해 요소들을 정렬된 순서로 제거한다.
Algorithm PQ-Sort(S, C)
Input sequence S, comparator C for
the elements of S
Output sequence S sorted in
increasing order according to C
P <- priority queue with
comparator C
while S.isEmpty ()
e <- S.removeFirst ()
P.insert (e, ⊘)
while P.isEmpty()
e <- P.removeMin().getKey()
S.addLast(e)

우선 순위 큐(PQ)의 구현

PQ 구현 방법

  • 시퀀스를 이용한 PQ
    • 단순하지만 효율적이지 않음
    • 두 가지 정렬 알고리즘 사용
      • 선택 정렬 (Selection sort)
      • 삽입 정렬 (Insertion sort)
  • 힙을 기반으로 한 PQ
    • 효율적인 구현
    • O(log n) 시간 내에 PQ 연산을 지원
    • 힙 정렬 (Heap-sort) 사용

시퀀스를 기반으로 한 우선 순위 큐

정렬되지 않은 리스트로 구현

성능:

  • insert : O(1)
    • 항목을 시퀀스의 시작이나 끝에 삽입할 수 있기 때문
  • removeMin , min : O(n)
    • 가장 작은 키를 찾기 위해 전체 시퀀스를 탐색해야 하기 때문

정렬된 리스트로 구현

성능:

  • insert : O(n)
    • 항목을 삽입할 위치를 찾아야 하기 때문
  • removeMin min : O(1)
    • 가장 작은 키가 항상 리스트의 시작에 있기 때문

Selection-Sort

정의:

  • 선택 정렬은 우선 순위 큐가 정렬되지 않은 시퀀스로 구현된 PQ-정렬의 변형

선택 정렬의 실행 시간:

  1. n개의 삽입 연산으로 우선 순위 큐에 요소를 삽입하는데 O(n) 시간이 소요
  2. n개의 removeMin 연산으로 우선 순위 큐에서 정렬된 순서로 요소를 제거하는데 걸리는 시간:

$O(1+2+⋯+(n−1)+n)=O(i=1∑ni)
O(1+2+⋯+(n−1)+n)=O(∑i=1ni)O(1 + 2 + \cdots + (n - 1) + n) = O\left(\sum_{i=1}^{n} i\right)$

결론:

  • O(n^2) 시간이 소요

Selection-Sort 예시

입력:(7,4,8,2,5,3,9)(7, 4, 8, 2, 5, 3, 9)(7,4,8,2,5,3,9)

Phase 1: 삽입 연산

  • 시퀀스 S에서 우선 순위 큐 P로 요소를 하나씩 삽입
단계 Sequence S Priority Queue P
(a) (4, 8, 2, 5, 3, 9) (7)
(b) (8, 2, 5, 3, 9) (7, 4)
(c) (2, 5, 3, 9) (7, 4, 8)
(d) (5, 3, 9) (7, 4, 8, 2)
(e) (3, 9) (7, 4, 8, 2, 5)
(f) (9) (7, 4, 8, 2, 5, 3)
(g) () (7, 4, 8, 2, 5, 3, 9)

Phase 2: 제거 연산

  • 우선 순위 큐 P에서 요소를 제거하여 정렬된 순서로 시퀀스 S에 삽입
단계 Sequence S Priority Queue P
(a) (2) (7, 4, 8, 5, 3, 9)
(b) (2, 3) (7, 4, 8, 5, 9)
(c) (2, 3, 4) (7, 8, 5, 9)
(d) (2, 3, 4, 5) (7, 8, 9)
(e) (2, 3, 4, 5, 7) (8, 9)
(f) (2, 3, 4, 5, 7, 8) (9)
(g) (2, 3, 4, 5, 7, 8, 9) ()

Phase 1 : 요소들을 순차적으로 우선 순위 큐에 삽입

Phase 2 : 요소들을 정렬된 순서로 제거하여 최종적으로 정렬된 시퀀스

Insertion-Sort

정의:

  • 삽입 정렬은 우선 순위 큐가 정렬된 시퀀스로 구현된 PQ-정렬의 변형

삽입 정렬의 실행 시간:

  1. n개의 삽입 연산으로 우선 순위 큐에 요소를 삽입하는데 걸리는 시간:

$O(1+2+⋯+(n−1)+n)=O(i=1∑ni)
O(1+2+⋯+(n−1)+n)=O(∑i=1ni)O(1 + 2 + \cdots + (n - 1) + n) = O\left(\sum_{i=1}^{n} i\right)$

  1. n개의 removeMin 연산으로 우선 순위 큐에서 정렬된 순서로 요소를 제거하는데 O(n) 시간이 소요.

결론:

  • O(n^2) 시간이 소요.

Insertion-Sort 예시

Phase 1: 삽입 연산

  • 시퀀스 S에서 우선 순위 큐 P로 요소를 하나씩 삽입
단계 Sequence S Priority Queue P
(a) (4, 8, 2, 5, 3, 9) (7)
(b) (8, 2, 5, 3, 9) (4, 7)
(c) (2, 5, 3, 9) (4, 7, 8)
(d) (5, 3, 9) (2, 4, 7, 8)
(e) (3, 9) (2, 4, 5, 7, 8)
(f) (9) (2, 3, 4, 5, 7, 8)
(g) () (2, 3, 4, 5, 7, 8, 9)

Phase 2: 제거 연산

  • 우선 순위 큐 P에서 요소를 제거하여 정렬된 순서로 시퀀스 S에 삽입
단계 Sequence S Priority Queue P
(a) (2) (3, 4, 5, 7, 8, 9)
(b) (2, 3) (4, 5, 7, 8, 9)
(c) (2, 3, 4) (5, 7, 8, 9)
(d) (2, 3, 4, 5) (7, 8, 9)
(e) (2, 3, 4, 5, 7) (8, 9)
(f) (2, 3, 4, 5, 7, 8) (9)
(g) (2, 3, 4, 5, 7, 8, 9) ()

Phase 1 : 요소들을 순차적으로 우선 순위 큐에 삽입

Phase 2 : 요소들을 정렬된 순서로 제거하여 최종적으로 정렬된 시퀀스

In-Place Selection-Sort 및 Insertion-Sort

외부 데이터 구조를 사용하지 않고, 선택 정렬과 삽입 정렬을 제자리에서 구현할 수 있습니다. 입력 시퀀스의 일부가 우선 순위 큐로 사용.

In-Place Insertion-Sort

  • 시퀀스의 초기 부분을 정렬된 상태로 유지
  • 시퀀스를 수정하는 대신, 교환(swap)을 사용

Quick-Sort

Quick-sort는 분할 정복(divide-and-conquer) 패러다임에 기반한 랜덤화된 정렬 알고리즘

  1. 분할 (Divide): 랜덤으로 x(피벗)을 선택하고 S를 다음과 같이 분할.
    • L: x보다 작은 요소들
    • E: x와 같은 요소들
    • G: x보다 큰 요소들
  2. 재귀 (Recur): L과 G를 각각 정렬.
  3. 정복 (Conquer): L, E, G를 합친다.

Quick-Sort: Partition

입력 시퀀스를 다음과 같이 분할함:

  • 각 요소 y를 순차적으로 S에서 제거함
  • y를 피벗 x와 비교하여 L, E, G 중 하나에 삽입함

각 삽입과 제거는 시퀀스의 시작이나 끝에서 일어나므로 O(1) 시간이 걸림
따라서, quick-sort의 분할 단계는 O(n) 시간이 걸림

Algorithm partition(S, p)
Input sequence S, position p of pivot
Output subsequences L, E, G of the
elements of S less than, equal to,
or greater than the pivot, resp.
L, E, G  empty sequences
x  S.remove(p)
while S.isEmpty()
y  S.remove(S.first())
if y < x
L.addLast(y)
else if y = x
E.addLast(y)
else { y > x }
G.addLast(y)
return L, E, G

Quick-Sort Tree

Quick-sort의 실행은 이진 트리로 묘사됨

  • 각 노드는 quick-sort의 재귀 호출을 나타내며 다음을 저장함:
    • 실행 전의 정렬되지 않은 시퀀스와 그 피벗
    • 실행 후의 정렬된 시퀀스
  • 루트는 초기 호출을 나타냄
  • 리프는 크기가 0 또는 1인 부분 시퀀스에 대한 호출을 나타냄

최악의 경우 실행 시간

Quick-sort의 최악의 경우는 피벗이 유일한 최소 또는 최대 요소일 때 발생함

  • L 또는 G 중 하나의 크기가 n-1이고, 다른 하나의 크기는 0임

  • 실행 시간은 다음의 합에 비례함:
    n+(n−1)+...+2+1

    n+(n−1)+...+2+1n + (n - 1) + ... + 2 + 1

따라서, Quick-sort의 최악의 경우 실행 시간은 O(n^2)임

Expected Running Time

Quick-sort가 크기 s인 시퀀스에 대한 재귀 호출을 고려함

  • 좋은 호출 (Good call): L과 G의 크기가 각각 3s/4보다 작은 경우
  • 나쁜 호출 (Bad call): L과 G 중 하나의 크기가 3s/4보다 큰 경우

호출이 좋은 경우의 확률은 1/2임

  • 가능한 피벗의 절반이 좋은 호출을 유발함

In-Place Quick-Sort

Quick-sort는 제자리(in-place)에서 실행되도록 구현할 수 있음

Partition 단계

  • 입력 시퀀스의 요소들을 다음과 같이 재배열하기 위해 교체 연산을 사용함
    • 피벗보다 작은 요소들은 h보다 작은 순위를 가짐
    • 피벗과 같은 요소들은 h와 k 사이의 순위를 가짐
    • 피벗보다 큰 요소들은 k보다 큰 순위를 가짐

재귀 호출

  • h보다 작은 순위를 가진 요소들을 고려함
  • k보다 큰 순위를 가진 요소들을 고려함

In-Place Partitioning

Partition을 수행하기 위해 두 개의 인덱스를 사용하여 S를 L과 E ∪ G로 나눔 (유사한 방법으로 E ∪ G를 E와 G로 나눌 수 있음).

절차

  • j와 k가 교차할 때까지 반복:
    • j를 오른쪽으로 이동하여 x보다 큰 요소를 찾음
    • k를 왼쪽으로 이동하여 x보다 작은 요소를 찾음
    • 인덱스 j와 k에 있는 요소를 교환

단계 요약

  1. j를 오른쪽으로 스캔하여 x보다 큰 요소를 찾음.
  2. k를 왼쪽으로 스캔하여 x보다 작은 요소를 찾음.
  3. 인덱스 j와 k의 요소를 교환
  4. j와 k가 교차할 때까지 반복

Summary of Sorting Algorithms

Algorithm Time Notes
Selection-Sort O(n2)O(n^2)O(n2) - in-place
- slow (good for small inputs)
Insertion-Sort O(n2)O(n^2)O(n2) - in-place
- slow (good for small inputs)
Quick-Sort O(nlog⁡n)O(n \log n)O(nlogn) expected - in-place, randomized
- fastest (good for large inputs)

Insight Penetration: Why is Quick-Sort Faster?

Quick-Sort의 속도가 다른 O(nlog⁡n)O(n \log n)O(nlogn) 알고리즘보다 빠른 이유

  • 일반적으로, Quick-Sort는 다른 O(nlogn) 알고리즘보다 실질적으로 훨씬 빠름. 이는 대부분의 아키텍처에서 내부 루프가 효율적으로 구현될 수 있기 때문이며, 대부분의 실제 데이터에서 설계 선택을 통해 O(n2) 시간이 필요한 경우를 최소화할 수 있기 때문임.

    O(nlog⁡n)O(n \log n)

    O(n2)O(n^2)

  • 또한, Quick-Sort는 메모리 계층 구조를 잘 활용하며, 가상 메모리와 사용 가능한 캐시를 완벽히 활용함. Quick-Sort는 제자리 정렬이 아니고 보조 메모리를 사용하지만, 현대 컴퓨터 아키텍처에 매우 적합함.

Quick-Sort의 내부 루프 효율성

  • 대부분의 컴퓨터 아키텍처에서 매우 효율적임.
    • 메모리 접근이 국한됨.
    • CPU 캐시 히트 비율이 크게 증가할 수 있음.

Insight Penetration

In-Place Sorting

  • 추가적인 메모리를 거의 사용하지 않고 정렬을 수행
    • 예: 삽입 정렬 (Insertion Sort)
      • 입력 데이터와 루프 변수를 저장하기 위해 추가 메모리를 사용
    • 퀵 정렬 (Quick Sort)
      • 재귀 호출을 위해 추가 메모리(스택)를 사용
      • 재귀 호출 횟수는 입력 데이터 크기에 비례하여 일정하지 않음
      • 그러나 실질적으로는 재귀 호출에 상대적으로 적은 메모리를 사용하므로 제자리 정렬 알고리즘으로 분류될 수 있음

Online Sorting

  • 온라인 정렬 알고리즘은 데이터 시퀀스를 한 번에 제공받지 않고 시간 순서대로 제공받은 데이터를 정렬할 수 있음
    • 예: 병합 정렬 (Merge Sort)
      • 병합 정렬은 대표적인 온라인 정렬 알고리즘

Divide-and-Conquer

Divide-and-conquer는 일반적인 알고리즘 설계 패러다임임:

  1. Divide: 입력 데이터 S를 두 개의 서로 겹치지 않는 부분 집합 S1과 S2로 나눔
  2. Recur: S1과 S2와 관련된 하위 문제를 해결
  3. Conquer: S1과 S2의 해를 결합하여 S에 대한 해를 만듦

재귀의 기본 사례는 크기가 0 또는 1인 하위 문제

Merge-Sort

Merge-sort는 divide-and-conquer 패러다임에 기반한 정렬 알고리즘:

  • 힙 정렬과 유사:
    • 비교 연산자를 사용
    • O(n log n) 실행 시간을 가짐
  • 힙 정렬과 다른점:
    • 보조 우선 순위 큐를 사용하지 않음
    • 데이터를 순차적으로 접근함 (디스크에 있는 데이터를 정렬하는 데 적합함)

Merge-Sort

Merge-sort는 n개의 요소를 가진 입력 시퀀스 S에 대해 다음 세 가지 단계로 구성됨:

  1. Divide: S를 대략 n/2개의 요소를 가진 두 개의 시퀀스 S1과 S2로 분할함
  2. Recur: S1과 S2를 재귀적으로 정렬함
  3. Conquer: S1과 S2를 하나의 정렬된 시퀀스로 병합함
Algorithm mergeSort(S, C)
Input sequence S with n
elements, comparator C
Output sequence S sorted
according to C
if S.size() > 1
(S1, S2) <- partition(S, n/2)
mergeSort(S1, C)
mergeSort(S2, C)
S <- merge(S1, S2)

Merging Two Sorted Sequences

Merge-sort의 정복 단계는 두 개의 정렬된 시퀀스 A와 B를 병합하여 A와 B의 요소들을 포함하는 정렬된 시퀀스 S를 만드는 것임

  • 두 개의 정렬된 시퀀스 A와 B를 병합하여 S를 생성함
  • 각각 n/2 요소를 가진 두 정렬된 시퀀스를 병합하는 작업은 이중 연결 리스트(doubly linked list)를 사용하여 구현되며, O(n) 시간이 걸림
Algorithm merge(A, B)
Input sequences A and B with
n/2 elements each
Output sorted sequence of A ∪ B
S <- empty sequence
while A.isEmpty() ^ B.isEmpty()
if A.first().element() < B.first().element()
S.addLast(A.remove(A.first()))
else
S.addLast(B.remove(B.first()))
while A.isEmpty()
S.addLast(A.remove(A.first()))
while B.isEmpty()
S.addLast(B.remove(B.first()))
return S

Merge-Sort Tree

Merge-sort의 실행은 이진 트리로 묘사됨

  • 각 노드는 merge-sort의 재귀 호출을 나타내며 다음을 저장함:
    • 실행 전의 정렬되지 않은 시퀀스와 그 분할
    • 실행 후의 정렬된 시퀀스
  • 루트는 초기 호출을 나타냄
  • 리프 노드는 크기가 0 또는 1인 부분 시퀀스에 대한 호출을 나타냄

이진 트리 구조는 merge-sort의 단계별 과정을 시각적으로 나타내며, 각 단계에서 시퀀스가 어떻게 분할되고 병합되는지를 보여줌.

Analysis of Merge-Sort

  • Height of the Merge-Sort Tree: O(log⁡n)O(\log n)O(logn)

    • 각 재귀 호출에서 시퀀스를 반으로 나눔
  • Work Done at Nodes of Depth iii: O(n)O(n)O(n)

    • 크기 2in인 2^i개의 시퀀스를 분할하고 병합함

      n2i\frac{n}{2^i}

    • 2^i+1개의 재귀 호출을 수행함

따라서, Merge-Sort의 총 실행 시간은 O(nlog⁡n)O(n \log n)O(nlogn)임

정렬 알고리즘 요약

알고리즘 시간 복잡도 비고
선택 정렬 O(n2)O(n^2)O(n2) - 제자리 정렬
- 느림 (작은 입력에 적합 < 1K)
삽입 정렬 O(n2)O(n^2)O(n2) - 제자리 정렬
- 느림 (작은 입력에 적합 < 1K)
퀵 정렬 O(nlog⁡n)O(n \log n)O(nlogn) 예상 - 제자리 정렬, 랜덤화
- 가장 빠름 (큰 입력에 적합 1K–1M)
힙 정렬 O(nlog⁡n)O(n \log n)O(nlogn) - 제자리 정렬
- 빠름 (큰 입력에 적합 1K–1M)
병합 정렬 O(nlog⁡n)O(n \log n)O(nlogn) - 온라인 정렬 (순차 데이터 접근)
- 빠름 (매우 큰 입력에 적합 > 1M)
728x90