11 KiB
Отчёт по заданию 1. Структуры данных: телефонный справочник
1. Цель работы
Реализовать три структуры данных «руками» (без классов, в процедурной парадигме):
связный список, хеш‑таблицу с цепочками и двоичное дерево поиска (BST).
Сравнить их на одной и той же задаче - телефонный справочник с операциями insert,
find, delete, list_all - и понять, какая структура когда лучше.
2. Что и как реализовано
Все три структуры реализованы в phonebook.py. Каждая - это набор функций,
которые получают/возвращают текущее состояние (узел или контейнер).
Объектная парадигма сознательно не использовалась - это позволяет «увидеть руки»
каждой операции.
2.1. Связный список
Узел - обычный словарь {'name', 'phone', 'next'}. Голова списка - это либо
такой словарь, либо None.
Ключевые функции (полный код - в phonebook.py):
| Функция | Сложность |
|---|---|
ll_insert(head, name, phone) |
вставка в конец или обновление: O(n) |
ll_find(head, name) |
линейный поиск: O(n) |
ll_delete(head, name) |
O(n) |
ll_list_all(head) |
собрать всё + сортировка: O(n log n) |
В ll_insert я специально хожу до конца, чтобы потом сравнить с худшим
случаем - это объясняет «провал» на графиках вставки (см. ниже).
2.2. Хеш-таблица
Контейнер - словарь {'size': int, 'buckets': [head|None, ...]}. Каждый бакет -
голова связного списка, и внутри бакета работают всё те же ll_* функции.
Хеш-функция: hash(name) % size. Размер таблицы - 2048.
| Функция | Сложность (амортизированно) |
|---|---|
ht_insert/ht_find/ht_delete |
O(1) при равномерной хеш-функции |
ht_list_all |
O(n log n) (надо собрать всё и отсортировать) |
2.3. Двоичное дерево поиска (BST)
Узел - словарь {'name', 'phone', 'left', 'right'}. Реализация итеративная,
без рекурсии - это важно для эксперимента с отсортированным входом (вырожденное
дерево глубины N, рекурсия упёрлась бы в лимит).
| Функция | Случайные данные | Отсортированные данные |
|---|---|---|
bst_insert/bst_find/bst_delete |
O(log n) | O(n) (вырожденный «список») |
bst_list_all |
O(n) (in‑order, сразу отсортировано) | O(n) |
3. Эксперимент
3.1. Методика
- Генерируем
N = 10 000записей вида("User_00000", "555-0000000")и т.д. - Два режима ввода: shuffled (случайный) и sorted (отсортированный по имени).
- Каждое испытание повторяется 5 раз, в CSV сохраняем все 5 значений и среднее.
- На каждом испытании:
- строим структуру заново;
- вставляем все N записей - замеряем время;
- ищем 100 случайных существующих имён + 10 несуществующих (всего 110 вызовов);
- удаляем 50 случайных имён.
Замер - time.perf_counter(). Все значения в секундах в CSV, в отчёте перевожу
в миллисекунды.
Для BST в режиме
sortedпришлось снизить N до 2 000. При N = 10 000 вставка занимает десятки минут (сложность операции -O(N**2), дерево превращается в связный список). Это и есть главная иллюстрация «деградации BST».
3.2. Результаты (средние, миллисекунды)
| Структура | Режим | N | вставка | поиск 110 ключей | удаление 50 |
|---|---|---|---|---|---|
| LinkedList | shuffled | 10 000 | 4 027 | 34.87 | 14.43 |
| LinkedList | sorted | 10 000 | 3 056 | 27.58 | 13.34 |
| HashTable | shuffled | 10 000 | 6.71 | 0.068 | 0.038 |
| HashTable | sorted | 10 000 | 6.42 | 0.068 | 0.033 |
| BST | shuffled | 10 000 | 16.84 | 0.172 | 0.115 |
| BST | sorted | 2 000 | 121.17 | 5.20 | 7.48 |
Полные сырые данные - в data/results.csv.
3.3. Графики
4. Анализ
4.1. Почему связный список такой медленный на вставке
Мой ll_insert идёт до конца списка, чтобы потом обновить узел, если он уже
есть. На N‑м элементе он совершает уже N шагов. Суммарно - около N**2/2
операций. На N = 10 000 это даёт ~50 миллионов проходов по узлам - отсюда
и ~4 секунды.
Если бы мы вставляли в начало (за O(1)), общая вставка стала бы линейной, но тогда:
- возможны дубликаты (обновление пропускается);
- для совместимости с двумя другими структурами всё равно понадобился бы поиск.
Поиск и удаление быстрее: 110 поисков ≈ 35 мс, потому что в среднем ищем N/2 шагов и таких запросов всего 110. Сложность каждой операции - O(N), но запросов мало.
4.2. Почему хеш-таблица почти не зависит от порядка
Хеш-функция превращает имя в индекс бакета. Сами имена User_00001,
User_00002, … равномерно распределяются по 2 048 бакетам, поэтому в каждом
бакете лежит в среднем ≈ 5 элементов. Все три операции работают за ~O(1).
Порядок входных данных не имеет значения, потому что hash(name) от него не
зависит. На графике видно: shuffled и sorted столбцы у HashTable одинаковой
высоты.
4.3. Почему BST деградирует на отсортированном входе
Когда имена приходят в алфавитном порядке, каждый новый ключ всегда больше предыдущего, поэтому он идёт в правое поддерево. Дерево превращается в правый «костыль» - фактически в односвязный список. Сложность вставки - O(N**2), поиска и удаления - O(N).
На графике bst_degradation.png это видно очень ярко: даже при в 5 раз
меньшем N (2 000 вместо 10 000) все операции у sorted BST занимают
в 5–10 раз больше времени, чем у shuffled BST.
Это известная слабость наивного BST. В реальном коде её решают самобалансирующимися деревьями (AVL, красно-чёрные, B-деревья), которые гарантируют глубину O(log n) даже на отсортированном входе.
4.4. Удаление
- В связном списке удаление - это пробежка до нужного узла + переключение ссылок. На 50 запросах суммарно ~14 мс при N = 10 000.
- В хеш-таблице удаление - это пробежка по бакету (короткий список), почти O(1): всего 0.038 мс на 50 удалений.
- В BST стандартное удаление: если у узла двое детей, заменяем его на минимум правого поддерева и рекурсивно удаляем его. На случайном дереве - O(log n) на удаление; на отсортированном - O(N).
5. Что выбирать в реальной жизни
| Сценарий | Лучший выбор | Почему |
|---|---|---|
Частые insert/find/delete, порядок не нужен |
Хеш-таблица | O(1) на всё |
| Нужно много раз получать отсортированный список | BST (с балансировкой) | in‑order обход - это и есть сортировка, O(N) |
| Очень мало данных или редкие операции; нужна простота | Связный список | минимум кода, O(N) - приемлемо |
| Нужны диапазонные запросы («все имена от 'A' до 'D'») | BST/сбалансированное | хеш-таблица их не умеет |
| Гарантированная производительность на любых данных | Хеш-таблица или AVL/RB-tree, но не наивное BST | наивное BST уязвимо к отсортированному входу |
Хеш-таблица в стандартной библиотеке Python - это и есть встроенный dict,
которым стоит пользоваться по умолчанию для пар «ключ → значение».
Понимать «руками» связный список и BST полезно, чтобы знать, что лежит
под капотом более сложных контейнеров и понимать,
когда их применять (например, OrderedDict, sortedcontainers.SortedDict,
индексы в БД и т. д.).



