2026-rff_mp/SobolevNS/docs/report_01.md

11 KiB
Raw Blame History

Отчёт по заданию 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) (inorder, сразу отсортировано) O(n)

3. Эксперимент

3.1. Методика

  • Генерируем N = 10 000 записей вида ("User_00000", "555-0000000") и т.д.
  • Два режима ввода: shuffled (случайный) и sorted (отсортированный по имени).
  • Каждое испытание повторяется 5 раз, в CSV сохраняем все 5 значений и среднее.
  • На каждом испытании:
    1. строим структуру заново;
    2. вставляем все N записей - замеряем время;
    3. ищем 100 случайных существующих имён + 10 несуществующих (всего 110 вызовов);
    4. удаляем 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. Графики

Вставка

Поиск

Удаление

Деградация BST на отсортированных данных

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 занимают в 510 раз больше времени, чем у 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 (с балансировкой) inorder обход - это и есть сортировка, O(N)
Очень мало данных или редкие операции; нужна простота Связный список минимум кода, O(N) - приемлемо
Нужны диапазонные запросы («все имена от 'A' до 'D'») BST/сбалансированное хеш-таблица их не умеет
Гарантированная производительность на любых данных Хеш-таблица или AVL/RB-tree, но не наивное BST наивное BST уязвимо к отсортированному входу

Хеш-таблица в стандартной библиотеке Python - это и есть встроенный dict, которым стоит пользоваться по умолчанию для пар «ключ → значение». Понимать «руками» связный список и BST полезно, чтобы знать, что лежит под капотом более сложных контейнеров и понимать, когда их применять (например, OrderedDict, sortedcontainers.SortedDict, индексы в БД и т. д.).