From 75a182304e37662a48717d2c740a4b8dc53eb2aa Mon Sep 17 00:00:00 2001 From: SobolevNS Date: Fri, 22 May 2026 12:24:28 +0300 Subject: [PATCH] add data structures report --- SobolevNS/docs/report_01.md | 169 ++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 SobolevNS/docs/report_01.md diff --git a/SobolevNS/docs/report_01.md b/SobolevNS/docs/report_01.md new file mode 100644 index 0000000..fd4271c --- /dev/null +++ b/SobolevNS/docs/report_01.md @@ -0,0 +1,169 @@ +# Отчёт по заданию 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 значений + и среднее. +* На каждом испытании: + 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. Графики + +![Вставка](data/task1_data_structures/docs/data/plots/insert_compare.png) + +![Поиск](data/task1_data_structures/docs/data/plots/find_compare.png) + +![Удаление](data/task1_data_structures/docs/data/plots/delete_compare.png) + +![Деградация BST на отсортированных данных](data/task1_data_structures/docs/data/plots/bst_degradation.png) + + +## 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`, +индексы в БД и т. д.).