Skip to content

Commit 4d66557

Browse files
authored
Куча (#13)
Куча
1 parent 0602e20 commit 4d66557

File tree

7 files changed

+143
-5
lines changed

7 files changed

+143
-5
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
[Графы](tutorial/graph.md)
3535

36-
[Куча](tutorial/heap.md) (в работе)
36+
[Куча](tutorial/heap.md)
3737

3838
[Очередь с приоритетом](tutorial/heap.md) (в работе)
3939

img/heap_1.png

21.2 KB
Loading

img/heap_2.png

76.3 KB
Loading

img/heap_3.png

76.2 KB
Loading

img/heap_4.png

51.7 KB
Loading

tutorial/heap.md

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,139 @@
11
# Куча
22

3-
https://www.youtube.com/watch?v=sAyOhkMZae4
3+
**Куча** - это полное бинарное дерево, обладающее следующими свойствами:
4+
5+
1. Для **кучи минимумов** каждый узел в куче имеет значение, не превышающее значений его дочерних узлов. Следовательно,
6+
верхний элемент (корневой узел) имеет **наименьшее** значение в куче. Для **кучи максимумов** каждый узел в куче
7+
имеет значение, не меньше значений его дочерних узлов. Следовательно, верхний элемент (корневой узел) имеет
8+
**наибольшее** значение в куче.
9+
1. Все уровни кучи, кроме, возможно, последнего, полностью заполнены, а все узлы последнего уровня максимально левые.
10+
Таким образом заполнение производится по уровням слева направо.
11+
12+
Ниже представлены два вида кучи: кучи минимума и максимума.
13+
14+
![Кучи минимума и максимума](../img/heap_1.png)
15+
16+
## Операции просеивания
17+
18+
Операции просеивания используются после вставки и удаления элемента для того, чтобы выполнялось основное свойство кучи о
19+
ключах(п.1). Различают просеивание вверх и просеивание вниз. Рассмотрим каждый алгоритм более детально. Далее будем
20+
рассматривать кучу минимума.
21+
22+
## Вставка элемента
23+
24+
**Просеивание вверх** - это всплытие добавленного листа дерева до тех пор, пока не будет выполнено условие, что новый
25+
узел в куче имеет значение большее значения родителя и не превышающее значений его дочерних узлов. Таким образом мы
26+
меняем просеиваемый узел с родителем до тех пор, пока он не станет корнем или пока его значение меньше значение
27+
родителя.
28+
29+
![Просеивание вверх](../img/heap_2.png)
30+
31+
Сложность данной операции **O(logN)**.
32+
33+
## Удаление элемента
34+
35+
**Просеивание вниз** - это погружение самого верхнего элемента, пока он не будет удовлетворять основному свойству кучи о
36+
ключах. Алгоритм следующий:
37+
38+
1. Берем самый правый в последнем ряду элемент и переносим его на место удаляемого элемента, т.е. на вершину кучи.
39+
Определяем элемент как просеиваемый.
40+
1. Если у просеиваемого элемента два сына, определяем среди них того, у которого меньший ключ. Меняем этот узел с
41+
просеиваемым если нужно.
42+
1. Если у просеиваемого элемента один сын, меняем этот узел с просеиваемым если нужно.
43+
1. Если после перестановки у просеиваемого элемента нет сыновей, то завершаем алгоритм, свойство кучи восстановлено.
44+
45+
![Просеивание вниз](../img/heap_3.png)
46+
47+
Сложность данной операции **O(logN)**.
48+
49+
## Определение минимального или максимального элемента
50+
51+
Свойство ключей кучи гарантирует, что на ее вершине будет всегда находиться минимальный или максимальный элемент.
52+
Поэтому эта операция очень простая и состоит в чтении с вершины кучи.
53+
54+
Сложность данной операции **O(1)**.
55+
56+
## Реализация кучи в памяти
57+
58+
Для реализации кучи очень удобно использовать массив. Для этого мы нумеруем узлы кучи подряд по слоям слева направо, и в
59+
таком порядке располагаем их в обычном массиве.
60+
61+
![Реализация кучи в памяти](../img/heap_4.png)
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+
```

tutorial/tree.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
**Дерево** — это направленный граф без циклов между узлами. Каждый узел может иметь потомков и родителя. Узел без
44
родителя называется корнем дерева и является начальным. Вершины, у которых нет потомков, называются листьями.
55

6-
Бинарное или двоичное дерево — это разновидность дерева, в котором у каждого узла может быть не более двух потомков. При
7-
этом в левом поддереве значения меньше, чем само значение родителя, а в правом поддереве значения должны быть больше. В
8-
случае если дерево сбалансировано, его высота ``К`` будет равна **O(logN)**.
6+
Бинарное или двоичное дерево — это дерево, в котором у каждого узла может быть не более двух потомков.
7+
8+
Бинарное дерево **поиска** — это бинарное дерево, в котором в левом поддереве значения меньше, чем само значение
9+
родителя, а в правом поддереве значения должны быть больше. В случае если дерево сбалансировано, его высота ``К`` будет
10+
равна **O(logN)**.
911

1012
Важно заметить, что если при построении дерева ключи ее узлов идут в случайном порядке, то такие деревья называют
1113
**сбалансированными**. Такие рандомизированные деревья поиска обеспечивают сбалансированность только в вероятностном

0 commit comments

Comments
 (0)