Opis

Dla mnie kopiec (heap) to coś jak BST, ale zamiast być lewo-prawo (lewo - małe, prawo - duże), jest góra-dół (korzeń - duży, liść - mały, lub odwrotnie). Przykładowo, przy kopcu minimalnym, jeśli w korzeniu jest wartość x, to na niższym poziomie będą tylko wartości większe (bądź równe) od x.

Fotka z wikipedii:

Rzeczy do zapamiętania - struktura:

d - wysokość drzewa

  • liście są albo na poziomie d, albo d-1
  • wszystkie liście na poziomie d-1 są najbardziej lewymi węzłami na tym poziomie
    (oznacza to: poziom x x x x _ _ _ _ jest poprawny, _ x x x x _ _ _ nie jest poprawny, ponieważ mamy przestrzeń z lewej strony)
  • najbardziej prawy węzeł na poziomie d-1 może mieć syna.

Układ:
Załóżmy, że używamy tablicy K do zapamiętania naszego kopca z n węzłami.
K[0] -> korzeń
K[floor(i/2)] -> rodzic
K[2i+1] -> lewe dziecko
K[2i+2] -> prawe dziecko

Kluczowe operacje

Zakładamy kopiec minimalny, tj. w korzeniu jest najmniejszy element kopca.

Dodawanie elementów (insert):

Kiedy dodajemy nowy element do kopca, umieszczamy go na końcu drzewa (w ostatnim wolnym miejscu w tablicy). Następnie przesuwamy ten element w górę drzewa, zamieniając go z rodzicem, aż znajdzie się na właściwej pozycji (tzn. nie jest mniejszy od swojego rodzica).

Usuwanie elementów (delete):

Usuwamy element z korzenia (najmniejszy element w kopcu minimalnym). Ostatni element w tablicy przesuwamy na miejsce usuniętego elementu. Następnie przesuwamy ten element w dół drzewa, zamieniając go z mniejszym z jego dzieci, aż znajdzie się na właściwej pozycji.

Poruszanie się po drzewie (move_up i move_down):

move_up: Przesuwa element w górę drzewa, zamieniając go z rodzicem, jeśli jest mniejszy. move_down: Przesuwa element w dół drzewa, zamieniając go z mniejszym z jego dzieci, jeśli jest większy.

Heapify:

heapify_slow: Buduje kopiec dodając i przesuwając każdy element w górę (wolniejsze, ale prostsze podejście).
heapify_fast: Buduje kopiec od dołu (szybsze podejście).

Heap sort

Szybki algorytm sortujący przy użyciu kopca, O(n log n). Na chłopski rozum - robimy kopiec z tablicy (O(n)), a później usuwamy z niego korzeń i dodajemy go do tablicy posortowanych. Usunięcie zakłada przywrócenie własności kopca (log n). Tak robimy aż skończy nam sie kopiec.

Implementacja

Moja implementacja kopca (min) i heap sorta:

# Key arrangement: node(parent) <= node(child)
from typing import List


class Heap:
    def __init__(self, n: int):
        self.heap = [None]*n
        self.size = 0

    def get_parent(self, i: int):
        return ((i - 1) // 2, self.heap[(i - 1) // 2])

    def get_left_child(self, i: int):
        return (2 * i + 1, self.heap[2 * i + 1])

    def get_right_child(self, i: int):
        return (2 * i + 2, self.heap[2 * i + 2])

    def insert(self, x: int):
        self.heap[self.size] = x
        self.move_up(self.size)
        self.size += 1

    def delete(self, i: int):
        self.size -= 1
        self.heap[i] = self.heap[self.size]
        self.heap[self.size] = None
        self.move_down(i)

    def move_up(self, i: int):
        if i > 0:
            i_parent, parent_val = self.get_parent(i)
            if parent_val > self.heap[i]:
                self.heap[i_parent], self.heap[i] = (
                    self.heap[i], self.heap[i_parent]
                    )
                self.move_up(i_parent)

    def move_down(self, i: int):
        while 2 * i + 1 < self.size:
            i_left, left_val = self.get_left_child(i)
            i_smallest = i_left
            if 2 * i + 2 < self.size:
                _, right_val = self.get_right_child(i)
                if right_val < left_val:
                    i_smallest = 2 * i + 2

            if self.heap[i_smallest] >= self.heap[i]:
                break

            self.heap[i], self.heap[i_smallest] = (
                self.heap[i_smallest], self.heap[i]
                )
            i = i_smallest

    # O(n log n)
    def heapify_slow(self, tab: List[int]):
        self.heap = tab
        self.size = len(tab)
        for i in range(1, len(tab)):
            self.move_up(i)

    # O(n)
    def heapify_fast(self, tab: List[int]):
        self.heap = tab
        self.size = len(tab)
        for i in range(len(tab) // 2 - 1, -1, -1):
            self.move_down(i)

    def extract_min(self):
        if self.size == 0:
            raise IndexError("Heap is empty")
        min_val = self.heap[0]
        self.delete(0)
        return min_val

    def heap_sort(self, tab: List[int]) -> List[int]:
        self.heapify_fast(tab)
        sorted_list = []
        original_size = self.size
        for _ in range(original_size):
            sorted_list.append(self.extract_min())
        return sorted_list