Merge pull request '[1] data structures' (#259) from SobolevNS/2026-rff_mp:01 into develop

Reviewed-on: UNN/2026-rff_mp#259
This commit is contained in:
kit8nino 2026-05-30 11:42:56 +00:00
commit fabb5cd5c1
10 changed files with 882 additions and 0 deletions

View File

@ -0,0 +1,22 @@
# Задание 1. Структуры данных - телефонный справочник
Реализация связного списка, хеш-таблицы и BST в процедурной парадигме (без классов).
## Как запустить
```bash
# 1) самопроверка структур
python3 phonebook.py
# 2) эксперимент (5 повторов × 3 структуры × 2 режима × 3 операции)
python3 experiment.py
# результат -> docs/data/results.csv
# 3) графики
python3 plot_results.py
# результат -> docs/data/plots/*.png
```
## Отчёт
См. [docs/report.md](docs/report.md).

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,112 @@
Структура,Режим,Операция,N,Trial,Время (сек)
LinkedList,shuffled,insert,10000,1,3.995075
LinkedList,shuffled,insert,10000,2,4.133491
LinkedList,shuffled,insert,10000,3,4.067831
LinkedList,shuffled,insert,10000,4,4.075638
LinkedList,shuffled,insert,10000,5,4.059367
LinkedList,shuffled,find,10000,1,0.037171
LinkedList,shuffled,find,10000,2,0.035212
LinkedList,shuffled,find,10000,3,0.040434
LinkedList,shuffled,find,10000,4,0.030968
LinkedList,shuffled,find,10000,5,0.033432
LinkedList,shuffled,delete,10000,1,0.014029
LinkedList,shuffled,delete,10000,2,0.016408
LinkedList,shuffled,delete,10000,3,0.017498
LinkedList,shuffled,delete,10000,4,0.013770
LinkedList,shuffled,delete,10000,5,0.016273
LinkedList,sorted,insert,10000,1,3.083864
LinkedList,sorted,insert,10000,2,3.123097
LinkedList,sorted,insert,10000,3,3.084625
LinkedList,sorted,insert,10000,4,3.200015
LinkedList,sorted,insert,10000,5,3.124164
LinkedList,sorted,find,10000,1,0.029411
LinkedList,sorted,find,10000,2,0.029928
LinkedList,sorted,find,10000,3,0.027749
LinkedList,sorted,find,10000,4,0.032859
LinkedList,sorted,find,10000,5,0.032080
LinkedList,sorted,delete,10000,1,0.016454
LinkedList,sorted,delete,10000,2,0.013526
LinkedList,sorted,delete,10000,3,0.015424
LinkedList,sorted,delete,10000,4,0.014688
LinkedList,sorted,delete,10000,5,0.012838
HashTable,shuffled,insert,10000,1,0.007074
HashTable,shuffled,insert,10000,2,0.006665
HashTable,shuffled,insert,10000,3,0.007361
HashTable,shuffled,insert,10000,4,0.007405
HashTable,shuffled,insert,10000,5,0.007248
HashTable,shuffled,find,10000,1,0.000072
HashTable,shuffled,find,10000,2,0.000062
HashTable,shuffled,find,10000,3,0.000063
HashTable,shuffled,find,10000,4,0.000062
HashTable,shuffled,find,10000,5,0.000066
HashTable,shuffled,delete,10000,1,0.000037
HashTable,shuffled,delete,10000,2,0.000034
HashTable,shuffled,delete,10000,3,0.000032
HashTable,shuffled,delete,10000,4,0.000030
HashTable,shuffled,delete,10000,5,0.000032
HashTable,sorted,insert,10000,1,0.007131
HashTable,sorted,insert,10000,2,0.006610
HashTable,sorted,insert,10000,3,0.006701
HashTable,sorted,insert,10000,4,0.006979
HashTable,sorted,insert,10000,5,0.008910
HashTable,sorted,find,10000,1,0.000065
HashTable,sorted,find,10000,2,0.000056
HashTable,sorted,find,10000,3,0.000068
HashTable,sorted,find,10000,4,0.000066
HashTable,sorted,find,10000,5,0.000076
HashTable,sorted,delete,10000,1,0.000036
HashTable,sorted,delete,10000,2,0.000037
HashTable,sorted,delete,10000,3,0.000038
HashTable,sorted,delete,10000,4,0.000045
HashTable,sorted,delete,10000,5,0.000042
BST,shuffled,insert,10000,1,0.018043
BST,shuffled,insert,10000,2,0.019312
BST,shuffled,insert,10000,3,0.017282
BST,shuffled,insert,10000,4,0.021092
BST,shuffled,insert,10000,5,0.016847
BST,shuffled,find,10000,1,0.000157
BST,shuffled,find,10000,2,0.000210
BST,shuffled,find,10000,3,0.000168
BST,shuffled,find,10000,4,0.000138
BST,shuffled,find,10000,5,0.000193
BST,shuffled,delete,10000,1,0.000129
BST,shuffled,delete,10000,2,0.000147
BST,shuffled,delete,10000,3,0.000122
BST,shuffled,delete,10000,4,0.000161
BST,shuffled,delete,10000,5,0.000128
BST,sorted,insert,2000,1,0.123235
BST,sorted,insert,2000,2,0.118658
BST,sorted,insert,2000,3,0.119944
BST,sorted,insert,2000,4,0.121595
BST,sorted,insert,2000,5,0.116209
BST,sorted,find,2000,1,0.005019
BST,sorted,find,2000,2,0.005133
BST,sorted,find,2000,3,0.005032
BST,sorted,find,2000,4,0.004812
BST,sorted,find,2000,5,0.004964
BST,sorted,delete,2000,1,0.008319
BST,sorted,delete,2000,2,0.007798
BST,sorted,delete,2000,3,0.007584
BST,sorted,delete,2000,4,0.008061
BST,sorted,delete,2000,5,0.007642
--- СРЕДНИЕ ---
Структура,Режим,Операция,N,Среднее (сек),Все замеры (сек)
LinkedList,shuffled,insert,10000,4.066280,3.995075;4.133491;4.067831;4.075638;4.059367
LinkedList,shuffled,find,10000,0.035443,0.037171;0.035212;0.040434;0.030968;0.033432
LinkedList,shuffled,delete,10000,0.015596,0.014029;0.016408;0.017498;0.013770;0.016273
LinkedList,sorted,insert,10000,3.123153,3.083864;3.123097;3.084625;3.200015;3.124164
LinkedList,sorted,find,10000,0.030406,0.029411;0.029928;0.027749;0.032859;0.032080
LinkedList,sorted,delete,10000,0.014586,0.016454;0.013526;0.015424;0.014688;0.012838
HashTable,shuffled,insert,10000,0.007151,0.007074;0.006665;0.007361;0.007405;0.007248
HashTable,shuffled,find,10000,0.000065,0.000072;0.000062;0.000063;0.000062;0.000066
HashTable,shuffled,delete,10000,0.000033,0.000037;0.000034;0.000032;0.000030;0.000032
HashTable,sorted,insert,10000,0.007266,0.007131;0.006610;0.006701;0.006979;0.008910
HashTable,sorted,find,10000,0.000066,0.000065;0.000056;0.000068;0.000066;0.000076
HashTable,sorted,delete,10000,0.000040,0.000036;0.000037;0.000038;0.000045;0.000042
BST,shuffled,insert,10000,0.018515,0.018043;0.019312;0.017282;0.021092;0.016847
BST,shuffled,find,10000,0.000173,0.000157;0.000210;0.000168;0.000138;0.000193
BST,shuffled,delete,10000,0.000138,0.000129;0.000147;0.000122;0.000161;0.000128
BST,sorted,insert,2000,0.119928,0.123235;0.118658;0.119944;0.121595;0.116209
BST,sorted,find,2000,0.004992,0.005019;0.005133;0.005032;0.004812;0.004964
BST,sorted,delete,2000,0.007881,0.008319;0.007798;0.007584;0.008061;0.007642
Can't render this file because it has a wrong number of fields in line 93.

View File

@ -0,0 +1,194 @@
"""
experiment.py
Замеры производительности трёх структур данных на одних и тех же данных:
LinkedList, HashTable, BST
в двух режимах:
случайный порядок (shuffled), отсортированный порядок (sorted)
для трёх операций:
insert N записей, find 110 раз, delete 50 раз
Каждый эксперимент повторяется TRIALS раз. Сохраняем все замеры + средние
в CSV. Для BST на отсортированных данных снижаем N - иначе эксперимент
длится десятки минут (вырожденное дерево, O(N^2) вставка).
"""
import csv
import os
import random
import time
import phonebook as pb
# ---------- параметры эксперимента ----------
N = 10_000 # число записей в основном эксперименте
N_BST_SORTED = 2_000 # для BST на отсортированных данных - меньше (O(N^2))
TRIALS = 5 # количество повторов каждого замера
N_FIND_EXISTING = 100
N_FIND_MISSING = 10
N_DELETE = 50
HT_SIZE = 2048 # размер хеш-таблицы
RNG_SEED = 42
OUT_CSV = os.path.join("docs", "data", "results.csv")
# --------------------------------------------
def gen_records(n):
"""Генерирует n записей вида ('User_00001', '555-0001-...')."""
return [(f"User_{i:05d}", f"555-{i:07d}") for i in range(n)]
def pick_keys(records, k_exist, k_miss, rng):
"""Выбирает k_exist существующих имён и k_miss отсутствующих."""
existing = [name for name, _ in rng.sample(records, k_exist)]
missing = [f"None_{i}" for i in range(k_miss)]
return existing + missing
# ---------- замеры по структурам ----------
def measure_linked_list(records, find_keys, delete_keys):
# вставка
t0 = time.perf_counter()
head = pb.ll_create()
for name, phone in records:
head = pb.ll_insert(head, name, phone)
t_insert = time.perf_counter() - t0
# поиск
t0 = time.perf_counter()
for name in find_keys:
pb.ll_find(head, name)
t_find = time.perf_counter() - t0
# удаление
t0 = time.perf_counter()
for name in delete_keys:
head = pb.ll_delete(head, name)
t_delete = time.perf_counter() - t0
return t_insert, t_find, t_delete
def measure_hash_table(records, find_keys, delete_keys):
t0 = time.perf_counter()
ht = pb.ht_create(size=HT_SIZE)
for name, phone in records:
pb.ht_insert(ht, name, phone)
t_insert = time.perf_counter() - t0
t0 = time.perf_counter()
for name in find_keys:
pb.ht_find(ht, name)
t_find = time.perf_counter() - t0
t0 = time.perf_counter()
for name in delete_keys:
pb.ht_delete(ht, name)
t_delete = time.perf_counter() - t0
return t_insert, t_find, t_delete
def measure_bst(records, find_keys, delete_keys):
t0 = time.perf_counter()
root = pb.bst_create()
for name, phone in records:
root = pb.bst_insert(root, name, phone)
t_insert = time.perf_counter() - t0
t0 = time.perf_counter()
for name in find_keys:
pb.bst_find(root, name)
t_find = time.perf_counter() - t0
t0 = time.perf_counter()
for name in delete_keys:
root = pb.bst_delete(root, name)
t_delete = time.perf_counter() - t0
return t_insert, t_find, t_delete
# ---------- запуск ----------
def run_one(structure_name, mode, n, rng_seed):
"""Готовит данные, прогоняет TRIALS раз и возвращает список (insert, find, delete)."""
base_records = gen_records(n)
runs = []
for trial in range(TRIALS):
# отдельный rng для воспроизводимости и независимости попыток
rng = random.Random(rng_seed + trial)
if mode == "shuffled":
records = base_records[:]
rng.shuffle(records)
elif mode == "sorted":
records = sorted(base_records, key=lambda x: x[0])
else:
raise ValueError(mode)
find_keys = pick_keys(records, N_FIND_EXISTING, N_FIND_MISSING, rng)
delete_keys = [name for name, _ in rng.sample(records, N_DELETE)]
if structure_name == "LinkedList":
r = measure_linked_list(records, find_keys, delete_keys)
elif structure_name == "HashTable":
r = measure_hash_table(records, find_keys, delete_keys)
elif structure_name == "BST":
r = measure_bst(records, find_keys, delete_keys)
else:
raise ValueError(structure_name)
runs.append(r)
return runs
def main():
os.makedirs(os.path.dirname(OUT_CSV), exist_ok=True)
rows = [["Структура", "Режим", "Операция", "N", "Trial", "Время (сек)"]]
summary = [] # (structure, mode, op, n, mean, all_trials)
configs = [
("LinkedList", "shuffled", N),
("LinkedList", "sorted", N),
("HashTable", "shuffled", N),
("HashTable", "sorted", N),
("BST", "shuffled", N),
("BST", "sorted", N_BST_SORTED), # вырожденный случай
]
for structure, mode, n in configs:
print(f"==> {structure:10s} | {mode:9s} | N={n}")
runs = run_one(structure, mode, n, RNG_SEED)
# runs = [(insert, find, delete), ...]
ops = ["insert", "find", "delete"]
for op_idx, op in enumerate(ops):
vals = [r[op_idx] for r in runs]
mean = sum(vals) / len(vals)
for trial_idx, v in enumerate(vals):
rows.append([structure, mode, op, n, trial_idx + 1, f"{v:.6f}"])
summary.append((structure, mode, op, n, mean, vals))
print(f" {op:7s}: mean={mean*1000:.3f} ms "
f"runs={[f'{v*1000:.3f}' for v in vals]}")
# сводная строка со средними
rows.append([])
rows.append(["--- СРЕДНИЕ ---"])
rows.append(["Структура", "Режим", "Операция", "N",
"Среднее (сек)", "Все замеры (сек)"])
for s, mode, op, n, mean, vals in summary:
rows.append([s, mode, op, n, f"{mean:.6f}",
";".join(f"{v:.6f}" for v in vals)])
with open(OUT_CSV, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(rows)
print(f"\nГотово. Результаты записаны в {OUT_CSV}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,267 @@
"""
phonebook.py
Три структуры данных для хранения телефонного справочника,
реализованные в процедурной парадигме (без классов).
Узлы и контейнеры представляются обычными словарями и списками.
"""
import sys
# Увеличиваем лимит рекурсии - нужно для BST в худшем случае
sys.setrecursionlimit(200_000)
# ============================================================
# 1. СВЯЗНЫЙ СПИСОК
# Узел: {'name': str, 'phone': str, 'next': dict|None}
# Голова списка - это либо узел, либо None (пустой список)
# ============================================================
def ll_create():
"""Создаёт пустой связный список."""
return None
def ll_insert(head, name, phone):
"""Вставляет или обновляет запись. Возвращает новую голову списка."""
# Если списка нет - создаём первый узел
if head is None:
return {'name': name, 'phone': phone, 'next': None}
# Если совпадение в голове - просто обновляем телефон
node = head
while node is not None:
if node['name'] == name:
node['phone'] = phone
return head
if node['next'] is None:
break
node = node['next']
# node - последний узел, добавляем после него
node['next'] = {'name': name, 'phone': phone, 'next': None}
return head
def ll_find(head, name):
"""Возвращает phone или None."""
node = head
while node is not None:
if node['name'] == name:
return node['phone']
node = node['next']
return None
def ll_delete(head, name):
"""Удаляет узел по имени. Возвращает новую голову (она могла измениться)."""
if head is None:
return None
# Удаление головы
if head['name'] == name:
return head['next']
prev = head
cur = head['next']
while cur is not None:
if cur['name'] == name:
prev['next'] = cur['next']
return head
prev = cur
cur = cur['next']
# Не нашли - игнорируем
return head
def ll_collect(head):
"""Возвращает несортированный список (name, phone) - служебная функция."""
out = []
node = head
while node is not None:
out.append((node['name'], node['phone']))
node = node['next']
return out
def ll_list_all(head):
"""Возвращает все записи, отсортированные по имени."""
items = ll_collect(head)
items.sort(key=lambda x: x[0])
return items
# ============================================================
# 2. ХЕШ-ТАБЛИЦА
# buckets - список фиксированной длины из голов связных списков
# ============================================================
def ht_create(size=1024):
"""Создаёт пустую хеш-таблицу с заданным числом бакетов."""
return {
'size': size,
'buckets': [None] * size,
}
def _ht_index(name, size):
"""Хеш-функция: встроенный hash + остаток от деления."""
return hash(name) % size
def ht_insert(ht, name, phone):
"""Вставляет или обновляет запись в нужном бакете."""
idx = _ht_index(name, ht['size'])
ht['buckets'][idx] = ll_insert(ht['buckets'][idx], name, phone)
def ht_find(ht, name):
"""Возвращает phone или None."""
idx = _ht_index(name, ht['size'])
return ll_find(ht['buckets'][idx], name)
def ht_delete(ht, name):
"""Удаляет запись (если она есть)."""
idx = _ht_index(name, ht['size'])
ht['buckets'][idx] = ll_delete(ht['buckets'][idx], name)
def ht_list_all(ht):
"""Собирает все записи из всех бакетов и сортирует по имени."""
out = []
for head in ht['buckets']:
out.extend(ll_collect(head))
out.sort(key=lambda x: x[0])
return out
# ============================================================
# 3. ДВОИЧНОЕ ДЕРЕВО ПОИСКА
# Узел: {'name': str, 'phone': str, 'left': dict|None, 'right': dict|None}
# Корень - узел или None
# ============================================================
def bst_create():
"""Создаёт пустое BST."""
return None
def bst_insert(root, name, phone):
"""Вставляет/обновляет. Итеративная реализация, чтобы не упереться в рекурсию
при отсортированном входе. Возвращает новый корень."""
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
cur = root
while True:
if name == cur['name']:
cur['phone'] = phone
return root
if name < cur['name']:
if cur['left'] is None:
cur['left'] = new_node
return root
cur = cur['left']
else:
if cur['right'] is None:
cur['right'] = new_node
return root
cur = cur['right']
def bst_find(root, name):
"""Возвращает phone или None. Итеративный поиск."""
cur = root
while cur is not None:
if name == cur['name']:
return cur['phone']
cur = cur['left'] if name < cur['name'] else cur['right']
return None
def _bst_min_node(node):
"""Возвращает узел с минимальным ключом в поддереве."""
while node['left'] is not None:
node = node['left']
return node
def bst_delete(root, name):
"""Удаление узла с возвратом нового корня. Стандартный алгоритм:
если у узла двое детей - заменяем его на минимальный из правого поддерева."""
if root is None:
return None
if name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
else:
# Нашли узел для удаления
if root['left'] is None:
return root['right']
if root['right'] is None:
return root['left']
# Двое детей: берём преемника (мин. в правом поддереве)
successor = _bst_min_node(root['right'])
root['name'] = successor['name']
root['phone'] = successor['phone']
root['right'] = bst_delete(root['right'], successor['name'])
return root
def bst_list_all(root):
"""Центрированный обход (in-order) - сразу даёт отсортированный по имени список.
Итеративный, чтобы не упасть на вырожденном дереве."""
out = []
stack = []
cur = root
while cur is not None or stack:
while cur is not None:
stack.append(cur)
cur = cur['left']
cur = stack.pop()
out.append((cur['name'], cur['phone']))
cur = cur['right']
return out
# ============================================================
# Маленький self-test, запускается только при прямом вызове
# ============================================================
if __name__ == "__main__":
data = [("Alice", "111"), ("Bob", "222"), ("Charlie", "333"),
("Dave", "444"), ("Eve", "555")]
# LinkedList
head = ll_create()
for n, p in data:
head = ll_insert(head, n, p)
assert ll_find(head, "Bob") == "222"
assert ll_find(head, "Nope") is None
head = ll_delete(head, "Bob")
assert ll_find(head, "Bob") is None
assert ll_list_all(head) == sorted([(n, p) for n, p in data if n != "Bob"])
# HashTable
ht = ht_create(size=8)
for n, p in data:
ht_insert(ht, n, p)
assert ht_find(ht, "Charlie") == "333"
ht_delete(ht, "Charlie")
assert ht_find(ht, "Charlie") is None
assert ht_list_all(ht) == sorted([(n, p) for n, p in data if n != "Charlie"])
# BST
root = bst_create()
for n, p in data:
root = bst_insert(root, n, p)
assert bst_find(root, "Dave") == "444"
root = bst_delete(root, "Dave")
assert bst_find(root, "Dave") is None
assert bst_list_all(root) == sorted([(n, p) for n, p in data if n != "Dave"])
print("phonebook.py: все самопроверки пройдены")

View File

@ -0,0 +1,118 @@
"""
plot_results.py - строит столбчатые диаграммы по итогам экспериментов.
"""
import csv
import os
import matplotlib.pyplot as plt
import numpy as np
CSV = os.path.join("docs", "data", "results.csv")
PLOTS_DIR = os.path.join("docs", "data", "plots")
os.makedirs(PLOTS_DIR, exist_ok=True)
def load_means():
"""Возвращает dict[(structure, mode, op)] = (mean, N)."""
means = {}
with open(CSV, encoding="utf-8") as f:
rows = list(csv.reader(f))
# ищем секцию "--- СРЕДНИЕ ---"
start = None
for i, row in enumerate(rows):
if row and row[0] == "--- СРЕДНИЕ ---":
start = i + 2 # пропустить заголовок секции
break
for row in rows[start:]:
if not row:
continue
structure, mode, op, n, mean, _trials = row
means[(structure, mode, op)] = (float(mean), int(n))
return means
def plot_grouped(means, op, fname, title):
"""Сгруппированные столбцы: для каждой структуры - два столбца (shuffled / sorted)."""
structures = ["LinkedList", "HashTable", "BST"]
modes = ["shuffled", "sorted"]
x = np.arange(len(structures))
width = 0.36
fig, ax = plt.subplots(figsize=(8, 5))
for i, mode in enumerate(modes):
vals_ms = []
labels = []
for s in structures:
mean, n = means[(s, mode, op)]
vals_ms.append(mean * 1000)
labels.append(f"N={n}")
bars = ax.bar(x + (i - 0.5) * width, vals_ms, width,
label=mode, alpha=0.85)
for bar, lab in zip(bars, labels):
h = bar.get_height()
ax.text(bar.get_x() + bar.get_width() / 2, h,
f"{h:.2f}\n{lab}", ha="center", va="bottom", fontsize=8)
ax.set_xticks(x)
ax.set_xticklabels(structures)
ax.set_ylabel("Время, мс (среднее по 5 запускам)")
ax.set_title(title)
ax.set_yscale("log")
ax.legend(title="порядок входных данных")
ax.grid(axis="y", linestyle="--", alpha=0.4)
plt.tight_layout()
out = os.path.join(PLOTS_DIR, fname)
plt.savefig(out, dpi=130)
plt.close()
print("saved:", out)
def plot_bst_degradation(means, fname):
"""Отдельный график: BST shuffled vs sorted - даже при меньшем N
отсортированные данные дают намного большее время."""
ops = ["insert", "find", "delete"]
shuffled = [means[("BST", "shuffled", op)][0] * 1000 for op in ops]
sorted_ = [means[("BST", "sorted", op)][0] * 1000 for op in ops]
n_shuf = means[("BST", "shuffled", "insert")][1]
n_sort = means[("BST", "sorted", "insert")][1]
x = np.arange(len(ops))
width = 0.36
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.bar(x - width/2, shuffled, width, label=f"shuffled (N={n_shuf})")
ax.bar(x + width/2, sorted_, width, label=f"sorted (N={n_sort})")
for i, v in enumerate(shuffled):
ax.text(i - width/2, v, f"{v:.2f}", ha="center", va="bottom", fontsize=9)
for i, v in enumerate(sorted_):
ax.text(i + width/2, v, f"{v:.2f}", ha="center", va="bottom", fontsize=9)
ax.set_xticks(x); ax.set_xticklabels(ops)
ax.set_ylabel("Время, мс")
ax.set_title("BST: деградация на отсортированных данных\n"
"(N для sorted в 5 раз меньше, но время больше)")
ax.set_yscale("log")
ax.legend()
ax.grid(axis="y", linestyle="--", alpha=0.4)
plt.tight_layout()
out = os.path.join(PLOTS_DIR, fname)
plt.savefig(out, dpi=130)
plt.close()
print("saved:", out)
def main():
means = load_means()
plot_grouped(means, "insert",
"insert_compare.png",
"Вставка всех записей (лог. шкала)")
plot_grouped(means, "find",
"find_compare.png",
"Поиск 110 ключей (лог. шкала)")
plot_grouped(means, "delete",
"delete_compare.png",
"Удаление 50 записей (лог. шкала)")
plot_bst_degradation(means, "bst_degradation.png")
if __name__ == "__main__":
main()

169
SobolevNS/docs/report_01.md Normal file
View 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) (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. Графики
![Вставка](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 занимают
в 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`,
индексы в БД и т. д.).