본문 바로가기

TIL

[TIL]20220529

(Heap)

힙은 데이터에서 최댁값과 최소값을 빠르게 찾기 위해 고안된 완전 이진 트리. 항상 큰 값이 상위 레벨에 있고 작은 값이 하위레벨에 있도록 한다(최대힙). 최소힙의 경우 최솟값이 맨 위 레벨에 있다.

 

 최대힙의 구현

class BinaryMaxHeap:
    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):
        biggest = cur
        left = 2 * cur
        right = 2 * cur + 1

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

        if right <= len(self) and self.items[right] > self.items[biggest]:
            biggest = right

        if biggest != cur:
            self.items[cur], self.items[biggest] = self.items[biggest], self.items[cur]
            self._percolate_down(biggest)

    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

 정렬 (Heap Sort)

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
        
def sorted_by_heap(lst):
    maxheap = BinaryMaxHeap()
    for elem in lst:
        maxheap.insert(elem)

    desc = [maxheap.extract() for _ in range(len(lst))]
    return list(reversed(desc))

 

heapq 모듈 사용법

heapq 모듈은 이진 트리(binary tree) 기반의 최소 힙(min heap) 자료구조를 제공한다.

heap[k] <= heap[2*k+1] and heap[k] <= heap[2*k+2]

min heap을 사용하면 원소들이 항상 정렬된 상태로 추가되고 삭제되며, min heap에서 가장 작은값은 언제나 인덱스 0, 즉, 이진 트리의 루트에 위치한다. 내부적으로 min heap 내의 모든 원소(k)는 항상 자식 원소들(2k+1, 2k+2) 보다 크기가 작거나 같도록 원소가 추가되고 삭제된다.

     1  ---> root
   /   \
  3     5
 / \   /
4   8 7
#위 공식을 만족시키는 간단한 min heap의 구조

heapq 모듈에은 파이썬의 보통 리스트를 마치 최소 힙처럼 다룰 수 있도록 도와준다. 그래서  그냥 빈 리스트를 생성해놓은 다음 heapq 모듈의 함수를 호출할 때 마다 이 리스트를 인자로 넘겨야 한다. 즉, 파이썬에서는 heapq 모듈을 통해서 원소를 추가하거나 삭제한 리스트가 그냥 최소 힙이다.

#최소 힘 생성 
heap = []
#-------------------------------------------------------------------------------
# 힙에 원소 추가 (O(logN))

heapq.heappush(heap, 4)
heapq.heappush(heap, 1)
heapq.heappush(heap, 7)
heapq.heappush(heap, 3)
>>>> heap
[1,3,7,4]

#-------------------------------------------------------------------------------
# 힙에서 원소 삭제 (O(logN))

heapq.heappop(heap)
>>>> 1

heap
>>>> [3,4,7]

#-------------------------------------------------------------------------------
# 최소값 삭제 하지 않고 얻기 
# 인덱스 0에 가장 작은 원소가 있다고 해서,인덱스 1에 두번째 작은 원소,
# 인덱스 2에 세번째 작은 원소가 있다는 보장은 없다.
# 왜냐하면 힙은 heappop() 함수로 원소를 삭제할 때마다 이진 트리의 재배치한다.

heap[0]
>>>> 4

#-------------------------------------------------------------------------------
# 기존 리스트를 힙으로 변환하기

heap = [4, 1, 7, 3, 8, 5]
heapq.heapify(heap)

heap
>>>> [1, 3, 5, 4, 8, 7]

#-------------------------------------------------------------------------------
# 최대힙 (응용)

import heapq

nums = [4, 1, 7, 3, 8, 5]
heap = []

for num in nums:
  heapq.heappush(heap, (-num, num))  # (우선 순위, 값)

while heap:
  print(heapq.heappop(heap)[1])  # index 1
  
>>>>
8
7
5
4
3
1

#-------------------------------------------------------------------------------
# K번째 최소값/최대값 (응용)

def kth_smallest(nums, k):
  heap = []
  for num in nums:
    heapq.heappush(heap, num)

  kth_min = None
  for _ in range(k):
    kth_min = heapq.heappop(heap)
  return kth_min

print(kth_smallest([4, 1, 7, 3, 8, 5], 3))
>>>> 4

#-------------------------------------------------------------------------------
# 힙 정렬 (응용)

import heapq

def heap_sort(nums):
  heap = []
  for num in nums:
    heapq.heappush(heap, num)

  sorted_nums = []
  while heap:
    sorted_nums.append(heapq.heappop(heap))
  return sorted_nums

print(heap_sort([4, 1, 7, 3, 8, 5]))
>>>> [1, 3, 4, 5, 7, 8]

 

이진 탐색

이진탐색이란?

  • 배열이 정렬되어있을 경우, 절반씩 줄여나가면서 탐색하는 기법

 

이진탐색의 효율성

  • 1억 개 목록을 선형탐색할 때, 1억 번을 연산해야 한다. 이진탐색으로 찾는다면, 27번 안에 찾을 수 있다.
  • import math math.log2(100000000) # 26.575424759098897
  • 이진탐색을 위해서는 정렬되어 있어야 한다

 

이진 탐색 구현

def binary_search(nums, target):
    def bs(start, end):
        if start > end:
            return -1

        mid = (start + end) // 2

        if nums[mid] < target:
            return bs(mid + 1, end)
        elif nums[mid] > target:
            return bs(start, mid - 1)
        else:
            return mid

    return bs(0, len(nums) - 1)

 

def binary_search_builtin(nums, target):
    idx = bisect.bisect_left(nums, target)
		# idx == len(nums) 가능하기 떄문.
		"""Return the index where to insert item x in list a, assuming a is sorted.

    The return value i is such that all e in a[:i] have e < x, and all e in
    a[i:] have e >= x.  So if x already appears in the list, a.insert(x) will
    insert just before the leftmost x already there.

    Optional args lo (default 0) and hi (default len(a)) bound the
    slice of a to be searched.
    """
    if idx < len(nums) and nums[idx] == target:
        return idx
    else:
        return -1

'TIL' 카테고리의 다른 글

[TIL]20220531  (0) 2022.05.31
[TIL]20220530  (0) 2022.05.30
[TIL]20220528  (0) 2022.05.29
[TIL]20220527  (0) 2022.05.27
[TIL]20220526  (0) 2022.05.26