[자료구조] 힙(Heap) 2

Max Heap의 파이썬 구현

파이썬의 Heapq 라이브러리

사실 파이썬에는 Heap을 위한 라이브러리가 구현되어 있긴 하다.

import heapq

Heap = []

heapq.heappush(Heap, 1)
heapq.heappush(Heap, 5)
heapq.heappush(Heap, 10)

print(Heap)
# 결과 : [1, 10, 5]

heapq.heappop(Heap)
print(Heap)
# 결과 : [5, 10]

heapq 라이브러리를 사용하면 우선순위 큐에 대한 코드를 쉽게 짤 수 있다.
하지만 직접 구현해서 해보는 것도 참을 순 없지.

파이썬 코드로 Max Heap 구현하기

코드 참고 :

class heap:
    def __init__(self, data):
        self.heap_array = list() # 인덱스 계산을 편하게 하기 위함.
        self.heap_array.append(None) # (0번 인덱스 비워두기)
        self.heap_array.append(data)

    def move_up(self, inserted_idx):
        if inserted_idx <= 1:
            return False

        parent_idx = inserted_idx // 2
        if self.heap_array[inserted_idx] > self.heap_array[parent_idx]:
            return True
        else:
            return False
    
    def insert(self, data):
        # 진짜 만약에 힙의 길이가 0이면?
        if len(self.heap_array) == 0:
            self.heap_array = list()
            self.heap_array = None # (0번 인덱스 비워두기)
            self.heap_array.append(data)
            return True
        
        # 자연스럽게 이진트리 구조로 만들어지기 때문에 추가해주면 됨.
        self.heap_array.append(data)

        # 인덱스값과 리스트의 길이를 맞추기 위해서 0번 인덱스에
	    # None을 삽입했으므로, 실제 idx값을 구하려면 1을 뺄 것.
        inserted_idx = len(self.heap_array) - 1

	# swap 하는 과정
        while self.move_up(inserted_idx):
	        parent_idx = inserted_idx // 2
	        self.heap_array[inserted_idx], self.heap_array[parent_idx] = self.heap_array[parent_idx], self.heap_array[inserted_idx]
	        inserted_idx = parent_idx

        return True

우선 삽입부에 대해서 구현을 하자면 이렇게 할 수 있을 것 같다.

맥스힙

힙은 인덱스 구조가 위와 같이 나타나는데, 왼쪽 자식노드는 부모노드의 2배, 오른쪽 자식 노드는 부모노드의 2배 + 1 이라는 것을 알 수 있다.
왼쪽 자식노드부터 순서대로 채워지기 때문에 가능한 구조이다.

그래서 데이터를 채우고 나서, index를 계산하여 해당 index 내 값을 비교하면서 재배치를 하는 형태로 코드가 짜여져있다.

# Test Code

My = heap(10)
My.insert(5)
print(My.heap_array)
My.insert(8)
print(My.heap_array)
My.insert(12)
print(My.heap_array)
My.insert(4)
print(My.heap_array)
My.insert(11)
print(My.heap_array)
My.insert(15)
print(My.heap_array)

# 출력 결과
[None, 10, 5]
[None, 10, 5, 8]
[None, 12, 10, 8, 5]
[None, 12, 10, 8, 5, 4]
[None, 12, 10, 11, 5, 4, 8]
[None, 15, 10, 12, 5, 4, 8, 11]

시험 해보면 위와 같다.

# 힙 데이터의 삭제(pop)
    
    def move_down(self, popped_idx):
        left_child_popped_idx = popped_idx * 2
        right_child_popped_idx = popped_idx * 2 + 1

        # Case 1 : 왼쪽 자식노드도 없을 때
        if left_child_popped_idx >= len(self.heap_array):
            return False
        # Case 2 : 오른쪽 자식 노드만 없을 때
        elif right_child_popped_idx >= len(self.heap_array):
            if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                return True
            else:
                return False
        # Case 3 : 왼쪽, 오른쪽 자식노드 모두 있을 때
        else:
            if self.heap_array[left_child_popped_idx] > self.heap_array[right_child_popped_idx]:
                if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                    return True
                else:
                    return False
            else:
                if self.heap_array[popped_idx] < self.heap_array[right_child_popped_idx]:
                    return True
                else:
                    return False
    
    def pop(self):
        if len(self.heap_array) <= 1:
            return None

        returned_data = self.heap_array[1]
        # 맨 끝 데이터를 루트노드로 이동.
        self.heap_array[1] = self.heap_array[-1]
        del self.heap_array[-1]
        popped_idx = 1

        while self.move_down(popped_idx):
            left_child_popped_idx = popped_idx * 2
            right_child_popped_idx = popped_idx * 2 + 1


            if right_child_popped_idx >= len(self.heap_array):
                if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                    self.heap_array[popped_idx], self.heap_array[left_child_popped_idx] = self.heap_array[left_child_popped_idx], self.heap_array[popped_idx]
                    popped_idx = left_child_popped_idx
            else:
                if self.heap_array[left_child_popped_idx] > self.heap_array[right_child_popped_idx]:
                    if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                        self.heap_array[popped_idx], self.heap_array[left_child_popped_idx] = self.heap_array[left_child_popped_idx], self.heap_array[popped_idx]
                        popped_idx = left_child_popped_idx
                else:
                    if self.heap_array[popped_idx] < self.heap_array[right_child_popped_idx]:
                        self.heap_array[popped_idx], self.heap_array[right_child_popped_idx] = self.heap_array[right_child_popped_idx], self.heap_array[popped_idx]
                        popped_idx = right_child_popped_idx
                
        return returned_data

그리고 삭제과정도 위와 같다.

흐름을 요약하면,

  • 제일 앞에 있는 데이터를 pop 할 테니 따로 빼둔다.
  • 맨 끝에 있는 데이터를 루트노드로 이동한다.
  • 그리고 Heap의 맨 뒤를 삭제한다.
  • index 계산을 하여 값들을 재배치한다.
# Test Code

My.heap_array = [None, 15, 10, 12, 5, 4, 8, 11]

My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)

# 출력 결과

[None, 12, 10, 11, 5, 4, 8]
[None, 11, 10, 8, 5, 4]
[None, 10, 5, 8, 4]

순서대로 15, 12, 11이 삭제되었고, 부모노드의 값이 자식노드의 값보다 크도록 재배치 된 것도 확인할 수 있다.

코드 고치기

코드를 치면서 아쉬웠던 점은,

  • heap_array를 출력하면 맨 앞의 None이 거슬린다.
  • 중복코드가 많이 있다.

조금 더 깔끔하게 만들어보자.

우선, None이 있는 이유는 단순히 인덱스 계산을 편하게 하기 위함이다.

그렇다면, 인덱스 계산과정 앞에다가 잠깐 맨 앞에 None을 넣어주고 다 끝나면 None을 삭제하면 되는 것으로 해결된다.

    def make_heap(self, data):
        self.heap_array = list()
        self.heap_array.append(data)
        return

    def __init__(self, data):
        self.make_heap(data) 

    def exchange(self, list_input, idx_1, idx_2):
        list_input[idx_1], list_input[idx_2] = list_input[idx_2], list_input[idx_1]
        return list_input, idx_2

    def insert(self, data):
        # 진짜 만약에 힙의 길이가 0이면?
        if len(self.heap_array) == 0:
            self.make_heap(data)
            return True
        
        # 자연스럽게 이진트리 구조로 만들어지기 때문에 추가해주면 됨.
        self.heap_array.append(data)

        # 인덱스값의 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)
	    # None을 삽입했으므로, 실제 idx값을 구하려면 1을 뺄 것.
        inserted_idx = len(self.heap_array) - 1

        ...

따라서, 중복 코드도 수정할 겸 make_heap코드를 따로 만들었고, __init__도 위와 같이 수정하였으며, insert 함수의 초반부를 위와 같이 수정했다.

# 계산이 끝났으니 맨 앞에 넣어두었던 None 삭제
        del self.heap_array[0]
# Test Code

My = heap(10)
My.insert(5)
print(My.heap_array)
My.insert(8)
print(My.heap_array)
My.insert(12)
print(My.heap_array)
My.insert(4)
print(My.heap_array)
My.insert(11)
print(My.heap_array)
My.insert(15)
print(My.heap_array)

# 출력 결과
[10, 5]
[10, 5, 8]
[12, 10, 8, 5]
[12, 10, 8, 5, 4]
[12, 10, 11, 5, 4, 8]
[15, 10, 12, 5, 4, 8, 11]

수정 전과 똑같이 정상적으로 구동한다!

    # pop 함수 초반부
    def pop(self):
        if len(self.heap_array) < 1:
            return None
        
        # 계산 과정에서 인덱스 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)

        ...


    # pop 함수 후반부 swap 과정 이후
    del self.heap_array[0]
        return returned_data

pop 함수도 위와 같이 None을 추가했다가 다시 빼는 과정을 넣어주었다.

# Test Code

My.heap_array = [15, 10, 12, 5, 4, 8, 11]

My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)

# 출력 결과
[12, 10, 11, 5, 4, 8]
[11, 10, 8, 5, 4]
[10, 5, 8, 4]

똑같이 작동이 잘 되는 것을 볼 수 있다!

이제 다음으로 중복코드를 없애보자.

인덱스를 계산하고, 값을 서로 비교하는 과정에서 중복코드가 많이 발생했다.
그런데 여기서 중복된 부분을 살펴보니, 인덱스를 계산하고, 부모노드보다 자식노드의 값이 더 크면 두 값을 서로 바꿔주는 것이 핵심이었다.

    def insert(self, data):
        # 진짜 만약에 힙의 길이가 0이면?
        if len(self.heap_array) == 0:
            self.make_heap(data)
            return True
        
        # 자연스럽게 이진트리 구조로 만들어지기 때문에 추가해주면 됨.
        self.heap_array.append(data)

        # 인덱스값의 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)
	    # None을 삽입했으므로, 실제 idx값을 구하려면 1을 뺄 것.
        inserted_idx = len(self.heap_array) - 1

	# swap 하는 과정
        while 1:
            parent_idx = inserted_idx // 2
            if inserted_idx <= 1:
                break
            if self.heap_array[inserted_idx] > self.heap_array[parent_idx]:
                self.heap_array[inserted_idx], self.heap_array[parent_idx] = self.heap_array[parent_idx], self.heap_array[inserted_idx]
            else:
                break
            inserted_idx = parent_idx
        
        # 계산이 끝났으니 맨 앞에 넣어두었던 None 삭제
        del self.heap_array[0]

        return True

따라서, 원래 있었던 move_up 함수를 삭제했고, 무한루프의 while구문 내의 if-break문으로 대체하였다.

그리고 swap이 끝난 지점에서 None을 다시 빼주는 코드도 추가 했다.

또한, 삽입과 삭제를 포함한 전체 과정 중 swap하는 파트, 즉

array[idx_1], array[idx_2] = array[idx_2], array[idx_1]
idx_1 = idx_2

와 같은 형식을 띄는 부분은 다른 함수를 정의하였다. 아래 코드처럼.

    def exchange(self, list_input, idx_1, idx_2):
        list_input[idx_1], list_input[idx_2] = list_input[idx_2], list_input[idx_1]
        return list_input, idx_2
    
self.heap_array, inserted_idx = self.exchange(self.heap_array, inserted_idx, parent_idx)

이런 식으로 작동하게 된다.

따라서 이렇게 수정된 삽입과정 코드는 아래와 같다.

    def make_heap(self, data):
        self.heap_array = list()
        self.heap_array.append(data)
        return
    def __init__(self, data):
        self.make_heap(data) 

    def exchange(self, list_input, idx_1, idx_2):
        list_input[idx_1], list_input[idx_2] = list_input[idx_2], list_input[idx_1]
        return list_input, idx_2

    def insert(self, data):
        # 진짜 만약에 힙의 길이가 0이면?
        if len(self.heap_array) == 0:
            self.make_heap(data)
            return True
        
        # 자연스럽게 이진트리 구조로 만들어지기 때문에 추가해주면 됨.
        self.heap_array.append(data)

        # 인덱스값의 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)
	    # None을 삽입했으므로, 실제 idx값을 구하려면 1을 뺄 것.
        inserted_idx = len(self.heap_array) - 1

	# swap 하는 과정
        while 1:
            if inserted_idx <= 1:
                break
            parent_idx = inserted_idx // 2
            if self.heap_array[inserted_idx] > self.heap_array[parent_idx]:
                self.heap_array, inserted_idx = self.exchange(self.heap_array, inserted_idx, parent_idx)
            else:
                break
        
        # 계산이 끝났으니 맨 앞에 넣어두었던 None 삭제
        del self.heap_array[0]

        return True

그리고 테스트도 해보아야지.

# Test Code

My = heap(10)
My.insert(5)
print(My.heap_array)
My.insert(8)
print(My.heap_array)
My.insert(12)
print(My.heap_array)
My.insert(4)
print(My.heap_array)
My.insert(11)
print(My.heap_array)
My.insert(15)
print(My.heap_array)

# 출력 결과

[10, 5]
[10, 5, 8]
[12, 10, 8, 5]
[12, 10, 8, 5, 4]
[12, 10, 11, 5, 4, 8]
[15, 10, 12, 5, 4, 8, 11]

똑같은 조건으로 테스트해보았고, 역시 같은 결과가 출력되었다.

삭제코드도 수정해보자.

    def pop(self):
        if len(self.heap_array) < 1:
            return None
        
        # 계산 과정에서 인덱스 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)
        
        returned_data = self.heap_array[1]
        # 맨 끝 데이터를 루트노드로 이동.
        self.heap_array[1] = self.heap_array[-1]
        del self.heap_array[-1]
        popped_idx = 1

        while 1:
            left_child_popped_idx = popped_idx * 2
            right_child_popped_idx = popped_idx * 2 + 1
            
            if left_child_popped_idx >= len(self.heap_array): # list 끝에 다다랐기 때문에 루프 중단
                break

            elif right_child_popped_idx >= len(self.heap_array):
                if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                    self.heap_array[popped_idx], self.heap_array[left_child_popped_idx] = self.heap_array[left_child_popped_idx], self.heap_array[popped_idx]
                    popped_idx = left_child_popped_idx
                else:
                    break
            else:
                if self.heap_array[left_child_popped_idx] > self.heap_array[right_child_popped_idx]:
                    if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                        self.heap_array[popped_idx], self.heap_array[left_child_popped_idx] = self.heap_array[left_child_popped_idx], self.heap_array[popped_idx]
                        popped_idx = left_child_popped_idx
                    else:
                        break
                else:
                    if self.heap_array[popped_idx] < self.heap_array[right_child_popped_idx]:
                        self.heap_array[popped_idx], self.heap_array[right_child_popped_idx] = self.heap_array[right_child_popped_idx], self.heap_array[popped_idx]
                        popped_idx = right_child_popped_idx
                    else:
                        break
        
        del self.heap_array[0]
        return returned_data

이 부분에서도 역시 move_down 함수를 삭제하고 대신에 while 1문 속의 조건문으로 산입하였다. 또한, 배열 속 아이템을 교환해주는 함수도 따로 빼주었다.

따라서 삭제 함수를 수정한 코드는 아래와 같다.

    # 힙 데이터의 삭제(pop)
    
    def pop(self):
        if len(self.heap_array) < 1:
            return None
        
        # 계산 과정에서 인덱스 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)
        
        returned_data = self.heap_array[1]
        # 맨 끝 데이터를 루트노드로 이동.
        self.heap_array[1] = self.heap_array[-1]
        del self.heap_array[-1]
        popped_idx = 1

        while 1:
            left_child_popped_idx = popped_idx * 2
            right_child_popped_idx = popped_idx * 2 + 1
            
            if left_child_popped_idx >= len(self.heap_array): # list 끝에 다다랐기 때문에 루프 중단
                break

            elif right_child_popped_idx >= len(self.heap_array):
                if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                    self.heap_array, popped_idx = self.exchange(self.heap_array, popped_idx, left_child_popped_idx)
                else:
                    break
            else:
                if self.heap_array[left_child_popped_idx] > self.heap_array[right_child_popped_idx]:
                    if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                        self.heap_array, popped_idx = self.exchange(self.heap_array, popped_idx, left_child_popped_idx)
                    else:
                        break
                else:
                    if self.heap_array[popped_idx] < self.heap_array[right_child_popped_idx]:
                        self.heap_array, popped_idx = self.exchange(self.heap_array, popped_idx, right_child_popped_idx)
                    else:
                        break
        
        del self.heap_array[0]
        return returned_data

코드 수정 전과 똑같은 조건으로 테스트를 해보았다.

# Test Code (삽입 과정 진행 직후)
# Test Case : [15, 10, 12, 5, 4, 8, 11]

My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)
My.pop()
print(My.heap_array)

# 출력 결과

[12, 10, 11, 5, 4, 8]
[11, 10, 8, 5, 4]
[10, 5, 8, 4]
[8, 5, 4]
[5, 4]

오케이. 코드 수정 전과 일치한다.

수정한 나의 Max Heap 코드

# Max Heap 구현
class heap:
    
    def make_heap(self, data):
        self.heap_array = list()
        self.heap_array.append(data)
        return
    def __init__(self, data):
        self.make_heap(data) 

    def exchange(self, list_input, idx_1, idx_2):
        list_input[idx_1], list_input[idx_2] = list_input[idx_2], list_input[idx_1]
        return list_input, idx_2

    def insert(self, data):
        # 진짜 만약에 힙의 길이가 0이면?
        if len(self.heap_array) == 0:
            self.make_heap(data)
            return True
        
        # 자연스럽게 이진트리 구조로 만들어지기 때문에 추가해주면 됨.
        self.heap_array.append(data)

        # 인덱스값의 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)
	    # None을 삽입했으므로, 실제 idx값을 구하려면 1을 뺄 것.
        inserted_idx = len(self.heap_array) - 1

	# swap 하는 과정
        while 1:
            if inserted_idx <= 1:
                break
            parent_idx = inserted_idx // 2
            if self.heap_array[inserted_idx] > self.heap_array[parent_idx]:
                self.heap_array, inserted_idx = self.exchange(self.heap_array, inserted_idx, parent_idx)
            else:
                break
        
        # 계산이 끝났으니 맨 앞에 넣어두었던 None 삭제
        del self.heap_array[0]

        return True

    # 힙 데이터의 삭제(pop)
    
    def pop(self):
        if len(self.heap_array) < 1:
            return None
        
        # 계산 과정에서 인덱스 계산을 편하게 하기 위해서
        # 계산 과정시에는 리스트 맨 앞에 None을 임의로 추가
        self.heap_array.insert(0, None)
        
        returned_data = self.heap_array[1]
        # 맨 끝 데이터를 루트노드로 이동.
        self.heap_array[1] = self.heap_array[-1]
        del self.heap_array[-1]
        popped_idx = 1

        while 1:
            left_child_popped_idx = popped_idx * 2
            right_child_popped_idx = popped_idx * 2 + 1
            
            if left_child_popped_idx >= len(self.heap_array): # list 끝에 다다랐기 때문에 루프 중단
                break

            elif right_child_popped_idx >= len(self.heap_array):
                if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                    self.heap_array, popped_idx = self.exchange(self.heap_array, popped_idx, left_child_popped_idx)
                else:
                    break
            else:
                if self.heap_array[left_child_popped_idx] > self.heap_array[right_child_popped_idx]:
                    if self.heap_array[popped_idx] < self.heap_array[left_child_popped_idx]:
                        self.heap_array, popped_idx = self.exchange(self.heap_array, popped_idx, left_child_popped_idx)
                    else:
                        break
                else:
                    if self.heap_array[popped_idx] < self.heap_array[right_child_popped_idx]:
                        self.heap_array, popped_idx = self.exchange(self.heap_array, popped_idx, right_child_popped_idx)
                    else:
                        break
        
        del self.heap_array[0]
        return returned_data

이렇게 Max Heap을 파이썬에서 구현한 코드는 위와 같다.

다음에 Min Heap도 구현해야지. 위의 과정에서 일부 수정을 거치면 될 것이다.

3편에서 계속..


© 2022.07. by Wookey_Kim

Powered by Hydejack v7.5.2