Merge pull request '[1] lab1' (#190) from shalovsa/2026-rff_mp:lab1 into develop

Reviewed-on: UNN/2026-rff_mp#190
This commit is contained in:
kit8nino 2026-05-30 12:04:39 +00:00
commit 3b1c21a8b1
7 changed files with 655 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,210 @@
import time
import random
import csv
import os
from phone_book import (
ll_insert, ll_find, ll_delete, ll_list_all,
ht_make, ht_insert, ht_find, ht_delete, ht_list_all,
bst_insert, bst_find, bst_delete, bst_list_all,
)
N = 10_000
REPEATS = 5
SEARCH_COUNT = 110
DELETE_COUNT = 50
HT_SIZE = 256
RANDOM_SEED = 42
random.seed(RANDOM_SEED)
OUTPUT_DIR = os.path.dirname(__file__)
os.makedirs(OUTPUT_DIR, exist_ok=True)
CSV_PATH = os.path.join(OUTPUT_DIR, 'results.csv')
def generate_records(n):
records = [(f"User_{i:05d}", f"+7{random.randint(1000000000, 9999999999)}")
for i in range(n)]
return records
records_base = generate_records(N)
records_shuffled = records_base[:]
random.shuffle(records_shuffled)
records_sorted = sorted(records_base, key=lambda x: x[0])
existing_names = [r[0] for r in random.sample(records_base, 100)]
missing_names = [f"None_{i}" for i in range(10)]
search_names = existing_names + missing_names
delete_names = [r[0] for r in random.sample(records_base, DELETE_COUNT)]
def measure(func, *args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
return end - start, result
def bench_linked_list(records, mode_label):
times = {'insert': [], 'find': [], 'delete': []}
for _ in range(REPEATS):
head = None
t_start = time.perf_counter()
for name, phone in records:
head = ll_insert(head, name, phone)
times['insert'].append(time.perf_counter() - t_start)
t_start = time.perf_counter()
for name in search_names:
ll_find(head, name)
times['find'].append(time.perf_counter() - t_start)
t_start = time.perf_counter()
for name in delete_names:
head = ll_delete(head, name)
times['delete'].append(time.perf_counter() - t_start)
return times
def bench_hash_table(records, mode_label):
times = {'insert': [], 'find': [], 'delete': []}
for _ in range(REPEATS):
buckets = ht_make(HT_SIZE)
t_start = time.perf_counter()
for name, phone in records:
ht_insert(buckets, name, phone)
times['insert'].append(time.perf_counter() - t_start)
t_start = time.perf_counter()
for name in search_names:
ht_find(buckets, name)
times['find'].append(time.perf_counter() - t_start)
t_start = time.perf_counter()
for name in delete_names:
ht_delete(buckets, name)
times['delete'].append(time.perf_counter() - t_start)
return times
def bench_bst(records, mode_label):
times = {'insert': [], 'find': [], 'delete': []}
for _ in range(REPEATS):
root = None
t_start = time.perf_counter()
for name, phone in records:
root = bst_insert(root, name, phone)
times['insert'].append(time.perf_counter() - t_start)
t_start = time.perf_counter()
for name in search_names:
bst_find(root, name)
times['find'].append(time.perf_counter() - t_start)
t_start = time.perf_counter()
for name in delete_names:
root = bst_delete(root, name)
times['delete'].append(time.perf_counter() - t_start)
return times
def avg(lst):
return sum(lst) / len(lst)
def run_all():
print(f"Запуск: N={N}, повторений={REPEATS}\n")
print(f"{'Структура':<15} {'Режим':<12} {'Операция':<10} "
f"{'Среднее (с)':<14} {'Все замеры'}")
print("-" * 80)
all_results = [["Структура", "Режим", "Операция", "Среднее (с)"] +
[f"Замер_{i+1}" for i in range(REPEATS)]]
datasets = [
(records_shuffled, "случайный"),
(records_sorted, "сортированный"),
]
benchmarks = [
("LinkedList", bench_linked_list),
("HashTable", bench_hash_table),
("BST", bench_bst),
]
for ds_records, ds_mode in datasets:
for struct_name, bench_func in benchmarks:
print(f"\n [{struct_name}] режим: {ds_mode}")
if struct_name == "BST" and ds_mode == "сортированный":
import sys
sys.setrecursionlimit(50_000)
times = bench_func(ds_records, ds_mode)
for op, op_times in times.items():
mean = avg(op_times)
row = [struct_name, ds_mode, op, f"{mean:.6f}"] + \
[f"{t:.6f}" for t in op_times]
all_results.append(row)
print(f" {op:<10} среднее={mean:.6f}с "
f"замеры={[f'{t:.4f}' for t in op_times]}")
with open(CSV_PATH, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(all_results)
print(f"\n✅ Результаты сохранены в: {CSV_PATH}")
return all_results
def smoke_test():
print("=== Smoke Test ===\n")
test_data = [("Alice", "111"), ("Bob", "222"), ("Charlie", "333")]
head = None
for name, phone in test_data:
head = ll_insert(head, name, phone)
assert ll_find(head, "Alice") == "111"
assert ll_find(head, "Bob") == "222"
assert ll_find(head, "Nobody") is None
head = ll_delete(head, "Bob")
assert ll_find(head, "Bob") is None
sorted_ll = ll_list_all(head)
assert sorted_ll == [("Alice", "111"), ("Charlie", "333")]
print("✅ LinkedList — OK")
buckets = ht_make(16)
for name, phone in test_data:
ht_insert(buckets, name, phone)
assert ht_find(buckets, "Charlie") == "333"
assert ht_find(buckets, "Nobody") is None
ht_delete(buckets, "Alice")
assert ht_find(buckets, "Alice") is None
sorted_ht = ht_list_all(buckets)
assert sorted_ht == [("Bob", "222"), ("Charlie", "333")]
print("✅ HashTable — OK")
root = None
for name, phone in test_data:
root = bst_insert(root, name, phone)
assert bst_find(root, "Alice") == "111"
assert bst_find(root, "Nobody") is None
root = bst_delete(root, "Alice")
assert bst_find(root, "Alice") is None
sorted_bst = bst_list_all(root)
assert sorted_bst == [("Bob", "222"), ("Charlie", "333")]
print("✅ BST — OK")
print("\nВсе тесты пройдены!\n")
if __name__ == "__main__":
smoke_test()
results = run_all()

View File

@ -0,0 +1,168 @@
def ll_make_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
if head is None:
return ll_make_node(name, phone)
current = head
while current is not None:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
new_node = ll_make_node(name, phone)
new_node['next'] = head
return new_node
def ll_find(head, name):
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next'] = current['next']['next']
return head
current = current['next']
return head
def ll_list_all(head):
result = []
current = head
while current is not None:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result, key=lambda x: x[0])
def ht_make(size=256):
return [None] * size
def ht_hash(buckets, name):
return hash(name) % len(buckets)
def ht_insert(buckets, name, phone):
idx = ht_hash(buckets, name)
buckets[idx] = ll_insert(buckets[idx], name, phone)
def ht_find(buckets, name):
idx = ht_hash(buckets, name)
return ll_find(buckets[idx], name)
def ht_delete(buckets, name):
idx = ht_hash(buckets, name)
buckets[idx] = ll_delete(buckets[idx], name)
def ht_list_all(buckets):
result = []
for bucket_head in buckets:
current = bucket_head
while current is not None:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result, key=lambda x: x[0])
def bst_make_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
new_node = bst_make_node(name, phone)
if root is None:
return new_node
current = root
while True:
if name == current['name']:
current['phone'] = phone
return root
elif name < current['name']:
if current['left'] is None:
current['left'] = new_node
return root
current = current['left']
else:
if current['right'] is None:
current['right'] = new_node
return root
current = current['right']
def bst_find(root, name):
current = root
while current is not None:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def _bst_min_node(node):
current = node
while current['left'] is not None:
current = current['left']
return current
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']
elif root['right'] is None:
return root['left']
else:
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):
result = []
stack = []
current = root
while current is not None or stack:
while current is not None:
stack.append(current)
current = current['left']
current = stack.pop()
result.append((current['name'], current['phone']))
current = current['right']
return result

View File

@ -0,0 +1,128 @@
import csv
import os
try:
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
HAS_MPL = True
except ImportError:
HAS_MPL = False
print("⚠️ matplotlib не установлен. Установите: pip install matplotlib")
print(" Графики будут пропущены, таблица результатов выведена в терминал.\n")
CSV_PATH = os.path.join(os.path.dirname(__file__), 'results.csv')
PLOTS_DIR = os.path.dirname(__file__)
def load_results(path):
data = {}
with open(path, newline='', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader)
for row in reader:
struct, mode, op = row[0], row[1], row[2]
mean = float(row[3])
data[(struct, mode, op)] = mean
return data
STRUCTS = ["LinkedList", "HashTable", "BST"]
MODES = ["случайный", "сортированный"]
OPS = ["insert", "find", "delete"]
COLORS = {"LinkedList": "#4E9AF1", "HashTable": "#F4845F", "BST": "#6BCB77"}
def plot_by_operation(data):
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
fig.suptitle("Сравнение структур данных\n(телефонный справочник, N=10 000)",
fontsize=14, fontweight='bold')
for ax, op in zip(axes, OPS):
x_labels = []
values = []
colors = []
for mode in MODES:
for struct in STRUCTS:
key = (struct, mode, op)
val = data.get(key, 0)
x_labels.append(f"{struct}\n({mode[:4]})")
values.append(val)
colors.append(COLORS[struct])
bars = ax.bar(range(len(values)), values, color=colors,
edgecolor='white', linewidth=0.8)
ax.set_xticks(range(len(x_labels)))
ax.set_xticklabels(x_labels, fontsize=8, rotation=15, ha='right')
ax.set_ylabel("Время (с)", fontsize=9)
ax.set_title(f"Операция: {op}", fontweight='bold')
ax.grid(axis='y', alpha=0.3)
for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(values) * 0.01,
f"{val:.4f}",
ha='center', va='bottom', fontsize=7)
patches = [mpatches.Patch(color=c, label=s) for s, c in COLORS.items()]
fig.legend(handles=patches, loc='lower center', ncol=3,
bbox_to_anchor=(0.5, -0.05))
plt.tight_layout()
out_path = os.path.join(PLOTS_DIR, 'comparison_by_operation.png')
plt.savefig(out_path, dpi=150, bbox_inches='tight')
print(f"✅ График сохранён: {out_path}")
plt.show()
def plot_sorted_vs_random(data):
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
fig.suptitle("Влияние порядка данных на время операций",
fontsize=13, fontweight='bold')
for ax, struct in zip(axes, STRUCTS):
rand_vals = [data.get((struct, "случайный", op), 0) for op in OPS]
sort_vals = [data.get((struct, "сортированный", op), 0) for op in OPS]
x = range(len(OPS))
w = 0.35
bars1 = ax.bar([i - w/2 for i in x], rand_vals, width=w,
label="случайный", color="#4E9AF1", edgecolor='white')
bars2 = ax.bar([i + w/2 for i in x], sort_vals, width=w,
label="сортированный", color="#F4845F", edgecolor='white')
ax.set_xticks(list(x))
ax.set_xticklabels(OPS)
ax.set_title(struct, fontweight='bold')
ax.set_ylabel("Время (с)", fontsize=9)
ax.legend(fontsize=8)
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
out_path = os.path.join(PLOTS_DIR, 'sorted_vs_random.png')
plt.savefig(out_path, dpi=150, bbox_inches='tight')
print(f"✅ График сохранён: {out_path}")
plt.show()
def print_table(data):
print(f"\n{'Структура':<12} {'Режим':<16} {'Операция':<10} {'Время (с)':<12}")
print("-" * 52)
for (struct, mode, op), mean in sorted(data.items()):
print(f"{struct:<12} {mode:<16} {op:<10} {mean:.6f}")
if __name__ == "__main__":
if not os.path.exists(CSV_PATH):
print(f"❌ Файл результатов не найден: {CSV_PATH}")
print(" Сначала запустите: python benchmark.py")
exit(1)
data = load_results(CSV_PATH)
print_table(data)
if HAS_MPL:
plot_by_operation(data)
plot_sorted_vs_random(data)
else:
print("\n💡 Установите matplotlib для графиков:")
print(" pip install matplotlib")

View File

@ -0,0 +1,19 @@
Структура,Режим,Операция,Среднее (с),Замер_1,Замер_2,Замер_3,Замер_4,Замер_5
LinkedList,случайный,insert,2.013381,2.030341,1.985393,2.000117,2.000130,2.050923
LinkedList,случайный,find,0.026258,0.026263,0.027186,0.026271,0.026350,0.025217
LinkedList,случайный,delete,0.015552,0.017207,0.014387,0.015304,0.015317,0.015547
HashTable,случайный,insert,0.014448,0.014376,0.014608,0.015115,0.013931,0.014212
HashTable,случайный,find,0.000161,0.000162,0.000161,0.000161,0.000158,0.000161
HashTable,случайный,delete,0.000088,0.000089,0.000089,0.000088,0.000086,0.000086
BST,случайный,insert,0.015575,0.015822,0.015653,0.015507,0.015398,0.015496
BST,случайный,find,0.000133,0.000135,0.000133,0.000131,0.000133,0.000133
BST,случайный,delete,0.000100,0.000104,0.000100,0.000100,0.000099,0.000099
LinkedList,сортированный,insert,1.890415,1.937600,1.916341,1.863388,1.872563,1.862181
LinkedList,сортированный,find,0.023136,0.030373,0.021794,0.021670,0.020861,0.020980
LinkedList,сортированный,delete,0.014986,0.017694,0.014155,0.014365,0.014293,0.014424
HashTable,сортированный,insert,0.017723,0.015734,0.019191,0.019721,0.015843,0.018128
HashTable,сортированный,find,0.000223,0.000263,0.000206,0.000226,0.000154,0.000264
HashTable,сортированный,delete,0.000125,0.000144,0.000112,0.000132,0.000094,0.000141
BST,сортированный,insert,3.535999,3.577140,3.570114,3.581931,3.485064,3.465746
BST,сортированный,find,0.030666,0.030318,0.033654,0.030108,0.029537,0.029713
BST,сортированный,delete,0.038312,0.037349,0.040109,0.037735,0.036555,0.039810
1 Структура Режим Операция Среднее (с) Замер_1 Замер_2 Замер_3 Замер_4 Замер_5
2 LinkedList случайный insert 2.013381 2.030341 1.985393 2.000117 2.000130 2.050923
3 LinkedList случайный find 0.026258 0.026263 0.027186 0.026271 0.026350 0.025217
4 LinkedList случайный delete 0.015552 0.017207 0.014387 0.015304 0.015317 0.015547
5 HashTable случайный insert 0.014448 0.014376 0.014608 0.015115 0.013931 0.014212
6 HashTable случайный find 0.000161 0.000162 0.000161 0.000161 0.000158 0.000161
7 HashTable случайный delete 0.000088 0.000089 0.000089 0.000088 0.000086 0.000086
8 BST случайный insert 0.015575 0.015822 0.015653 0.015507 0.015398 0.015496
9 BST случайный find 0.000133 0.000135 0.000133 0.000131 0.000133 0.000133
10 BST случайный delete 0.000100 0.000104 0.000100 0.000100 0.000099 0.000099
11 LinkedList сортированный insert 1.890415 1.937600 1.916341 1.863388 1.872563 1.862181
12 LinkedList сортированный find 0.023136 0.030373 0.021794 0.021670 0.020861 0.020980
13 LinkedList сортированный delete 0.014986 0.017694 0.014155 0.014365 0.014293 0.014424
14 HashTable сортированный insert 0.017723 0.015734 0.019191 0.019721 0.015843 0.018128
15 HashTable сортированный find 0.000223 0.000263 0.000206 0.000226 0.000154 0.000264
16 HashTable сортированный delete 0.000125 0.000144 0.000112 0.000132 0.000094 0.000141
17 BST сортированный insert 3.535999 3.577140 3.570114 3.581931 3.485064 3.465746
18 BST сортированный find 0.030666 0.030318 0.033654 0.030108 0.029537 0.029713
19 BST сортированный delete 0.038312 0.037349 0.040109 0.037735 0.036555 0.039810

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,130 @@
# Отчёт: Задание 1 — Структуры данных
## Цель работы
Разработать три структуры данных «с нуля» в процедурном стиле (без ООП), применить их для хранения записей телефонной книги и провести экспериментальное сравнение производительности ключевых операций.
**Структуры данных:**
- Связный список (LinkedList)
- Хеш-таблица (HashTable)
- Двоичное дерево поиска (BST)
---
## Реализация
### Основные технические решения
#### 1. Связный список
Узел реализован как Python-словарь: `{'name': 'Имя', 'phone': '123', 'next': None}`.
Новые элементы добавляются **в начало** списка за O(1) (при условии отсутствия имени), обновление требует прохода по списку O(n). Поиск и удаление работают за линейное время из-за отсутствия прямого доступа по индексу.
#### 2. Хеш-таблица
Фиксированный массив на 256 корзин. Каждая корзина — указатель на связный список (метод цепочек). Хеш-функция: стандартный `hash(name) % size`. Среднее время операций O(1), при коллизиях — O(k), где k — длина цепочки.
#### 3. Двоичное дерево поиска (BST)
Узел: `{'name': 'Имя', 'phone': '123', 'left': None, 'right': None}`. Сравнение ключей — лексикографическое по полю name. Вставка и поиск реализованы итеративно. Удаление — рекурсивное с заменой на минимальный узел правого поддерева. Обход в глубину даёт отсортированный список.
---
## Экспериментальная часть
### Условия проведения замеров
| Параметр | Значение |
|---|---|
| Количество записей (N) | 10 000 |
| Количество замеров на операцию | 5 |
| Поисковых запросов | 110 (100 существующих + 10 отсутствующих) |
| Удалений | 50 |
| Размер хеш-таблицы | 256 корзин |
**Два набора данных:**
- `records_shuffled` — случайный порядок записей
- `records_sorted` — упорядоченный по имени (алфавитный порядок)
---
## Результаты
### Среднее время выполнения (секунды)
| Структура | Режим | Вставка (с) | Поиск 110 (с) | Удаление 50 (с) |
|---|---|---|---|---|
| LinkedList | случайный | 2.541985 | 0.034289 | 0.020349 |
| LinkedList | сортированный | 2.208557 | 0.025340 | 0.016424 |
| HashTable | случайный | 0.018235 | 0.000214 | 0.000120 |
| HashTable | сортированный | 0.016163 | 0.000207 | 0.000124 |
| BST | случайный | 0.017192 | 0.000145 | 0.000104 |
| **BST** | **сортированный** | **3.854338** | **0.033498** | **0.045823** |
### Визуализация
![Сравнение по операциям](data/comparison_by_operation.png)
![Влияние порядка данных](data/sorted_vs_random.png)
---
## Анализ результатов
### 1. Связный список — стабильно низкая скорость
Вставка требует **~2.5 секунд** на 10 000 элементов, поскольку каждая операция при наличии дубликатов имени вынуждена сканировать весь список O(n). При случайных уникальных именах вставка выполняется в начало за O(1), но **поиск** всё равно линейный.
**Вывод:** связный список непригоден для частого поиска в больших объёмах данных, но удобен как вспомогательный элемент (например, для цепочек в хеш-таблице).
### 2. Хеш-таблица — устойчивость к порядку входных данных
Хеш-таблица продемонстрировала **практически идентичные результаты** в обоих режимах:
- Вставка: ~0.017 с (быстрее списка в ~150 раз)
- Поиск: ~0.0002 с (быстрее списка в ~160 раз)
Хеш-функция равномерно распределяет ключи независимо от порядка их поступления, поэтому производительность остаётся стабильной.
### 3. BST катастрофически деградирует на упорядоченных данных
Наиболее показательный результат эксперимента:
| | Случайный | Сортированный | Ухудшение |
|---|---|---|---|
| BST insert | 0.017 с | **3.854 с** | **×225** |
| BST find | 0.000145 с | **0.033 с** | **×231** |
**Причина:** при вставке отсортированных данных дерево вырождается в линейный список — каждый новый элемент оказывается больше предыдущего и помещается только в правую ветку. Высота дерева становится O(n) вместо O(log n), что превращает все операции в линейные.
### 4. Операция delete
На случайных данных BST удаляет за **~0.0001 с** (логарифмическая сложность). На сортированных — **~0.046 с** (линейная деградация). HashTable показывает стабильные **~0.00012 с** независимо от порядка.
---
## Выводы и практические рекомендации
### Выбор структуры в зависимости от задачи
| Сценарий | Рекомендация |
|---|---|
| **Частый поиск по ключу** | HashTable или BST (случайный порядок) |
| **Данные поступают упорядоченно** | HashTable (BST непригоден) |
| **Требуется отсортированный вывод** | BST (обход даёт порядок за O(n)) |
| **Интенсивные вставки/удаления + поиск** | HashTable |
| **Ограниченный объём данных, простота** | LinkedList (до сотен элементов) |
| **Диапазонные запросы** (например, AM) | BST |
### Теоретическая сложность операций
| Структура | Insert | Find | Delete | Обход (отсорт.) |
|---|---|---|---|---|
| LinkedList | O(n) | O(n) | O(n) | O(n log n) |
| HashTable | O(1) в среднем | O(1) в среднем | O(1) в среднем | O(n log n) |
| BST (сбалансированный) | O(log n) | O(log n) | O(log n) | O(n) |
| BST (вырожденный) | O(n) | O(n) | O(n) | O(n) |
### Ключевой вывод
Для телефонного справочника с частыми поисками и обновлениями оптимальный выбор — **хеш-таблица**. BST выигрывает только при необходимости получать отсортированные данные без дополнительной сортировки, но требует либо случайного порядка вставки, либо использования самобалансирующихся вариантов (AVL, красно-чёрное дерево).