From 748478a6068c762f0e18122efa1875e24fb68a55 Mon Sep 17 00:00:00 2001 From: "ivan.stasevich" Date: Sun, 26 Apr 2026 19:16:38 +0300 Subject: [PATCH] feat: add heaps --- .gitignore | 2 +- README.md | 5 +- src/n_heaps/README.md | 188 +++++++++++++ src/{n_graphs => n_heaps}/__init__.py | 0 src/n_heaps/heap_sort.py | 45 ++++ src/n_heaps/heapify.py | 21 ++ src/n_heaps/huffman.py | 66 +++++ src/n_heaps/parallel.py | 14 + src/n_heaps/priority_queue.py | 59 +++++ src/{n_graphs => o_graphs}/ABSTRACT.md | 0 src/{n_graphs => o_graphs}/README.md | 0 src/o_graphs/__init__.py | 0 src/{n_graphs => o_graphs}/greed_stone.py | 0 src/{n_graphs => o_graphs}/journey_path.py | 0 tasks.xlsx | Bin 0 -> 12823 bytes tests/test_graphs/test_greed_stone.py | 2 +- tests/test_graphs/test_journey_path.py | 2 +- tests/test_heaps/__init__.py | 0 tests/test_heaps/test_heap_sort.py | 35 +++ tests/test_heaps/test_heapify.py | 129 +++++++++ tests/test_heaps/test_huffman.py | 53 ++++ tests/test_heaps/test_parallel.py | 216 +++++++++++++++ tests/test_heaps/test_priority_queue.py | 294 +++++++++++++++++++++ 23 files changed, 1126 insertions(+), 5 deletions(-) create mode 100644 src/n_heaps/README.md rename src/{n_graphs => n_heaps}/__init__.py (100%) create mode 100644 src/n_heaps/heap_sort.py create mode 100644 src/n_heaps/heapify.py create mode 100644 src/n_heaps/huffman.py create mode 100644 src/n_heaps/parallel.py create mode 100644 src/n_heaps/priority_queue.py rename src/{n_graphs => o_graphs}/ABSTRACT.md (100%) rename src/{n_graphs => o_graphs}/README.md (100%) create mode 100644 src/o_graphs/__init__.py rename src/{n_graphs => o_graphs}/greed_stone.py (100%) rename src/{n_graphs => o_graphs}/journey_path.py (100%) create mode 100644 tasks.xlsx create mode 100644 tests/test_heaps/__init__.py create mode 100644 tests/test_heaps/test_heap_sort.py create mode 100644 tests/test_heaps/test_heapify.py create mode 100644 tests/test_heaps/test_huffman.py create mode 100644 tests/test_heaps/test_parallel.py create mode 100644 tests/test_heaps/test_priority_queue.py diff --git a/.gitignore b/.gitignore index 2d0653a..226bbf0 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ - +assets/extra/ diff --git a/README.md b/README.md index 155f368..93d0326 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Ключевые особенности -- **14** разобранных тем, к каждой теме легкий конспект +- **15+** разобранных тем, к каждой теме легкий конспект - **70+** практических задач с LeetCode, тренировок от Яндекса и реальных собеседований - Решение **к каждой задаче** на лаконичном Python с комментариями - **800+** автоматизированных тестов для проверки решений @@ -26,7 +26,8 @@ | 11 | Теория чисел | Продвинутые подходы | `k_number_theory` | | 12 | 2D Динамическое программирование | Продвинутые подходы | `l_dp2` | | 13 | Деревья | Продвинутые структуры данных | `m_trees` | -| 14 | Графы | Продвинутые структуры данных | `n_trees` | +| 14 | Кучи | Продвинутые структуры данных | `n_heaps` | +| 15 | Графы | Продвинутые структуры данных | `o_trees` | Каждая тема содержит: diff --git a/src/n_heaps/README.md b/src/n_heaps/README.md new file mode 100644 index 0000000..284dcc0 --- /dev/null +++ b/src/n_heaps/README.md @@ -0,0 +1,188 @@ +# Задачи на кучи + +## A. Построение кучи + +| Поле | Значение | +|-----------|---------------------------------------------------| +| Сложность | Легкая | +| Источник | https://stepik.org/lesson/41560/step/1?unit=20013 | + +Переставить элементы заданного массива чисел так, чтобы он удовлетворял свойствам мин-кучи. + +Формат входа. В первой строке задано число N (1 ≤ N ≤ 10^5) — количество элементов в массиве. Во второй +строке задано N целых чисел, разделенных пробелами — элементы массива. Все числа по модулю не превосходят 10^9. + +Формат выхода. В первой строке выведите число S — количество перестановок, которые необходимо сделать, чтобы +превратить массив в мин-кучу. В следующих S строках выведите по одной перестановке в формате "i j", где i и j — индексы +элементов, которые нужно поменять местами. Индексация элементов массива начинается с нуля. + +Пример. +Ввод: + +```text +5 +5 4 3 2 1 +``` + +Вывод: + +```text +3 +1 4 +0 1 +1 3 +``` + +## B. Очередь с приоритетом + +| Поле | Значение | +|-----------|--------------------------------------------------| +| Сложность | Легкая | +| Источник | https://stepik.org/lesson/13240/step/8?unit=3426 | + +Реализуйте структуру данных "очередь с приоритетом" на основе кучи. Ваша очередь должна поддерживать следующие +операции: + +- insert(x) - вставить элемент x в очередь с приоритетом +- extract_max() - извлечь элемент с максимальным приоритетом из очереди с приоритетом и вывести его. Гарантируется, что + очередь с приоритетом не будет пустой в момент вызова операции extract_max(). + +Формат входа. В первой строке число N (1 ≤ N ≤ 10^5) — количество операций. В каждой из следующих N строк записано +описание операции. Операция insert(x) задается строкой "Insert x", где x — целое число, по модулю не превосходящее 10^9. +Операция extract_max() задается строкой "ExtractMax". + +Формат выхода. Для каждой операции extract_max() выведите в отдельной строке число, извлеченное из очереди с +приоритетом. + +Пример. + +Ввод: + +```text +6 +Insert 200 +Insert 10 +ExtractMax +Insert 5 +Insert 500 +ExtractMax +``` + +Вывод: + +```text +200 +500 +``` + +## С. Сортировка массива + +| Поле | Значение | +|-----------|----------------------------------------------------------| +| Сложность | Средняя | +| Источник | https://leetcode.com/problems/sort-an-array/description/ | + +## D. Кодирование строки + +| Поле | Значение | +|-----------|----------------------------------------| +| Сложность | Средняя | +| Источник | https://stepik.org/lesson/13239/step/5 | + +Дана строка из строчных букв латинского алфавита. Закодируйте строку с помощью алгоритма Хаффмана. + +Формат входа. Строка, которую нужно закодировать. Длина строки не превосходит 10^4. + +Формат выхода. В первой строке выведите количество различных букв K, встречающихся в строке, и размер получившейся +закодированной +строки. В следующих K строках запишите коды букв в формате "letter: code". В последней строке выведите закодированную +строку. + +Пример. + +Ввод: + +```text +abacabad +``` + +Вывод: + +```text +4 14 +a: 0 +b: 10 +c: 110 +d: 111 +01001100100111 +``` + +## E. Параллельная обработка + +| Поле | Значение | +|-----------|---------------------------------------------------| +| Сложность | Средняя | +| Источник | https://stepik.org/lesson/41560/step/2?unit=20013 | + +По данным n процессорам и m задачам определите, для каждой из задач, +каким процессором она будет обработана. + +Формат входа. В первой строке заданы два целых числа n и m (1 ≤ n ≤ 10^5, 1 ≤ m ≤ 10^5) — количество процессоров и +задач. Во второй строке задано m целых чисел t_i (0 ≤ t_i ≤ 10^9) — время, необходимое для обработки i-й задачи. + +Формат выхода. Выведите m строк. В i-й строке должно быть указано, каким процессором будет обработана i-я задача и в +какое время она начнет обрабатываться. Процессоры нумеруются от 0 до n-1. Если несколько процессоров свободны в момент +начала обработки задачи, выберите процессор с наименьшим номером. + +Пример 1. + +Ввод: + +```text +2 5 +1 2 3 4 5 +``` + +Вывод: + +```text +0 0 +1 0 +0 1 +1 2 +0 4 +``` + +Пример 2. + +Ввод: + +```text +4 20 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +``` + +Вывод: + +```text +0 0 +1 0 +2 0 +3 0 +0 1 +1 1 +2 1 +3 1 +0 2 +1 2 +2 2 +3 2 +0 3 +1 3 +2 3 +3 3 +0 4 +1 4 +2 4 +3 4 +``` diff --git a/src/n_graphs/__init__.py b/src/n_heaps/__init__.py similarity index 100% rename from src/n_graphs/__init__.py rename to src/n_heaps/__init__.py diff --git a/src/n_heaps/heap_sort.py b/src/n_heaps/heap_sort.py new file mode 100644 index 0000000..139b91b --- /dev/null +++ b/src/n_heaps/heap_sort.py @@ -0,0 +1,45 @@ +# сортировка кучей - O(NlogN) +import heapq + + +def heap_sort(a: list[int]) -> list[int]: + n = len(a) + h: list[int] = [] + for i in range(n): + heapq.heappush(h, a[i]) + return [heapq.heappop(h) for _ in range(n)] + + +# просеивание вниз - O(logN) +def sift_down(a: list[int], size: int, i: int) -> None: + while 2 * i + 1 < size: + # потомки вершины i + left = 2 * i + 1 + right = 2 * i + 2 + # берём максимум из потомков + j = left + if right < size and a[right] > a[left]: + j = right + # если текущий элемент больше или равен потомка, + # значит он располагается правильно - выходим + if a[i] >= a[j]: + break + a[i], a[j] = a[j], a[i] + i = j + + +# построение кучи - O(N) +def build_heap(a: list[int], size: int) -> None: + for i in range(size // 2, -1, -1): + sift_down(a, size, i) + + +# сортировка кучей на месте - O(NlogN) +def heap_sort_inplace(a: list[int]) -> None: + n = len(a) + build_heap(a, n) + size = n + for _i in range(n, 1, -1): + a[size - 1], a[0] = a[0], a[size - 1] + size -= 1 + sift_down(a, size, 0) diff --git a/src/n_heaps/heapify.py b/src/n_heaps/heapify.py new file mode 100644 index 0000000..5ad57e4 --- /dev/null +++ b/src/n_heaps/heapify.py @@ -0,0 +1,21 @@ +def sift_down(a: list[int], size: int, i: int) -> list[tuple[int, int]]: + swaps: list[tuple[int, int]] = [] + while 2 * i + 1 < size: + left = 2 * i + 1 + right = 2 * i + 2 + j = left + if right < size and a[right] < a[left]: + j = right + if a[i] <= a[j]: + break + a[i], a[j] = a[j], a[i] + swaps.append((i, j)) + i = j + return swaps + + +def heapify(size: int, a: list[int]) -> list[tuple[int, int]]: + swaps: list[tuple[int, int]] = [] + for i in range(size // 2, -1, -1): + swaps += sift_down(a, size, i) + return swaps diff --git a/src/n_heaps/huffman.py b/src/n_heaps/huffman.py new file mode 100644 index 0000000..dacf265 --- /dev/null +++ b/src/n_heaps/huffman.py @@ -0,0 +1,66 @@ +import heapq + + +# O(n log n) +def huffman_encode(s: str) -> tuple[str, dict[str, str]]: + result = "" + tree = huffman_tree(s) + for c in s: + result += tree[c] + return result, tree + + +# O(n) +def huffman_decode(s: str, tree: dict[str, str]) -> str: + result = "" + code = "" + for c in s: + code += c + if code in tree: + result += tree[code] + code = "" + return result + + +# O(n log n) +def huffman_tree(s: str) -> dict[str, str]: + # Считаем частоту встречаемости каждого символа в строке. + freq = count_chars(s) + # Создаем кучу: каждый элемент очереди представляет собой кортеж (частота, символ). + queue = [(v, k) for k, v in freq.items()] + heapq.heapify(queue) + # Создаем словарь для хранения кодов Хаффмана. + tree = {c: "" for c in freq} + # Если в строке есть только один символ, то кодируем его как "0". + if len(freq) == 1: + tree[s[0]] = "0" + # Пока в очереди есть хотя бы два узла. + while len(queue) >= 2: + # Извлекаем два узла с минимальной частотой - их нужно закодировать в первую очередь. + left = extract_min(queue) + right = extract_min(queue) + # Добавляем новый узел в очередь: + # его название равно конкатенации названий двух извлеченных узлов (a + b = ab), + # а его частота равна сумме частот двух извлеченных узлов (1 + 2 = 3). + queue.append((left[0] + right[0], left[1] + right[1])) + # Обновляем предков: добавляем "0" к коду левого узла и "1" к коду правого узла. + for ancestor in left[1]: + tree[ancestor] = "0" + tree[ancestor] + for ancestor in right[1]: + tree[ancestor] = "1" + tree[ancestor] + return tree + + +# O(n) +def count_chars(s: str) -> dict[str, int]: + freq = {} + for c in s: + if c not in freq: + freq[c] = 0 + freq[c] += 1 + return freq + + +# O(log n) +def extract_min(queue: list[tuple[int, str]]) -> tuple[int, str]: + return heapq.heappop(queue) diff --git a/src/n_heaps/parallel.py b/src/n_heaps/parallel.py new file mode 100644 index 0000000..73e3c78 --- /dev/null +++ b/src/n_heaps/parallel.py @@ -0,0 +1,14 @@ +import heapq + + +def parallel(n: int, m: int, time: list[int]) -> list[tuple[int, int]]: + result = [] + h: list[tuple[int, int]] = [] + for i in range(n): + heapq.heappush(h, (0, i)) + for i in range(m): + t = time[i] + x = heapq.heappop(h) + result.append((x[1], x[0])) + heapq.heappush(h, (x[0] + t, x[1])) + return result diff --git a/src/n_heaps/priority_queue.py b/src/n_heaps/priority_queue.py new file mode 100644 index 0000000..51eb75c --- /dev/null +++ b/src/n_heaps/priority_queue.py @@ -0,0 +1,59 @@ +""" +Макс-куча +""" + + +class PriorityQueue: + def __init__(self, arr: list[int] | None = None) -> None: + self.arr: list[int] = [] if arr is None else arr + self.size = len(self.arr) + self.heapify() + + def swap(self, i: int, j: int) -> None: + tmp = self.arr[i] + self.arr[i] = self.arr[j] + self.arr[j] = tmp + + # просеивание вверх + def sift_up(self, i: int) -> None: + # (i - 1) // 2 - индекс родителя i-й вершины + while i > 0 and self.arr[i] > self.arr[(i - 1) // 2]: + self.swap(i, (i - 1) // 2) + i = (i - 1) // 2 + + # просеивание вниз + def sift_down(self, i: int) -> None: + while 2 * i + 1 < self.size: + # потомки вершины i + left = 2 * i + 1 + right = 2 * i + 2 + # берём максимум из потомков + j = left + if right < self.size and self.arr[right] > self.arr[left]: + j = right + # если текущий элемент больше или равен потомка, + # значит он располагается правильно - выходим + if self.arr[i] >= self.arr[j]: + break + self.swap(i, j) + i = j + + # превращение массива в кучу + def heapify(self) -> None: + for i in range(self.size // 2 - 1, -1, -1): + self.sift_down(i) + + # вставка + def insert(self, x: int) -> None: + self.arr.append(x) + self.sift_up(self.size) + self.size += 1 + + # извлечение максимума + def extract_max(self) -> int: + mx = self.arr[0] + self.swap(0, self.size - 1) + self.arr.pop() + self.size -= 1 + self.sift_down(0) + return mx diff --git a/src/n_graphs/ABSTRACT.md b/src/o_graphs/ABSTRACT.md similarity index 100% rename from src/n_graphs/ABSTRACT.md rename to src/o_graphs/ABSTRACT.md diff --git a/src/n_graphs/README.md b/src/o_graphs/README.md similarity index 100% rename from src/n_graphs/README.md rename to src/o_graphs/README.md diff --git a/src/o_graphs/__init__.py b/src/o_graphs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/n_graphs/greed_stone.py b/src/o_graphs/greed_stone.py similarity index 100% rename from src/n_graphs/greed_stone.py rename to src/o_graphs/greed_stone.py diff --git a/src/n_graphs/journey_path.py b/src/o_graphs/journey_path.py similarity index 100% rename from src/n_graphs/journey_path.py rename to src/o_graphs/journey_path.py diff --git a/tasks.xlsx b/tasks.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1a8df4c1f3a2c39023b3bf37b4c2a6776390773e GIT binary patch literal 12823 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 F{{gWgAsqk! literal 0 HcmV?d00001 diff --git a/tests/test_graphs/test_greed_stone.py b/tests/test_graphs/test_greed_stone.py index 2019b54..e69a43a 100644 --- a/tests/test_graphs/test_greed_stone.py +++ b/tests/test_graphs/test_greed_stone.py @@ -1,6 +1,6 @@ import pytest -from src.n_graphs.greed_stone import greed_stone +from src.o_graphs.greed_stone import greed_stone @pytest.mark.parametrize( diff --git a/tests/test_graphs/test_journey_path.py b/tests/test_graphs/test_journey_path.py index 84401e4..b325e15 100644 --- a/tests/test_graphs/test_journey_path.py +++ b/tests/test_graphs/test_journey_path.py @@ -3,7 +3,7 @@ import pytest -from src.n_graphs.journey_path import Flight, get_route, get_route_enhanced +from src.o_graphs.journey_path import Flight, get_route, get_route_enhanced @pytest.mark.parametrize("func", [get_route, get_route_enhanced]) diff --git a/tests/test_heaps/__init__.py b/tests/test_heaps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_heaps/test_heap_sort.py b/tests/test_heaps/test_heap_sort.py new file mode 100644 index 0000000..b0fee6f --- /dev/null +++ b/tests/test_heaps/test_heap_sort.py @@ -0,0 +1,35 @@ +from collections.abc import Callable + +import pytest + +from src.n_heaps.heap_sort import heap_sort, heap_sort_inplace + + +@pytest.mark.parametrize( + "func", + [ + heap_sort, + heap_sort_inplace, + ], +) +@pytest.mark.parametrize( + "a", + [ + [1, 2, 3, 4, 5], + [5, 4, 3, 2, 1], + [1, 3, 2, 5, 4], + [1, 1, 1, 1, 1], + [1, 1, 2, 2, 3, 3, 4, 4, 5, 5], + [5, 5, 4, 4, 3, 3, 2, 2, 1, 1], + [1], + ], +) +def test_heap_sort(func: Callable[[list[int]], list[int] | None], a: list[int]) -> None: + a = a.copy() + result = func(a) + expected = sorted(a) + + if result is None: + assert a == expected + else: + assert result == expected diff --git a/tests/test_heaps/test_heapify.py b/tests/test_heaps/test_heapify.py new file mode 100644 index 0000000..855ab10 --- /dev/null +++ b/tests/test_heaps/test_heapify.py @@ -0,0 +1,129 @@ +import pytest + +from src.n_heaps.heapify import heapify + + +def is_min_heap(a: list[int]) -> bool: + return all(a[i] <= a[2 * i + 1] and (2 * i + 2 >= len(a) or a[i] <= a[2 * i + 2]) for i in range(len(a) // 2)) + + +def replay_swaps(original: list[int], swaps: list[tuple[int, int]]) -> list[int]: + a = original[:] + for i, j in swaps: + a[i], a[j] = a[j], a[i] + return a + + +@pytest.mark.parametrize( + ("a", "expected_swaps", "expected_heap"), + [ + ([], [], []), # пустой массив → никаких свапов, результат тот же + ([1], [], [1]), # один элемент → куча уже валидна + ([1, 2], [], [1, 2]), # уже min-heap → изменений нет + ([2, 1], [(0, 1)], [1, 2]), # два элемента в обратном порядке → один свап + ([1, 2, 3], [], [1, 2, 3]), # уже корректная куча + ([3, 2, 1], [(0, 2)], [1, 2, 3]), # выбирается правый ребёнок (он меньше) + ([3, 1, 2], [(0, 1)], [1, 3, 2]), # выбирается левый ребёнок + ([5, 4, 3, 2, 1], [(1, 4), (0, 1), (1, 3)], [1, 2, 3, 5, 4]), # несколько sift_down + ([5, 5, 5], [], [5, 5, 5]), # все элементы равны → свапов нет + ([2, 2, 1], [(0, 2)], [1, 2, 2]), # равные + меньший правый ребёнок + ([0, -1, -2, -3], [(1, 3), (0, 1), (1, 3)], [-3, -1, -2, 0]), # отрицательные числа + ], +) +def test_heapify_exact_cases(a: list[int], expected_swaps: list[tuple[int, int]], expected_heap: list[int]) -> None: + # проверяем точные сценарии: последовательность свапов и финальное состояние + original = a[:] + + swaps = heapify(len(a), a) + + assert swaps == expected_swaps + assert a == expected_heap + assert is_min_heap(a) + assert replay_swaps(original, swaps) == a + + +def test_heapify_ignores_elements_after_size() -> None: + # heapify должен работать только на первых size элементах + a = [5, 4, 3, 2, 1, -100, -200] + + swaps = heapify(5, a) + + assert swaps == [(1, 4), (0, 1), (1, 3)] + assert a == [1, 2, 3, 5, 4, -100, -200] + assert is_min_heap(a[:5]) + assert a[5:] == [-100, -200] + + +def test_heapify_size_smaller_than_zero_swaps_nothing() -> None: + # если size = 0 → функция не должна ничего менять + a = [3, 2, 1] + + swaps = heapify(0, a) + + assert swaps == [] + assert a == [3, 2, 1] + + +def test_heapify_returns_empty_for_already_valid_heap() -> None: + # если массив уже min-heap → никаких свапов не происходит + a = [1, 3, 2, 7, 6, 4, 5] + + swaps = heapify(len(a), a) + + assert swaps == [] + assert a == [1, 3, 2, 7, 6, 4, 5] + assert is_min_heap(a) + + +@pytest.mark.parametrize( + "a", + [ + [7, 6, 5, 4, 3, 2, 1], + [10, 9, 8, 7, 6, 5, 4, 3], + [4, 1, 3, 2, 16, 9, 10, 14, 8, 7], + [3, 1, 1, 0, 2, 2, -1], + ], +) +def test_heapify_general_properties(a) -> None: + # проверяем общие свойства: + # - результат является min-heap + # - элементы не теряются + # - свапы корректно воспроизводят результат + original = a[:] + + swaps = heapify(len(a), a) + + assert is_min_heap(a) + assert sorted(a) == sorted(original) + assert replay_swaps(original, swaps) == a + assert all(0 <= i < len(a) and 0 <= j < len(a) for i, j in swaps) + + +def test_heapify_prefers_right_child_when_right_is_smaller() -> None: + # если правый ребёнок меньше левого → выбирается правый + a = [3, 2, 1] + + swaps = heapify(len(a), a) + + assert swaps == [(0, 2)] + assert a == [1, 2, 3] + + +def test_heapify_prefers_left_child_when_children_are_equal() -> None: + # если дети равны → выбирается левый (по коду) + a = [3, 1, 1] + + swaps = heapify(len(a), a) + + assert swaps == [(0, 1)] + assert a == [1, 3, 1] + + +def test_heapify_stops_when_parent_equals_child() -> None: + # если родитель <= ребёнка → sift_down останавливается + a = [1, 1, 2] + + swaps = heapify(len(a), a) + + assert swaps == [] + assert a == [1, 1, 2] diff --git a/tests/test_heaps/test_huffman.py b/tests/test_heaps/test_huffman.py new file mode 100644 index 0000000..0db8380 --- /dev/null +++ b/tests/test_heaps/test_huffman.py @@ -0,0 +1,53 @@ +import pytest + +from src.n_heaps.huffman import huffman_decode, huffman_encode, huffman_tree + + +# O(n) +def is_prefix_code(tree: dict[str, str]) -> bool: + codes = list(tree.values()) + for i in range(len(codes)): + for j in range(i + 1, len(codes)): + # Если один код является префиксом другого, то это не префиксный код. + if codes[i].startswith(codes[j]) or codes[j].startswith(codes[i]): + return False + return True + + +@pytest.mark.parametrize( + ("tree", "expected"), + [ + ({"a": "0"}, True), + ({"a": "0", "b": "1"}, True), + ({"a": "00", "b": "01", "c": "1"}, True), + ({"a": "00", "b": "01", "c": "10", "d": "11"}, True), + ({"a": "000", "b": "001", "c": "01", "d": "10", "e": "11"}, True), + ({"a": "00", "b": "00"}, False), # 'a' и 'b' имеют одинаковые коды. + ({"a": "0", "b": "01"}, False), # 'a' является префиксом 'b'. + ], +) +def test_is_prefix_code(tree: dict[str, str], expected: bool) -> None: + assert is_prefix_code(tree) == expected + + +@pytest.mark.parametrize( + "string", + ["a", "ab", "abc", "aabbcc", "aabbccdd", "aabbccddeeff", "hello world", "aaaaaabbcccddddeeeee"], +) +def test_huffman_tree(string: str) -> None: + tree = huffman_tree(string) + assert is_prefix_code(tree) + + +@pytest.mark.parametrize( + "string", + ["a", "ab", "abc", "aabbcc", "aabbccdd", "aabbccddeeff", "hello world", "aaaaaabbcccddddeeeee"], +) +def test_huffman_encode_decode(string: str) -> None: + encoded, tree = huffman_encode(string) + + assert len(encoded) <= 8 * len(string) + assert all(char in "01" for char in encoded) + + decode_tree = {v: k for k, v in tree.items()} + assert huffman_decode(encoded, decode_tree) == string diff --git a/tests/test_heaps/test_parallel.py b/tests/test_heaps/test_parallel.py new file mode 100644 index 0000000..39f1476 --- /dev/null +++ b/tests/test_heaps/test_parallel.py @@ -0,0 +1,216 @@ +import pytest + +from src.n_heaps.parallel import parallel + + +def test_parallel_sample_1() -> None: + # пример 1 из условия: разные времена задач, 2 процессора + assert parallel(2, 5, [1, 2, 3, 4, 5]) == [ + (0, 0), + (1, 0), + (0, 1), + (1, 2), + (0, 4), + ] + + +def test_parallel_sample_2() -> None: + # пример 2 из условия: 4 процессора, все задачи длятся 1 + assert parallel(4, 20, [1] * 20) == [ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + (0, 1), + (1, 1), + (2, 1), + (3, 1), + (0, 2), + (1, 2), + (2, 2), + (3, 2), + (0, 3), + (1, 3), + (2, 3), + (3, 3), + (0, 4), + (1, 4), + (2, 4), + (3, 4), + ] + + +def test_parallel_one_processor() -> None: + # один процессор: все задачи выполняются строго последовательно + assert parallel(1, 5, [3, 2, 4, 1, 5]) == [ + (0, 0), + (0, 3), + (0, 5), + (0, 9), + (0, 10), + ] + + +def test_parallel_more_processors_than_tasks() -> None: + # процессоров больше, чем задач: каждая задача стартует в 0 + assert parallel(5, 3, [10, 20, 30]) == [ + (0, 0), + (1, 0), + (2, 0), + ] + + +def test_parallel_processors_equal_tasks() -> None: + # процессоров столько же, сколько задач: все задачи стартуют в 0 + assert parallel(3, 3, [5, 1, 10]) == [ + (0, 0), + (1, 0), + (2, 0), + ] + + +def test_parallel_zero_duration_tasks() -> None: + # задачи длительностью 0 не увеличивают время освобождения процессора + assert parallel(2, 5, [0, 0, 0, 0, 0]) == [ + (0, 0), + (0, 0), + (0, 0), + (0, 0), + (0, 0), + ] + + +def test_parallel_mixed_zero_and_positive_tasks() -> None: + # нулевые задачи сразу освобождают процессор и влияют на выбор по номеру + assert parallel(2, 6, [0, 5, 0, 2, 1, 0]) == [ + (0, 0), + (0, 0), + (1, 0), + (1, 0), + (1, 2), + (1, 3), + ] + + +def test_parallel_tie_choose_smallest_processor_id() -> None: + # если несколько процессоров свободны одновременно, выбирается меньший номер + assert parallel(3, 6, [2, 2, 2, 1, 1, 1]) == [ + (0, 0), + (1, 0), + (2, 0), + (0, 2), + (1, 2), + (2, 2), + ] + + +def test_parallel_tie_after_different_durations() -> None: + # проверяем tie-breaker после задач разной длины + assert parallel(2, 5, [5, 1, 4, 1, 1]) == [ + (0, 0), + (1, 0), + (1, 1), + (0, 5), + (1, 5), + ] + + +def test_parallel_large_values() -> None: + # большие значения времени не должны ломать вычисления + assert parallel(2, 4, [10**9, 10**9, 10**9, 10**9]) == [ + (0, 0), + (1, 0), + (0, 10**9), + (1, 10**9), + ] + + +def test_parallel_empty_time_but_m_zero() -> None: + # формально по условию m >= 1, но функция корректно возвращает пустой ответ при m = 0 + assert parallel(3, 0, []) == [] + + +def test_parallel_uses_only_first_m_tasks() -> None: + # функция обрабатывает только первые m элементов списка time + assert parallel(2, 3, [1, 2, 3, 100, 200]) == [ + (0, 0), + (1, 0), + (0, 1), + ] + + +def test_parallel_raises_if_m_greater_than_time_length() -> None: + # если m больше длины time, текущая реализация падает с IndexError + with pytest.raises(IndexError): + parallel(2, 3, [1, 2]) + + +def test_parallel_raises_if_no_processors() -> None: + # если n = 0, куча пустая, heappop падает с IndexError + with pytest.raises(IndexError): + parallel(0, 1, [10]) + + +@pytest.mark.parametrize( + ("n", "time", "expected"), + [ + ( + 2, + [1, 1, 1, 1], + [(0, 0), (1, 0), (0, 1), (1, 1)], + ), + ( + 3, + [3, 1, 2, 1, 1], + [(0, 0), (1, 0), (2, 0), (1, 1), (1, 2)], + ), + ( + 4, + [2, 4, 1, 3, 5, 2], + [(0, 0), (1, 0), (2, 0), (3, 0), (2, 1), (0, 2)], + ), + ], +) +def test_parallel_exact_cases(n, time, expected) -> None: + # несколько точных сценариев для разных n и длительностей задач + assert parallel(n, len(time), time) == expected + + +@pytest.mark.parametrize( + ("n", "time"), + [ + (1, [1, 2, 3, 4]), + (2, [5, 0, 2, 0, 3]), + (3, [1, 1, 1, 1, 1, 1, 1]), + (4, [10, 1, 7, 3, 0, 2, 8]), + (5, [0, 0, 5, 5, 1, 1, 10]), + ], +) +def test_parallel_general_properties(n, time) -> None: + # общие инварианты: + # - ответ имеет длину m + # - номера процессоров в допустимом диапазоне + # - время старта неотрицательное + # - каждый процессор получает задачи в неубывающем порядке времени старта + result = parallel(n, len(time), time) + + assert len(result) == len(time) + assert all(0 <= processor < n for processor, _ in result) + assert all(start_time >= 0 for _, start_time in result) + + starts_by_processor: dict[int, list[int]] = {} + for processor, start_time in result: + starts_by_processor.setdefault(processor, []).append(start_time) + + for starts in starts_by_processor.values(): + assert starts == sorted(starts) + + +def test_parallel_input_list_is_not_modified() -> None: + # функция не должна изменять список длительностей задач + time = [3, 1, 2, 4] + original = time[:] + + parallel(2, len(time), time) + + assert time == original diff --git a/tests/test_heaps/test_priority_queue.py b/tests/test_heaps/test_priority_queue.py new file mode 100644 index 0000000..a59b65d --- /dev/null +++ b/tests/test_heaps/test_priority_queue.py @@ -0,0 +1,294 @@ +import pytest + +from src.n_heaps.priority_queue import PriorityQueue + + +def is_max_heap(a: list[int]) -> bool: + return all(a[i] >= a[2 * i + 1] and (2 * i + 2 >= len(a) or a[i] >= a[2 * i + 2]) for i in range(len(a) // 2)) + + +def test_init_empty_queue() -> None: + # пустая очередь создаётся без ошибок + pq = PriorityQueue() + + assert pq.arr == [] + assert pq.size == 0 + + +def test_init_heapifies_array() -> None: + # при создании из массива он превращается в max-heap + pq = PriorityQueue([1, 5, 3, 2, 4]) + + assert pq.size == 5 + assert is_max_heap(pq.arr) + assert sorted(pq.arr) == [1, 2, 3, 4, 5] + + +def test_init_keeps_already_valid_heap() -> None: + # уже корректная max-heap остаётся валидной + pq = PriorityQueue([10, 7, 9, 1, 3, 4]) + + assert pq.arr == [10, 7, 9, 1, 3, 4] + assert pq.size == 6 + assert is_max_heap(pq.arr) + + +def test_swap_changes_two_elements() -> None: + # swap меняет местами два элемента + pq = PriorityQueue([3, 2, 1]) + + pq.swap(0, 2) + + assert pq.arr == [1, 2, 3] + + +def test_sift_up_moves_element_to_root() -> None: + # sift_up поднимает большой элемент до корня + pq = PriorityQueue([10, 7, 9]) + pq.arr.append(15) + pq.size += 1 + + pq.sift_up(3) + + assert pq.arr[0] == 15 + assert is_max_heap(pq.arr) + + +def test_sift_up_does_nothing_when_parent_is_larger() -> None: + # если родитель больше, sift_up ничего не меняет + pq = PriorityQueue([10, 7, 9]) + before = pq.arr[:] + + pq.sift_up(1) + + assert pq.arr == before + assert is_max_heap(pq.arr) + + +def test_sift_up_does_nothing_for_root() -> None: + # для корня sift_up ничего не делает + pq = PriorityQueue([10, 7, 9]) + before = pq.arr[:] + + pq.sift_up(0) + + assert pq.arr == before + + +def test_sift_down_moves_small_root_down() -> None: + # sift_down опускает слишком маленький корень вниз + pq = PriorityQueue([10, 9, 8, 7, 6]) + pq.arr[0] = 1 + + pq.sift_down(0) + + assert is_max_heap(pq.arr) + assert pq.arr[0] == 9 + + +def test_sift_down_prefers_right_child_when_right_is_larger() -> None: + # если правый потомок больше левого, выбирается правый + pq = PriorityQueue([10, 9, 8]) + pq.arr = [1, 2, 3] + pq.size = 3 + + pq.sift_down(0) + + assert pq.arr == [3, 2, 1] + assert is_max_heap(pq.arr) + + +def test_sift_down_prefers_left_child_when_children_are_equal() -> None: + # если потомки равны, выбирается левый + pq = PriorityQueue([10, 9, 8]) + pq.arr = [1, 3, 3] + pq.size = 3 + + pq.sift_down(0) + + assert pq.arr == [3, 1, 3] + assert is_max_heap(pq.arr) + + +def test_sift_down_stops_when_parent_is_equal_to_child() -> None: + # если родитель равен максимальному потомку, sift_down останавливается + pq = PriorityQueue([3, 3, 2]) + before = pq.arr[:] + + pq.sift_down(0) + + assert pq.arr == before + assert is_max_heap(pq.arr) + + +def test_insert_into_empty_queue() -> None: + # вставка в пустую очередь + pq = PriorityQueue() + + pq.insert(10) + + assert pq.arr == [10] + assert pq.size == 1 + assert is_max_heap(pq.arr) + + +def test_insert_small_element_stays_leaf() -> None: + # маленький элемент остаётся внизу + pq = PriorityQueue([10, 7, 9]) + + pq.insert(1) + + assert pq.size == 4 + assert sorted(pq.arr) == [1, 7, 9, 10] + assert is_max_heap(pq.arr) + + +def test_insert_large_element_becomes_root() -> None: + # большой элемент поднимается в корень + pq = PriorityQueue([10, 7, 9]) + + pq.insert(100) + + assert pq.arr[0] == 100 + assert pq.size == 4 + assert sorted(pq.arr) == [7, 9, 10, 100] + assert is_max_heap(pq.arr) + + +def test_insert_duplicates() -> None: + # дубликаты корректно хранятся в куче + pq = PriorityQueue([5, 5, 5]) + + pq.insert(5) + + assert pq.size == 4 + assert pq.arr == [5, 5, 5, 5] + assert is_max_heap(pq.arr) + + +def test_insert_negative_numbers() -> None: + # отрицательные числа тоже поддерживаются + pq = PriorityQueue([-10, -20, -30]) + + pq.insert(-5) + + assert pq.arr[0] == -5 + assert pq.size == 4 + assert sorted(pq.arr) == [-30, -20, -10, -5] + assert is_max_heap(pq.arr) + + +def test_extract_max_from_one_element_queue() -> None: + # извлечение единственного элемента + pq = PriorityQueue([42]) + + result = pq.extract_max() + + assert result == 42 + assert pq.arr == [] + assert pq.size == 0 + + +def test_extract_max_returns_largest_element() -> None: + # extract_max возвращает максимум + pq = PriorityQueue([1, 5, 3, 2, 4]) + + result = pq.extract_max() + + assert result == 5 + assert pq.size == 4 + assert sorted(pq.arr) == [1, 2, 3, 4] + assert is_max_heap(pq.arr) + + +def test_extract_max_multiple_times_returns_descending_order() -> None: + # последовательные извлечения возвращают элементы по убыванию + values = [3, 1, 10, 7, 2, 10, -1] + pq = PriorityQueue(values) + + extracted = [pq.extract_max() for _ in range(len(values))] + + assert extracted == sorted(values, reverse=True) + assert pq.arr == [] + assert pq.size == 0 + + +def test_extract_max_with_duplicates() -> None: + # одинаковые максимумы извлекаются корректно + pq = PriorityQueue([5, 1, 5, 3]) + + assert pq.extract_max() == 5 + assert pq.extract_max() == 5 + assert pq.extract_max() == 3 + assert pq.extract_max() == 1 + assert pq.arr == [] + assert pq.size == 0 + + +def test_extract_max_with_negative_numbers() -> None: + # максимум среди отрицательных чисел — число ближе к нулю + pq = PriorityQueue([-10, -1, -5, -20]) + + result = pq.extract_max() + + assert result == -1 + assert pq.size == 3 + assert sorted(pq.arr) == [-20, -10, -5] + assert is_max_heap(pq.arr) + + +def test_extract_max_from_empty_queue_raises_index_error() -> None: + # текущая реализация на пустой очереди падает с IndexError + pq = PriorityQueue() + + with pytest.raises(IndexError): + pq.extract_max() + + +def test_heapify_empty_array() -> None: + # heapify для пустого массива ничего не делает + pq = PriorityQueue() + + pq.heapify() + + assert pq.arr == [] + assert pq.size == 0 + + +def test_heapify_single_element() -> None: + # heapify для одного элемента ничего не меняет + pq = PriorityQueue([1]) + + pq.heapify() + + assert pq.arr == [1] + assert pq.size == 1 + assert is_max_heap(pq.arr) + + +@pytest.mark.parametrize( + "values", + [ + [1, 2, 3, 4, 5], # возрастающий массив + [5, 4, 3, 2, 1], # убывающий массив + [4, 1, 3, 2, 16, 9, 10], # смешанный массив + [0, -1, -2, -3, -4], # отрицательные числа + [2, 2, 2, 2], # все элементы равны + [7, 1, 7, 3, 7, 2], # дубликаты максимумов + ], +) +def test_priority_queue_general_properties(values: list[int]) -> None: + # общие свойства очереди: + # - после heapify структура является max-heap + # - все элементы сохранены + # - extract_max возвращает элементы по убыванию + pq = PriorityQueue(values[:]) + + assert is_max_heap(pq.arr) + assert sorted(pq.arr) == sorted(values) + + extracted = [pq.extract_max() for _ in range(len(values))] + + assert extracted == sorted(values, reverse=True) + assert pq.arr == [] + assert pq.size == 0