Merge pull request '[1] data structures' (#259) from SobolevNS/2026-rff_mp:01 into develop
Reviewed-on: #259
This commit is contained in:
commit
fabb5cd5c1
22
SobolevNS/docs/data/task1_data_structures/README.md
Normal file
22
SobolevNS/docs/data/task1_data_structures/README.md
Normal 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 |
112
SobolevNS/docs/data/task1_data_structures/docs/data/results.csv
Normal file
112
SobolevNS/docs/data/task1_data_structures/docs/data/results.csv
Normal 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.
|
194
SobolevNS/docs/data/task1_data_structures/experiment.py
Normal file
194
SobolevNS/docs/data/task1_data_structures/experiment.py
Normal 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()
|
||||
267
SobolevNS/docs/data/task1_data_structures/phonebook.py
Normal file
267
SobolevNS/docs/data/task1_data_structures/phonebook.py
Normal 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: все самопроверки пройдены")
|
||||
118
SobolevNS/docs/data/task1_data_structures/plot_results.py
Normal file
118
SobolevNS/docs/data/task1_data_structures/plot_results.py
Normal 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
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