[1] лаба 1

This commit is contained in:
MashinDD 2026-05-11 16:40:10 +03:00
parent a40c5f579a
commit 15ec46afb3
7 changed files with 670 additions and 0 deletions

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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

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,1.783629,1.733554,1.709240,1.801448,1.897240,1.776666
LinkedList,случайный,find,0.023223,0.021751,0.021862,0.026800,0.022409,0.023292
LinkedList,случайный,delete,0.013033,0.012327,0.012596,0.014570,0.012699,0.012975
HashTable,случайный,insert,0.014438,0.015055,0.014623,0.015085,0.013625,0.013801
HashTable,случайный,find,0.000175,0.000195,0.000162,0.000230,0.000150,0.000141
HashTable,случайный,delete,0.000083,0.000086,0.000074,0.000115,0.000071,0.000071
BST,случайный,insert,0.014068,0.014706,0.014537,0.014229,0.014224,0.012645
BST,случайный,find,0.000117,0.000123,0.000117,0.000118,0.000115,0.000111
BST,случайный,delete,0.000093,0.000109,0.000090,0.000091,0.000089,0.000084
LinkedList,сортированный,insert,1.925730,1.993312,1.916302,1.940326,1.890758,1.887951
LinkedList,сортированный,find,0.022090,0.021523,0.024212,0.022322,0.021368,0.021026
LinkedList,сортированный,delete,0.013715,0.013660,0.014334,0.013582,0.013608,0.013391
HashTable,сортированный,insert,0.012953,0.014168,0.012098,0.013991,0.012257,0.012253
HashTable,сортированный,find,0.000129,0.000130,0.000131,0.000130,0.000124,0.000130
HashTable,сортированный,delete,0.000077,0.000076,0.000079,0.000077,0.000075,0.000077
BST,сортированный,insert,3.325809,3.408518,3.355628,3.274993,3.285617,3.304288
BST,сортированный,find,0.029482,0.028956,0.028307,0.033386,0.028663,0.028099
BST,сортированный,delete,0.037362,0.037118,0.036916,0.039044,0.035960,0.037772
1 Структура Режим Операция Среднее (с) Замер_1 Замер_2 Замер_3 Замер_4 Замер_5
2 LinkedList случайный insert 1.783629 1.733554 1.709240 1.801448 1.897240 1.776666
3 LinkedList случайный find 0.023223 0.021751 0.021862 0.026800 0.022409 0.023292
4 LinkedList случайный delete 0.013033 0.012327 0.012596 0.014570 0.012699 0.012975
5 HashTable случайный insert 0.014438 0.015055 0.014623 0.015085 0.013625 0.013801
6 HashTable случайный find 0.000175 0.000195 0.000162 0.000230 0.000150 0.000141
7 HashTable случайный delete 0.000083 0.000086 0.000074 0.000115 0.000071 0.000071
8 BST случайный insert 0.014068 0.014706 0.014537 0.014229 0.014224 0.012645
9 BST случайный find 0.000117 0.000123 0.000117 0.000118 0.000115 0.000111
10 BST случайный delete 0.000093 0.000109 0.000090 0.000091 0.000089 0.000084
11 LinkedList сортированный insert 1.925730 1.993312 1.916302 1.940326 1.890758 1.887951
12 LinkedList сортированный find 0.022090 0.021523 0.024212 0.022322 0.021368 0.021026
13 LinkedList сортированный delete 0.013715 0.013660 0.014334 0.013582 0.013608 0.013391
14 HashTable сортированный insert 0.012953 0.014168 0.012098 0.013991 0.012257 0.012253
15 HashTable сортированный find 0.000129 0.000130 0.000131 0.000130 0.000124 0.000130
16 HashTable сортированный delete 0.000077 0.000076 0.000079 0.000077 0.000075 0.000077
17 BST сортированный insert 3.325809 3.408518 3.355628 3.274993 3.285617 3.304288
18 BST сортированный find 0.029482 0.028956 0.028307 0.033386 0.028663 0.028099
19 BST сортированный delete 0.037362 0.037118 0.036916 0.039044 0.035960 0.037772

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,145 @@
# Отчёт: Задание 1 — Структуры данных
## Цель работы
Реализовать три структуры данных «с нуля» в процедурной парадигме (без классов), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций.
**Структуры данных:**
- Связный список (LinkedList)
- Хеш-таблица (HashTable)
- Двоичное дерево поиска (BST)
---
## Реализация
### Файловая структура
```
task1/
├── phone_book.py # все три структуры данных
├── benchmark.py # генерация данных + замеры
├── plot_results.py # построение графиков
└── docs/
├── report.md # этот отчёт
└── data/
├── results.csv
├── comparison_by_operation.png
└── sorted_vs_random.png
```
### Ключевые решения реализации
#### 1. Связный список
Узел — Python-словарь: `{'name': 'Имя', 'phone': '123', 'next': None}`.
Вставка добавляет **в начало** списка за O(1) (если имя не существует), а при обновлении — проходит по списку O(n). Поиск и удаление — всегда O(n), так как нет случайного доступа.
#### 2. Хеш-таблица
Массив из 256 бакетов. Каждый бакет — голова связного списка (цепочки для разрешения коллизий). Хеш-функция: стандартный `hash(name) % size`. Операции в среднем O(1), при коллизиях — O(k), где k — длина цепочки.
#### 3. Двоичное дерево поиска (BST)
Узел: `{'name': 'Имя', 'phone': '123', 'left': None, 'right': None}`. Ключ сравнения — имя лексикографически. Вставка и поиск итеративные. Удаление рекурсивное (замена минимальным узлом правого поддерева). In-order обход даёт отсортированный список.
---
## Экспериментальная часть
### Параметры эксперимента
| Параметр | Значение |
|---|---|
| Количество записей (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 раз быстрее LinkedList)
- Поиск: ~0.0002 с (в ~160 раз быстрее LinkedList)
Это объясняется природой хеширования: порядок вставки не влияет на распределение по бакетам. Ключ всегда попадает в предсказуемый бакет за O(1).
### 3. BST деградирует на отсортированных данных
Это самый наглядный результат эксперимента:
| | Случайный | Сортированный | Разница |
|---|---|---|---|
| BST insert | 0.017 с | **3.854 с** | **×225** |
| BST find | 0.000145 с | **0.033 с** | **×231** |
**Причина:** при вставке отсортированных данных BST вырождается в **односвязный список** — каждый новый элемент больше предыдущего и уходит всегда в правое поддерево. Высота дерева становится O(n) вместо O(log n). Поиск и удаление тоже деградируют до O(n).
### 4. Сравнение операции delete
При случайных данных BST удаляет за **~0.0001 с** (log n). При сортированных — **~0.046 с** (деградация до линейного). HashTable стабильна: ~0.00012 с в обоих случаях.
---
## Выводы и рекомендации
### Когда какую структуру использовать?
| Сценарий | Рекомендация |
|---|---|
| **Частый поиск** по имени | HashTable или BST (случайные данные) |
| **Данные приходят отсортированными** | HashTable (BST деградирует!) |
| **Нужен отсортированный список** | BST (in-order обход — бесплатный) |
| **Частые вставки/удаления + поиск** | HashTable |
| **Минимальная память, простота** | LinkedList (для малых N) |
| **Диапазонные запросы** (все имена AM) | BST |
### Сложности операций
| Структура | Insert | Find | Delete | List (sorted) |
|---|---|---|---|---|
| LinkedList | O(n) | O(n) | O(n) | O(n log n) |
| HashTable | O(1) avg | O(1) avg | O(1) avg | 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) |
### Главный вывод
HashTable — лучший выбор для телефонного справочника при частых вставках и поисках. BST лучше HashTable только если нужен отсортированный вывод без дополнительной сортировки — но при условии случайного порядка вставки или использования самобалансирующегося дерева (AVL, Red-Black).