알고리즘

알고리즘 4 (합병 정렬, 힙 정렬)

zhelddustmq 2024. 6. 28. 11:16

1. 버블 정렬(이전 글)

2. 선택 정렬 (이전 글)

3. 삽입 정렬 (이전 글)

4. 퀵 정렬 (이전 글)

5. 합병 정렬

6. 힙 정렬

 

 

5. 합병 정렬(Merge Sort): 데이터를 낱개로 쪼갠 후 merge 함수를 이용해 병합하는 방식

 

합병 정렬 구현 방식

5-1 merge함수: 두 정렬된 파일을 하나의 파일로 합쳐 빈 파일에 저장하는 함수

A= [1, 2, 3, 5]  # 정렬된 배열 A
B= [4, 6, 7, 8]  # 정렬된 배열 B
C= [] # 두 집합을 합칠 빈 배열 C


        ↓
1단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        1과 4를 비교합니다!
        1 < 4 이므로 1을 C 에 넣습니다.
     C:[1]

           ↓
2단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        2와 4를 비교합니다!
        2 < 4 이므로 2를 C 에 넣습니다.
     C:[1, 2]


              ↓
3단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        3과 4를 비교합니다!
        3 < 4 이므로 3을 C 에 넣습니다.
     C:[1, 2, 3]

                 ↓
3단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        5와 4를 비교합니다!
        5 > 4 이므로 4을 C 에 넣습니다.
     C:[1, 2, 3, 4]

                 ↓
3단계 : [1, 2, 3, 5] 
           ↓
       [4, 6, 7, 8] 
        5와 6을 비교합니다!
        5 < 6 이므로 5을 C 에 넣습니다.
     C:[1, 2, 3, 4, 5]

A 의 모든 원소 삽입 끝났습니다.

이후, B에서 안 들어간 원소인
       [6, 7, 8] 하나씩 C 에 추가해주면 됩니다.
     C:[1, 2, 3, 4, 5, 6, 7, 8]

그러면 A 와 B를 합치면서 정렬할 수 있었습니다.

 

merge함수 구현 코드

#두 정렬된 배열을 받고
def merge(arr1, arr2):
	#새 배열에 정렬해서 리턴
    result = []
    i = j = 0
    #두 배열 비교하면서 작은거 먼저 result에 append한 후 다음 요소로 넘김
    while i < len(arr1) and j < len(arr2):
        if arr1[i] < arr2[j]:
            result.append(arr1[i])
            i += 1
        else:
            result.append(arr2[j])
            j += 1
	#요소 끝까지 비교 안했으면 나머지 넣음(어차피 정렬된 배열이라 append만 하면 됨)
    while i < len(arr1):
        result.append(arr1[i])
        i += 1

	#마찬가지
    while j < len(arr2):
        result.append(arr2[j])
        j += 1

    return result

 

 

5-2 merge함수를 이용한 Merge Sort

def mergesort(lst):
    #데이터를 낱개로 나눠버리면 정렬할것도 없이 하나라 이미 정렬되어있다고 간주
    if len(lst) <= 1:
        return lst

    mid = len(lst) // 2
    #가운데를 기준으로 왼쪽과 오른쪽 정렬하기
    L = lst[:mid]
    R = lst[mid:]
    return merge(mergesort(L), mergesort(R))

 

 

5-3 합병 정렬시간 복잡도:

합병 정렬 알고리즘

[1, 2, 3, 4, 5, 6, 7, 8] 을 정렬하기 위해        
[1, 2, 3, 5] 과 [4, 6, 7, 8] 을 병합했습니다. 

[1, 2, 3, 5] 과 [4, 6, 7, 8] 을 정렬하기 위해 
[3, 5] [1, 2] 과 [6, 8] [4,7] 을 병합했습니다. 

[3, 5] [1, 2] 과 [6, 8] [4,7] 을 정렬하기 위해
[5] [3] [2] [1] [6] [8] [7] [4] 를 병합했습니다.


이를 얼마나 시간이 걸리는지에 대한 관점으로 보면,
T(N) 을 N 의 길이를 정렬하기 위해 걸리는 시간이라고 하겠습니다.

1단계 
[1, 2, 3, 4, 5, 6, 7, 8] 을 정렬하기 위해          
[1, 2, 3, 5] 과 [4, 6, 7, 8] 을 병합했습니다.    

즉, 크기 N 인 배열을 정렬하기 위해
크기 N/2 인 부분 배열 2개를 비교 합니다.
그러면 N/2 * 2 = N 번 비교하게 됩니다

2단계
[1, 2, 3, 5] 과 [4, 6, 7, 8] 을 정렬하기 위해 
[3, 5] [1, 2] 과 [6, 8] [4,7] 을 병합했습니다.

즉, 크기 N/2 인 부분 배열 2개를 정렬하기 위해
크기 N/4 인 부분 배열 4개를 비교 합니다.
그러면 N/4 * 4 = N 번 비교하게 됩니다

3단계
[3, 5] [1, 2] 과 [6, 8] [4,7] 을 정렬하기 위해   
[5] [3] [2] [1] [6] [8] [7] [4] 를 병합했습니다.

즉, 크기 N/4 인 부분 배열 4개를 정렬하기 위해
크기 N/8 인 부분 배열 8개를 비교 합니다.
그러면 N/8 * 2 = N 번 비교하게 됩니다

즉 모든 단계에서 N 만큼 비교를 합니다.
그러면 총 몇 단계인지만 알면 시간 복잡도를 구할 수 있습니다!

크기가 N  → N/2 → N/2^2 → N/2^3 → .... → 1

등비수열의 합으로 인해 k = log_2N

바로 log_2N  번 반복하게 되면 1이 됩니다.
이걸 수식으로 나타내면 
N만큼의 연산을 logN 번 반복한다고 해서 

시간 복잡도는 총 O(Nlog_2N) = O(NlogN) 이 된다! 라고 생각할 수 있습니다.

 

 

6. 힙 정렬(Heap Sort): 힙으로 정렬하는 방법

 

6-1 힙(Heap):  힙은 데이터에서 최대값과 최소값을 빠르게 찾기 위해 고안된 완전 이진 트리(Complete Binary Tree)

      8      Level 0
    6   3    Level 1
     2 1     Level 2  # -> 이진 트리 O 완전 이진 트리 X 이므로 힙이 아닙니다!

      8      Level 0
    6   3    Level 1  # -> 이진 트리 O 완전 이진 트리 O 인데 모든 부모 노드의 값이
   4 2       Level 2  # 자식 노드보다 크니까 힙이 맞습니다!


      8      Level 0
    6   3    Level 1  # -> 이진 트리 O 완전 이진 트리 O 인데 모든 부모 노드의 값이
   4 2 5     Level 2  # 자식 노드보다 크진 않아서 힙이 아닙니다..!

최소 힙과 최대

  • BST(이진탐색트리)와 다르다. 좌, 우 자식의 위치가 대소관계를 반영하지 않음. (그림 참조)
  • 계산 편의를 위해 인덱스는 1부터 사용한다. (parent: x, left: 2x, right: 2x+1)

6-2 최소 힙 - 삽입 시간복잡도와 추출 시간복잡도

원소를 삽입할 때는결국 원소를 맨 밑에 넣어서 꼭대기까지 비교하면서 올리고 있습니다.

완전 이진트리의 최대 높이는 O(log(N))
그러면, 반복하는 최대 횟수도 O(log(N)) 입니다.

즉! 최소 힙의 원소 추가는  (log(N)) 만큼의 시간 복잡도를 가진다고 분석할 수 있습니다.

마찬가지로 원소를 추출할때도 (log(N)) 만큼의 시간 복잡도를 가진다고 분석할 수 있습니다.

 

 

힙 정렬(Heap Sort)는 삽입하는 노드가 총 n개이므로 평균과 최악 모두 시간복잡도는 O(nlogn) 입니다.

 

 

힙 코드 구현

class BinaryMinHeap:
    def __init__(self):
        # 계산 편의를 위해 0이 아닌 1번째 인덱스부터 사용한다.
        self.items = [None]

    def __len__(self):
        # len() 연산을 가능하게 하는 매직 메서드 덮어쓰기(Override).
        return len(self.items) - 1

	#들어온 노드 루트노드로 보내는 함수
    def _percolate_up(self):
        # percolate: 스며들다.
        cur = len(self)
        # left 라면 2*cur, right 라면 2*cur + 1 이므로 parent 는 항상 cur // 2
        parent = cur // 2

        while parent > 0:
            if self.items[cur] < self.items[parent]:
                self.items[cur], self.items[parent] = self.items[parent], self.items[cur]

            cur = parent
            parent = cur // 2

	#힙 트리 유지시키는 함수
    def _percolate_down(self, cur):
        smallest = cur
        left = 2 * cur
        right = 2 * cur + 1

        if left <= len(self) and self.items[left] < self.items[smallest]:
            smallest = left

        if right <= len(self) and self.items[right] < self.items[smallest]:
            smallest = right
		#왼쪽이든 오른쪽이든 자식노드가 부모노드보다 작다면 스위치
        if smallest != cur:
            self.items[cur], self.items[smallest] = self.items[smallest], self.items[cur]
            #반복
            self._percolate_down(smallest)
            
	#노드 넣는 함수
    def insert(self, k):
        self.items.append(k)
        self._percolate_up()
	
    #제일 작은 값 추출 (부모노드)
    def extract(self):
        if len(self) < 1:
            return None

        root = self.items[1]
        self.items[1] = self.items[-1]
        self.items.pop()
        self._percolate_down(1)

        return root

 

6-3 힙 정렬 코드구현(위에 힙 코드가 포함되어 있어야함)

 

def heapsort(lst):
    #최소 힙 인스턴스 만들고
    minheap = BinaryMinHeap()
    
    #요소들 다 넣어준 다음
    for elem in lst:
        minheap.insert(elem)

	#extract하면 항상 루트노드에 최솟값이 들어가기 때문에 뽑은것들 계속 result에 append하면 됨
    return [minheap.extract() for _ in range(len(lst))]