[1] data structures #259
169
SobolevNS/docs/report_01.md
Normal file
169
SobolevNS/docs/report_01.md
Normal file
|
|
@ -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. Графики
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## 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`,
|
||||
индексы в БД и т. д.).
|
||||
Loading…
Reference in New Issue
Block a user