|
1 | 1 | # Куча |
2 | 2 |
|
3 | | -https://www.youtube.com/watch?v=sAyOhkMZae4 |
| 3 | +**Куча** - это полное бинарное дерево, обладающее следующими свойствами: |
| 4 | + |
| 5 | +1. Для **кучи минимумов** каждый узел в куче имеет значение, не превышающее значений его дочерних узлов. Следовательно, |
| 6 | + верхний элемент (корневой узел) имеет **наименьшее** значение в куче. Для **кучи максимумов** каждый узел в куче |
| 7 | + имеет значение, не меньше значений его дочерних узлов. Следовательно, верхний элемент (корневой узел) имеет |
| 8 | + **наибольшее** значение в куче. |
| 9 | +1. Все уровни кучи, кроме, возможно, последнего, полностью заполнены, а все узлы последнего уровня максимально левые. |
| 10 | + Таким образом заполнение производится по уровням слева направо. |
| 11 | + |
| 12 | +Ниже представлены два вида кучи: кучи минимума и максимума. |
| 13 | + |
| 14 | + |
| 15 | + |
| 16 | +## Операции просеивания |
| 17 | + |
| 18 | +Операции просеивания используются после вставки и удаления элемента для того, чтобы выполнялось основное свойство кучи о |
| 19 | +ключах(п.1). Различают просеивание вверх и просеивание вниз. Рассмотрим каждый алгоритм более детально. Далее будем |
| 20 | +рассматривать кучу минимума. |
| 21 | + |
| 22 | +## Вставка элемента |
| 23 | + |
| 24 | +**Просеивание вверх** - это всплытие добавленного листа дерева до тех пор, пока не будет выполнено условие, что новый |
| 25 | +узел в куче имеет значение большее значения родителя и не превышающее значений его дочерних узлов. Таким образом мы |
| 26 | +меняем просеиваемый узел с родителем до тех пор, пока он не станет корнем или пока его значение меньше значение |
| 27 | +родителя. |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +Сложность данной операции **O(logN)**. |
| 32 | + |
| 33 | +## Удаление элемента |
| 34 | + |
| 35 | +**Просеивание вниз** - это погружение самого верхнего элемента, пока он не будет удовлетворять основному свойству кучи о |
| 36 | +ключах. Алгоритм следующий: |
| 37 | + |
| 38 | +1. Берем самый правый в последнем ряду элемент и переносим его на место удаляемого элемента, т.е. на вершину кучи. |
| 39 | + Определяем элемент как просеиваемый. |
| 40 | +1. Если у просеиваемого элемента два сына, определяем среди них того, у которого меньший ключ. Меняем этот узел с |
| 41 | + просеиваемым если нужно. |
| 42 | +1. Если у просеиваемого элемента один сын, меняем этот узел с просеиваемым если нужно. |
| 43 | +1. Если после перестановки у просеиваемого элемента нет сыновей, то завершаем алгоритм, свойство кучи восстановлено. |
| 44 | + |
| 45 | + |
| 46 | + |
| 47 | +Сложность данной операции **O(logN)**. |
| 48 | + |
| 49 | +## Определение минимального или максимального элемента |
| 50 | + |
| 51 | +Свойство ключей кучи гарантирует, что на ее вершине будет всегда находиться минимальный или максимальный элемент. |
| 52 | +Поэтому эта операция очень простая и состоит в чтении с вершины кучи. |
| 53 | + |
| 54 | +Сложность данной операции **O(1)**. |
| 55 | + |
| 56 | +## Реализация кучи в памяти |
| 57 | + |
| 58 | +Для реализации кучи очень удобно использовать массив. Для этого мы нумеруем узлы кучи подряд по слоям слева направо, и в |
| 59 | +таком порядке располагаем их в обычном массиве. |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +Тогда индексы хранения левого и правого ребенка для i-ого элемента будут ``2 * i + 1`` и ``2 * i + 2`` соответственно. |
| 64 | +Индекс родителя вычисляется по формуле ``(i - 1 ) // 2``. Ниже представлена реализация кучи минимума на python. |
| 65 | +Представление в виде массива значительно сокращает использование памяти по сравнению с классической реализацией бинарных |
| 66 | +деревьев в виде объектов и ссылок на них. |
| 67 | + |
| 68 | +```python |
| 69 | +class MinHeap: |
| 70 | + """ Реализация кучи минимумов на python с использованием массива """ |
| 71 | + |
| 72 | + def __init__(self): |
| 73 | + self.heap_list = [] |
| 74 | + |
| 75 | + def push(self, key: int): |
| 76 | + """ Добавление в кучу """ |
| 77 | + self.heap_list.append(key) |
| 78 | + cur_idx = len(self.heap_list) - 1 |
| 79 | + while cur_idx > 0 and self.heap_list[cur_idx] < self._get_parent(cur_idx): |
| 80 | + parent_idx = _get_parent_index(cur_idx) |
| 81 | + # Меняем текущий элемент с родителем |
| 82 | + self.heap_list[cur_idx], self.heap_list[parent_idx] = self.heap_list[parent_idx], self.heap_list[cur_idx] |
| 83 | + cur_idx = parent_idx |
| 84 | + |
| 85 | + def peek(self): |
| 86 | + """ Получение минимума """ |
| 87 | + return self.heap_list[0] |
| 88 | + |
| 89 | + def pop(self): |
| 90 | + """ Извлечение с вершины кучи """ |
| 91 | + result = self.peek() |
| 92 | + # Берем самый правый в последнем ряду элемент и переносим его на место удаляемого элемента |
| 93 | + self.heap_list[0] = self.heap_list.pop() |
| 94 | + cur_idx = 0 |
| 95 | + max_idx = len(self.heap_list) - 1 |
| 96 | + while True: |
| 97 | + left_idx = _get_left_child_index(cur_idx) |
| 98 | + right_idx = _get_right_child_index(cur_idx) |
| 99 | + |
| 100 | + if left_idx > max_idx and right_idx > max_idx: |
| 101 | + # Если после перестановки у просеиваемого элемента нет сыновей, то завершаем алгоритм |
| 102 | + break |
| 103 | + |
| 104 | + if left_idx <= max_idx and right_idx <= max_idx: |
| 105 | + # Если у просеиваемого элемента два сына |
| 106 | + min_idx = left_idx if self.heap_list[left_idx] < self.heap_list[right_idx] else right_idx |
| 107 | + elif left_idx <= max_idx: |
| 108 | + min_idx = left_idx |
| 109 | + else: |
| 110 | + min_idx = right_idx |
| 111 | + |
| 112 | + self.heap_list[cur_idx], self.heap_list[min_idx] = self.heap_list[min_idx], self.heap_list[cur_idx] |
| 113 | + cur_idx = min_idx |
| 114 | + |
| 115 | + return result |
| 116 | + |
| 117 | + def _get_parent(self, idx: int) -> int: |
| 118 | + """ Получение родительского ключа """ |
| 119 | + parent_idx = _get_parent_index(idx) |
| 120 | + return self.heap_list[parent_idx] |
| 121 | + |
| 122 | + def __str__(self): |
| 123 | + return str(self.heap_list) |
| 124 | + |
| 125 | + |
| 126 | +def _get_parent_index(i: int) -> int: |
| 127 | + """ Индекс родителя """ |
| 128 | + return (i - 1) // 2 |
| 129 | + |
| 130 | + |
| 131 | +def _get_left_child_index(i: int) -> int: |
| 132 | + """ Индекс левого ребенка """ |
| 133 | + return 2 * i + 1 |
| 134 | + |
| 135 | + |
| 136 | +def _get_right_child_index(i: int) -> int: |
| 137 | + """ Индекс правого ребенка """ |
| 138 | + return 2 * i + 2 |
| 139 | +``` |
0 commit comments