2026-rff_mp/nikolaevda/task1/Zadanie1.py

650 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import random
import time
import csv
import os
import traceback
import sys
sys.setrecursionlimit(30000)
def ll_insert(head, name, phone):
"""
проходит до конца (или сразу добавляет в конец)
возвращает новую голову (если вставка в начало) или изменяет список по ссылке.
"""
new_node = {'name': name, 'phone': phone, 'next': None}
# в случае пустого списка, новый узел становится головой
if head is None:
return new_node
# в противном случае проходим в конец списка
current = head
while current['next'] is not None:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
# проверяем последний узел
if current['name'] == name:
current['phone'] = phone
else:
current['next'] = new_node
return head
def ll_find(head, name):
"""
ищет узел по имени.
возвращает телефон или None
"""
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:
new_head = head['next']
head['next'] = None
return new_head
# ищем узел для удаления
current = head
while current['next'] is not None:
if current['next']['name'] == name:
target = current['next']
current['next'] = target['next']
target['next'] = None # разрываем связь у удаляемого узла (иными словами, обнуление ссылки)
return head
current = current['next']
return head
def ll_collect(head, result_list):
"""собирает все данные из связного списка в result_list"""
current = head
while current is not None:
result_list.append((current['name'], current['phone']))
current = current['next']
def ll_list_all(head):
"""
собирает все записи в список и сортирует
"""
result = []
current = head
while current is not None:
result.append((current['name'], current['phone']))
current = current['next']
# ручная сортировка пузырьком
n = len(result)
for i in range(n):
for j in range(0, n - i - 1):
if result[j][0] > result[j + 1][0]:
result[j], result[j + 1] = result[j + 1], result[j]
return result
def hash_table(size):
"""создание хеш-таблицы"""
return [None] * size
def hash_func(name, buckets_count):
"""
использует умножение на простое число для лучшего распределения
"""
h = 0
multiplier = 1
for char in name:
h = (h + ord(char) * multiplier) % buckets_count
multiplier = (multiplier * 31) % buckets_count
return h
def ht_insert(buckets, name, phone):
"""добавить или обновить запись"""
if buckets is None:
return
index = hash_func(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets, name):
"""найти телефон по имени"""
idx = hash_func(name, len(buckets))
return ll_find(buckets[idx], name)
def ht_delete(buckets, name):
"""
удалить запись
"""
idx = hash_func(name, len(buckets))
buckets[idx] = ll_delete(buckets[idx], name)
def bubble_sort(records):
"""пузырьковая сортировка"""
n = len(records)
for i in range(n - 1):
swapped = False
for j in range(n - 1 - i):
if records[j][0] > records[j + 1][0]:
records[j], records[j + 1] = records[j + 1], records[j]
swapped = True
if not swapped:
break
return records
def ht_list_all(buckets):
"""
собрание всех записей и сортировка
"""
# Собираем все записи
full_data = []
for head in buckets:
ll_collect(head, full_data)
# Сортируем пузырьком
bubble_sort(full_data)
return full_data
#Hash_table1 = hash_table(3)
#ht_insert(Hash_table1, 'Alena', '010')
#ht_insert(Hash_table1, 'Helena', '111')
#ht_insert(Hash_table1, 'Gena', '222')
#print(ht_list_all(Hash_table1))
def bst_insert(root, name, phone):
"""
рекурсивная вставка или обновление записи
возвращает новый корень (если корень меняется)
"""
# если дерево пусто, создаём новый узел
if root is None:
return {'name': name, 'phone': phone, 'left': None, 'right': None}
# вставка в левое поддерево
if name < root['name']:
root['left'] = bst_insert(root['left'], name, phone)
# вставка в правое поддерево
elif name > root['name']:
root['right'] = bst_insert(root['right'], name, phone)
# имя уже существует — обновляем телефон
else:
root['phone'] = phone
return root
def bst_find(root, name):
"""
рекурсивный поиск телефона по имени
возвращает phone или None
"""
# не нашли
if root is None:
return None
# нашли
if root['name'] == name:
return root['phone']
# ищем в левом поддереве
elif name < root['name']:
return bst_find(root['left'], name)
# ищем в правом поддереве
else:
return bst_find(root['right'], name)
def bst_find_min(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 and root['right'] is None:
return None
elif root['left'] is None:
return root['right']
elif root['right'] is None:
return root['left']
# находим минимальный элемент в правом поддереве
successor = bst_find_min(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=None):
"""
центрированный (in-order) обход дерева
рекурсивно собирает записи в отсортированном порядке
"""
if result is None:
result = []
if root is not None:
# сначала обходим левое поддерево (все меньшие ключи)
bst_list_all(root['left'], result)
# затем текущий узел
result.append((root['name'], root['phone']))
# затем правое поддерево (все большие ключи)
bst_list_all(root['right'], result)
return result
def generate_records(count=10000):
"""
генерация тестовых данных
70% уникальных имён, 30% повторяющихся (для коллизий)
"""
records = []
base_names = ["Алексей", "Борис", "Владимир", "Дмитрий", "Елена",
"Иван", "Мария", "Николай", "Ольга", "Павел"]
for i in range(count):
if random.random() < 0.7:
name = f"User_{i:05d}"
else:
name = random.choice(base_names) + f"_{random.randint(1, 100)}"
phone = f"+7-{random.randint(100,999)}-{random.randint(100,999)}-{random.randint(1000,9999)}"
records.append((name, phone))
shuffled = records.copy()
random.shuffle(shuffled)
sorted_records = sorted(records, key=lambda x: x[0])
return shuffled, sorted_records
def measure_insertion(structure_name, records):
"""
замер времени вставки
возвращает список замеров и заполненную структуру
"""
times = []
filled_structure = None
for run in range(5):
if structure_name == "linked_list":
structure = None
elif structure_name == "hash_table":
structure = hash_table(2003)
elif structure_name == "bst":
structure = None
start = time.perf_counter()
for name, phone in records:
if structure_name == "linked_list":
structure = ll_insert(structure, name, phone)
elif structure_name == "hash_table":
ht_insert(structure, name, phone)
elif structure_name == "bst":
structure = bst_insert(structure, name, phone)
end = time.perf_counter()
times.append(end - start)
if run == 4: # Сохраняем после последнего замера
filled_structure = structure
return times, filled_structure
def measure_search(structure_name, structure, search_names):
"""
замер времени поиска
возвращает список замеров
"""
times = []
for run in range(5):
start = time.perf_counter()
for name in search_names:
if structure_name == "linked_list":
ll_find(structure, name)
elif structure_name == "hash_table":
ht_find(structure, name)
elif structure_name == "bst":
bst_find(structure, name)
end = time.perf_counter()
times.append(end - start)
return times
def measure_deletion(structure_name, original_structure, delete_names):
"""
замер времени удаления
возвращает список замеров
"""
times = []
for run in range(5):
# создаём копию структуры
if structure_name == "linked_list":
all_records = ll_list_all(original_structure)
test_structure = None
for name, phone in all_records:
test_structure = ll_insert(test_structure, name, phone)
elif structure_name == "hash_table":
all_records = ht_list_all(original_structure)
test_structure = hash_table(2003)
for name, phone in all_records:
ht_insert(test_structure, name, phone)
elif structure_name == "bst":
all_records = bst_list_all(original_structure)
test_structure = None
for name, phone in all_records:
test_structure = bst_insert(test_structure, name, phone)
start = time.perf_counter()
for name in delete_names:
if structure_name == "linked_list":
test_structure = ll_delete(test_structure, name)
elif structure_name == "hash_table":
ht_delete(test_structure, name)
elif structure_name == "bst":
test_structure = bst_delete(test_structure, name)
end = time.perf_counter()
times.append(end - start)
return times
print(f"Текущая рабочая директория: {os.getcwd()}")
print(f"Путь к файлу: {os.path.abspath(__file__)}")
def run_experiment():
"""
запуск всех экспериментов и сохранение результатов
"""
current_dir = os.path.dirname(__file__)
docs_dir = os.path.dirname(current_dir)
csv_file = os.path.join(docs_dir, "experiment_results.csv")
os.makedirs(docs_dir, exist_ok=True)
print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ")
print("Телефонный справочник - 10000 записей")
print(f"\nРезультаты будут сохранены в: {csv_file}")
# генерация данных
print("\n1. Генерация тестовых данных...")
shuffled_records, sorted_records = generate_records(10000)
print(f"Сгенерировано 10000 записей")
print(f"Уникальных имён: {len(set([r[0] for r in shuffled_records]))}")
# подготовка имён для поиска и удаления
random.seed(42)
existing_names = [shuffled_records[i][0] for i in random.sample(range(10000), 100)]
nonexisting_names = [f"NotExist_{i}" for i in range(10)]
search_names = existing_names + nonexisting_names
delete_names = [shuffled_records[i][0] for i in random.sample(range(10000), 50)]
results = [["Структура", "Режим", "Операция",
"Замер1(с)", "Замер2(с)", "Замер3(с)", "Замер4(с)", "Замер5(с)",
"Среднее(с)"]]
# тестирование для каждого режима
for mode_name, records in [("случайный", shuffled_records),
("отсортированный", sorted_records)]:
print(f"\n2. Тестирование режима: {mode_name}")
for struct_name in ["linked_list", "hash_table", "bst"]:
print(f"\n {struct_name.upper()}:")
# вставка
print("Вставка 10000 записей...")
insert_times, filled_struct = measure_insertion(struct_name, records)
avg_insert = sum(insert_times) / 5
print(f"Время: {avg_insert:.4f} сек (среднее)")
# поиск
print("Поиск 110 записей (100 существующих + 10 которых нет)...")
search_times = measure_search(struct_name, filled_struct, search_names)
avg_search = sum(search_times) / 5
print(f"Время: {avg_search:.4f} сек (среднее)")
# удаление
print("Удаление 50 случайных записей...")
delete_times = measure_deletion(struct_name, filled_struct, delete_names)
avg_delete = sum(delete_times) / 5
print(f"Время: {avg_delete:.4f} сек (среднее)")
# сохраняем результаты
results.append([struct_name, mode_name, "вставка"] + insert_times + [avg_insert])
results.append([struct_name, mode_name, "поиск"] + search_times + [avg_search])
results.append([struct_name, mode_name, "удаление"] + delete_times + [avg_delete])
# сохранение CSV
print("\n3. Сохранение результатов...")
try:
with open(csv_file, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f, delimiter=';')
writer.writerows(results)
print(f"Результаты сохранены в: {csv_file}")
except Exception as e:
print(f"Ошибка сохранения: {e}")
# вывод табл.
print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ")
print(f"{'Структура':<15} {'Режим':<12} {'Операция':<10} {'Среднее время (сек)':<20}")
for row in results[1:]:
struct, mode, op, t1, t2, t3, t4, t5, avg = row
print(f"{struct:<15} {mode:<12} {op:<10} {avg:<20.6f}")
return results
def create_report_table(results):
"""Создание сводной таблицы"""
print("СВОДНАЯ ТАБЛИЦА (среднее время в секундах)")
print(f"{'Структура':<12} {'Режим':<12} {'Вставка':<12} {'Поиск':<12} {'Удаление':<12}")
summary = {}
for row in results[1:]:
struct, mode, op, _, _, _, _, _, avg = row
key = (struct, mode)
if key not in summary:
summary[key] = {}
summary[key][op] = avg
names = {'linked_list': 'LinkedList', 'hash_table': 'HashTable', 'bst': 'BST'}
for (struct, mode), ops in summary.items():
print(f"{names[struct]:<12} {mode:<12} {ops.get('вставка', 0):<12.6f} {ops.get('поиск', 0):<12.6f} {ops.get('удаление', 0):<12.6f}")
def print_analysis():
"""вывод краткого анализа"""
print("АНАЛИЗ РЕЗУЛЬТАТОВ")
print("""
1. Влияние порядка данных на BST:
- На случайных данных: быстро O(log n)
- На отсортированных: деградация до O(n) (дерево вырождается в список)
2. Хеш-таблица не чувствительна к порядку:
- Хеш-функция случайно распределяет данные по bucket'ам
- Порядок вставки не влияет на время операций
3. Связный список всегда медленен при поиске:
- Поиск требует последовательного прохода O(n)
- Нет индексов или сортировки для ускорения
4. Сравнение удаления:
- Связный список: O(n) — нужен поиск элемента
- Хеш-таблица: O(1) — прямой доступ по индексу
- BST: O(log n) в среднем, O(n) на отсортированных
5. Рекомендация для реальных задач:
- Хеш-таблица: частый поиск, словари, кэши
- BST (сбалансированный): нужны отсортированные данные
- Связный список: маленькие объёмы, очереди/стеки
- Для телефонного справочника ЛУЧШЕ: ХЕШ-ТАБЛИЦА
""")
def create_graphs(results):
"""Построение столбчатых диаграмм"""
import matplotlib.pyplot as plt
import numpy as np
data = {}
for row in results[1:]:
struct = row[0]
mode = row[1]
op = row[2]
avg = row[8]
if struct not in data:
data[struct] = {}
if mode not in data[struct]:
data[struct][mode] = {}
data[struct][mode][op] = avg
# Настройки
struct_names = {'linked_list': 'LinkedList', 'hash_table': 'HashTable', 'bst': 'BST'}
colors = {'linked_list': '#3498db', 'hash_table': '#2ecc71', 'bst': '#e74c3c'}
modes = ['случайный', 'отсортированный']
operations = ['вставка', 'поиск', 'удаление']
op_titles = ['Вставка (10000 записей)', 'Поиск (110 запросов)', 'Удаление (50 записей)']
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
fig.suptitle('Сравнение производительности структур данных', fontsize=14, fontweight='bold')
for idx, (op, title) in enumerate(zip(operations, op_titles)):
ax = axes[idx]
x = np.arange(len(modes))
width = 0.25
multiplier = 0
for struct in ['linked_list', 'hash_table', 'bst']:
values = [data[struct][mode][op] for mode in modes]
bars = ax.bar(x + multiplier * width, values, width,
label=struct_names[struct], color=colors[struct])
for bar, val in zip(bars, values):
if val < 0.001:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
f'{val:.6f}', ha='center', va='bottom', fontsize=7)
elif val < 0.01:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
f'{val:.5f}', ha='center', va='bottom', fontsize=7)
else:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
f'{val:.3f}', ha='center', va='bottom', fontsize=8)
multiplier += 1
ax.set_title(title)
ax.set_ylabel('Время (сек)')
ax.set_yscale('log')
ax.set_ylim(1e-5, 10)
ax.set_xticks(x + width)
ax.set_xticklabels(['Случайный', 'Отсортированный'])
ax.legend()
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
current_dir = os.path.dirname(__file__)
docs_dir = os.path.dirname(current_dir)
path = os.path.join(docs_dir, 'graphs.png')
plt.savefig(path, dpi=150)
plt.close()
print(f"\nГрафики сохранены: {path}")
return path
if __name__ == "__main__":
results = run_experiment()
create_report_table(results)
create_graphs(results)
print_analysis()
print("ЭКСПЕРИМЕНТ ВЫПОЛНЕН ПОЛНОСТЬЮ!")