heap sort, priority queue 등에서 활용된다.
n개의 노드를 가진 힙의 높이 : (logN)을 반내림한다.(소숫값 버림)
n개의 노드를 가진 완전 이진 트리의 리프 노드 개수 : (n/2)를 반올림한다.
level은 0부터 시작한다.
node X의 level은 X의 parent의 level + 1
이다.
node X가 root 노드라면, X의 level은 0이다.
2*i + 1
이다.(if 2*i + 1 < n)2*i + 2
이다.(if 2*2 + 2 < n)(j-1)/2
를 내림한 값이다. (if j != 0)힙 정렬에서 유용한 연산
루트의 왼쪽 부트리와 오른쪽 부트리가 최대힙(또는 최소힙)일 경우, 루트를 포함한 전체 트리를 힙으로 만듦
rebuild heap python code
def rebuildHeap(A, r, n): # A : 리스트, r : 루트노드, n : 전체 노드 개수
# r은 left subtree와 right subtree가 최대 힙일 때, 루트가 r인 최대 힙을 만듦
# r은 리스트에서 root의 위치임
current = r
value = A[r] # 루트의 값을 value가 참조(value에 저장)
while (2*current + 1 < n):
leftChild = 2*current + 1
rightChild = leftChild + 1
# 두 자식 노드 중 큰 값의 노드를 largerChild로 이동
if rightChild < n and A[rightChild] > A[leftChild]:
largerChild = rightChild
else:
largerChild = leftChild
if value < A[largerChild]: # largerChild의 값이 크면
A[current] = A[largerChild]
current = largerChild # current를 largerChild로 내림
else:
break
A[current] = value
# while문 바깥에 있는 이유 : current로 비교한 후 일단 value보다 큰 값을 다 바꿔주고 마지막에 current자리
# 즉 마지막에 바뀐 largerChild자리에 원래의 value값을 넣어주면 된다. (매번 할 필요가 없음)
기본연산 : 원소비교(loop 1번 돌 때마다 자식들과 비교)
힙의 높이가 h
라고 했을 때, 최악의 경우에 비교를 2h만큼 하게 된다.
총 n개의 노드가 있을 때, 높이 h
인 힙은 logn의 내림
으로 표현 가능하다.
따라서 rebuildHeap은 O(h)
만큼의 시간 복잡도를 갖는다. 즉 O(logN)
n = len(heap)
for i in range(n//2 - 1, -1, -1):
rebuildHeap(A, i, n) # 각 i에 대해서 rebuild하면 된다!
n-1
이다.
- 힙의 루트에 있는 원소 A[0](즉, 최대 원소)와 마지막 원소 A[last]를 교환한다.
- 힙의 크기를 1 줄인다. 루트가 A[0]인 semiheap에 대하여
rebuildHeap
을 호출하여 힙으로 만든다.last = 0
이 될 때까지 위의 단계 1-2를 반복한다.
쉽게 말하면 첫 번째 원소와 마지막 원소를 바꾼 후 마지막 원소 제외 후 다시 정렬
heap_size = n
for last in range(n-1, 0, -1):
A[0], A[last] = A[last], A[0]
heap_size -= 1
rebuildHeap(A, 0, heap_size)
def heapsort(A):
n = len(A)
for i in range(n//2 - 1, -1, -1): # step 1
rebuildHeap(A, i, n)
heap_size = n
for last in range(n-1, 0, -1): # step 2
A[0], A[last] = A[last], A[0]
heap_size -= 1
rebuildHeap(A, 0, heap_size)
RebuildHeap은 O(h)
만큼의 시간 복잡도를 갖는다.
(루트노드부터 내려오고, 최악의 경우엔 리프노드까지 내려가므로)
힙 높이 h는 logn의 내림
값과 같다. 따라서 RebuildHeap은 O(logN)
의 시간 복잡도를 갖는다.
Heap Sort는 대략 n/2만큼 rebuildHeap을 수행하기 때문에, n/2*logn의 시간이 걸리게 되므로, O(nlogn)
의 시간 복잡도를 갖는다고 할 수 있다.
하지만 이는 tight bound하지 않다. 엄밀하게 분석하자면, O(nlogn)
보다는 더 작다. 오히려 O(n)
이라고 볼 수 있다. (허나 이는 최종 수행시간에 영향을 미치지는 않는다.), (자식 하나만 제대로 비교하고 남은 자식은 break로 탈출하기 때문)
O(logn)
이다.O(nlogn)
이다.단계 1 + 단계 2의 수행시간을 모두 더하면 총 수행시간은 O(nlogn)
이다.
우선순위 큐는 중요한 힙의 응용(application of heaps)이다.
키와 관련이 있는 집합의 요소들을 동적으로 유지하게끔 해준다.
수도코드
Algorigthm insert(A, key)
n <- n+1
i <- n-1 // 마지막 원소의 인덱스를 i로 놓기
while i > 0 and A[Parent(i)] < key
L[i] <- L[Parent(i)]
i <- Parent(i)
L[i] <- key
최악의 경우 루트 노드까지 새로 추가한 노드가 올라가므로 높이만큼의 시간 복잡도가 걸린다.
따라서 O(h)
즉 O(logN)
이다.
수도코드
Algorithm deleteMax(A)
if n < 1
then error "heap underflow"
maximum <- A[0]
A[0] <- A[n-1]
n <- n-1
rebuildHeap(A, 0, n) // 업데이트 된 n이다.
return maximum
주요 연산은 rebuildHeap이고, 최악의 경우 루트 노드가 리프 노드까지 내려가는 경우이므로 O(logN)
이다.
heapq.heappush(heap, item) // insert()와 동일
heapq.heappop(heap) // deleteMin()과 동일
heapq.heappushpop(heap, item) // item 삽입 후 deleteMin() 수행
heapq.heapify(h) // 리스트 h를 힙으로 만듦
++ 참고 : 여러 개의 원소를 넣을 때는 튜플을 이용하자(튜플인 경우 맨 앞 요소가 기준이다.)
정리 : n개의 자료(원소)를 키 비교에 의하여 정렬하는 알고리즘은 최악의 경우 적어도 log(n!)의 내림
만큼 키를 비교한다. (≈(nlogn - 1.443n)의 내림
과 비슷함)
키 비교에 의한 정렬 알고리즘의 최악의 경우 시간 복잡도는 Ω(nlogn)
이다.