Merge pull request '[1] 1-st-exersize' (#245) from SavelevMI/2026-rff_mp:1-st-exersize into develop

Reviewed-on: #245
This commit is contained in:
VladimirGub 2026-05-30 11:37:53 +00:00
commit e47b0a93c7
10 changed files with 542 additions and 0 deletions

View File

@ -0,0 +1,18 @@
# Генерация тестовых наборов данных
import random
def generate_records(n, seed=42):
random.seed(seed)
records = []
for i in range(1, n + 1):
name = f"User_{i:05d}"
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
records.append((name, phone))
return records
def prepare_datasets(base_records):
shuffled = base_records.copy()
random.shuffle(shuffled)
sorted_records = sorted(base_records, key=lambda x: x[0])
return shuffled, sorted_records

View File

@ -0,0 +1,66 @@
# Двоичное дерево поиска (не сбалансированное)
def create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return create_node(name, phone)
if name == root['name']:
root['phone'] = phone
elif name < root['name']:
root['left'] = bst_insert(root['left'], name, phone)
else:
root['right'] = bst_insert(root['right'], name, phone)
return root
def bst_find(root, name):
if root is None:
return None
if name == root['name']:
return root['phone']
elif name < root['name']:
return bst_find(root['left'], name)
else:
return bst_find(root['right'], name)
def _find_min(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']
# Узел имеет двух потомков: заменяем наименьшим из правого поддерева
min_node = _find_min(root['right'])
root['name'] = min_node['name']
root['phone'] = min_node['phone']
root['right'] = bst_delete(root['right'], min_node['name'])
return root
def bst_list_all(root):
result = []
def inorder(node):
if node is None:
return
inorder(node['left'])
result.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return result

View File

@ -0,0 +1,18 @@
# Генерация тестовых наборов данных
import random
def generate_records(n, seed=42):
random.seed(seed)
records = []
for i in range(1, n + 1):
name = f"User_{i:05d}"
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
records.append((name, phone))
return records
def prepare_datasets(base_records):
shuffled = base_records.copy()
random.shuffle(shuffled)
sorted_records = sorted(base_records, key=lambda x: x[0])
return shuffled, sorted_records

View File

@ -0,0 +1,38 @@
# Хеш-таблица на основе списка корзин, каждая корзина связный список
import linked_list as ll
def create_hash_table(size=10):
return [None] * size
def _hash_function(key, size):
return hash(key) % size
def ht_insert(buckets, name, phone):
idx = _hash_function(name, len(buckets))
head = buckets[idx]
new_head = ll.ll_insert(head, name, phone)
buckets[idx] = new_head
return buckets
def ht_find(buckets, name):
idx = _hash_function(name, len(buckets))
head = buckets[idx]
return ll.ll_find(head, name)
def ht_delete(buckets, name):
idx = _hash_function(name, len(buckets))
head = buckets[idx]
new_head = ll.ll_delete(head, name)
buckets[idx] = new_head
return buckets
def ht_list_all(buckets):
all_records = []
for head in buckets:
current = head
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records

View File

@ -0,0 +1,58 @@
def create_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
current = head
# Поиск существующей записи
while current is not None:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
# Создание нового узла
new_node = create_node(name, phone)
if head is None:
return new_node
current = head
while current['next'] is not None:
current = current['next']
current['next'] = new_node
return head
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']
prev = head
current = head['next']
while current is not None:
if current['name'] == name:
prev['next'] = current['next']
return head
prev = current
current = current['next']
return head
def ll_list_all(head):
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda pair: pair[0])
return records

View File

@ -0,0 +1,73 @@
# Запуск экспериментального сравнения трёх структур данных
# Результаты сохраняются в experiment_results.csv
import csv
import sys
sys.setrecursionlimit(20000)
import linked_list as ll
import hash_table as ht
import bst
import data_utils
import benchmark
def main():
N = 10000 # количество записей
base_records = data_utils.generate_records(N)
shuffled, sorted_records = data_utils.prepare_datasets(base_records)
# Описания структур для бенчмарка
structures = {
'LinkedList': {
'name': 'LinkedList',
'create': lambda: None,
'insert': ll.ll_insert,
'find': ll.ll_find,
'delete': ll.ll_delete
},
'HashTable': {
'name': 'HashTable',
'create': lambda: ht.create_hash_table(10), # 10 корзин
'insert': ht.ht_insert,
'find': ht.ht_find,
'delete': ht.ht_delete
},
'BST': {
'name': 'BST',
'create': lambda: None,
'insert': bst.bst_insert,
'find': bst.bst_find,
'delete': bst.bst_delete
}
}
all_results = []
REPEATS = 5 # минимум 5 повторений
for name, struct in structures.items():
print(f"Testing {name} on random order...")
res_random = benchmark.measure_operations(struct, shuffled, 'random', REPEATS)
all_results.extend(res_random)
print(f"Testing {name} on sorted order...")
res_sorted = benchmark.measure_operations(struct, sorted_records, 'sorted', REPEATS)
all_results.extend(res_sorted)
# Сохранение CSV
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
for row in all_results:
writer.writerow([
row['structure'],
row['mode'],
row['repetition'],
f"{row['insert_time']:.6f}",
f"{row['find_time']:.6f}",
f"{row['delete_time']:.6f}"
])
print("Experiment finished. Results saved to experiment_results.csv")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,43 @@
# Загружает CSV с результатами и строит столбчатую диаграмму сравнения
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
def main():
df = pd.read_csv('experiment_results.csv')
mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index()
structures = mean_times['Structure'].unique()
operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)']
titles = ['Insertion', 'Search', 'Deletion']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for ax, op, title in zip(axes, operations, titles):
x = np.arange(len(structures))
width = 0.35
random_vals = []
sorted_vals = []
for s in structures:
rand = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')]
sort = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')]
random_vals.append(rand[op].values[0] if not rand.empty else 0)
sorted_vals.append(sort[op].values[0] if not sort.empty else 0)
ax.bar(x - width/2, random_vals, width, label='Random order')
ax.bar(x + width/2, sorted_vals, width, label='Sorted order')
ax.set_xticks(x)
ax.set_xticklabels(structures)
ax.set_ylabel('Time (seconds)')
ax.set_title(title)
ax.legend()
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150)
plt.show()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,146 @@
# Модель лабиринта: клетки, карта и загрузка из файла (Builder pattern)
class Cell:
def __init__(self, x, y):
self._x = x
self._y = y
self._is_wall = False
self._is_start = False
self._is_exit = False
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@property
def is_wall(self):
return self._is_wall
@is_wall.setter
def is_wall(self, value):
self._is_wall = value
@property
def is_start(self):
return self._is_start
@is_start.setter
def is_start(self, value):
self._is_start = value
@property
def is_exit(self):
return self._is_exit
@is_exit.setter
def is_exit(self, value):
self._is_exit = value
def is_passable(self):
return not self._is_wall
class Maze:
def __init__(self, width, height):
self._width = width
self._height = height
self._cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
self._start = None
self._exit = None
@property
def width(self):
return self._width
@property
def height(self):
return self._height
@property
def start(self):
return self._start
@property
def exit(self):
return self._exit
def get_cell(self, x, y):
if 0 <= x < self._width and 0 <= y < self._height:
return self._cells[y][x]
return None
def set_cell(self, x, y, cell_type):
cell = self.get_cell(x, y)
if cell is None:
return
if cell_type == 'wall':
cell.is_wall = True
elif cell_type == 'start':
if self._start:
self._start.is_start = False
cell.is_start = True
cell.is_wall = False
self._start = cell
elif cell_type == 'exit':
if self._exit:
self._exit.is_exit = False
cell.is_exit = True
cell.is_wall = False
self._exit = cell
elif cell_type == 'path':
cell.is_wall = False
def get_neighbors(self, cell):
neighbors = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # up, down, left, right
for dx, dy in directions:
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.get_cell(nx, ny)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder:
def build_from_file(self, filename):
raise NotImplementedError("Must be implemented in subclass")
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename):
with open(filename, 'r') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines) if height > 0 else 0
start_count = 0
exit_count = 0
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == "#":
maze.set_cell(x, y, "wall")
elif ch == "S":
maze.set_cell(x, y, "start")
start_count += 1
elif ch == "E":
maze.set_cell(x, y, "exit")
exit_count += 1
elif ch == " ":
maze.set_cell(x, y, "path")
if start_count != 1 or exit_count != 1:
raise ValueError(f"Лабиринт должен иметь ровно один вход S и один выход E. Найдено: S={start_count}, E={exit_count}")
return maze

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,82 @@
# Отчёт по лабораторной работе «Структуры данных»
## Цель работы
Реализовать три структуры данных «с нуля» (связный список, хеш‑таблицу, двоичное дерево поиска) для хранения записей телефонного справочника. Экспериментально сравнить производительность операций вставки, поиска и удаления на наборе из 10000 записей при случайном и отсортированном порядке поступления данных.
## Реализованные структуры
Все структуры написаны в процедурном стиле без использования классов.
1. **Связный список** узлы в виде словарей `{'name': str, 'phone': str, 'next': None}`.
2. **Хеш‑таблица** массив из 10 корзин, каждая корзина связный список. Хеш‑функция `hash(name) % size`.
3. **Двоичное дерево поиска** узлы `{'name': str, 'phone': str, 'left': None, 'right': None}`. Операции реализованы рекурсивно.
## Методика эксперимента
- **Генерация данных**: 10000 записей с именами `User_00001``User_10000`. Телефоны случайные строки вида `XXX-XXXX`.
- **Два режима подачи данных**:
*Случайный* записи перемешаны.
*Отсортированный* записи по возрастанию имени.
- **Измеряемые операции**:
Вставка всех 10000 записей.
Поиск 110 имён (100 существующих + 10 несуществующих).
Удаление 50 случайных существующих записей.
- **Повторы**: каждый эксперимент выполнен 5 раз, зафиксировано среднее время.
Результаты замеров сохранены в `experiment_results.csv`. Время измерялось через `time.perf_counter()`.
## Результаты измерений
### Связный список (LinkedList)
При 10000 записях связный список показал ожидаемо низкую производительность. Вставка всех элементов заняла около **4.4 секунды** в среднем, поиск около **0.027 секунды**, удаление 50 записей около **0.012 секунды**. Порядок входных данных практически не повлиял на результаты (случайный и отсортированный режимы показали близкие значения). Это объясняется тем, что связный список всегда работает за линейное время O(n) независимо от того, как приходят данные.
### Хеш‑таблица (HashTable)
Хеш‑таблица с 10 корзинами показала значительное ускорение по сравнению со связным списком. Вставка 10000 записей заняла в среднем **0.56 секунды** (почти в 8 раз быстрее списка). Поиск выполняется за **0.004 секунды** (в 7 раз быстрее), а удаление за **0.0016 секунды** (в 7.5 раз быстрее). Порядок данных практически не влияет на производительность разница между случайным и отсортированным режимами не превышает 10%, что соответствует теоретической сложности O(1) в среднем.
### Двоичное дерево поиска (BST)
Здесь наблюдается самая интересная картина:
**На случайных данных** BST показал выдающуюся производительность. Вставка всех 10000 записей заняла всего **0.025 секунды**, что в 22 раза быстрее хеш‑таблицы и в 176 раз быстрее связного списка. Поиск выполняется за **0.00024 секунды** (в 16 раз быстрее хеш‑таблицы), удаление за **0.00017 секунды** (почти в 10 раз быстрее). Это идеальный случай сбалансированного дерева.
**На отсортированных данных** ситуация кардинально меняется. Дерево вырождается в линейный список, и производительность падает катастрофически. Вставка замедлилась до **10.15 секунды** это в 406 раз медленнее, чем на случайных данных, и даже медленнее, чем у связного списка (в 2.3 раза). Поиск вырос до **0.091 секунды** (в 380 раз медленнее), удаление до **0.057 секунды** (в 335 раз медленнее). Это классический пример деградации BST при упорядоченных входных данных.
## Анализ результатов
### Как порядок входных данных влияет на скорость вставки в BST
Эксперимент наглядно демонстрирует проблему наивной реализации двоичного дерева поиска. На случайных данных дерево остаётся достаточно сбалансированным, и операции выполняются за логарифмическое время (O(log n)). Однако на отсортированных данных каждый новый элемент становится самым большим и добавляется только в правую ветку. В результате дерево превращается в односвязный список высотой 10000 узлов, а сложность всех операций деградирует до линейной O(n). Это подтверждается цифрами: время вставки выросло с 0.025 до 10.15 секунд разница в 406 раз.
### Почему хеш‑таблица почти не чувствительна к порядку
Хеш‑функция распределяет ключи по корзинам независимо от того, в каком порядке они поступают. «User_00001» и «User_10000» с равной вероятностью могут попасть в любую из 10 корзин. Поэтому порядок ввода не влияет на длину цепочек в каждой корзине. Результаты подтверждают это: в случайном и отсортированном режимах время выполнения операций отличается незначительно (менее 10%).
### Почему связный список всегда медленен при поиске
Связный список не имеет индексов или другой структуры для ускорения доступа. Чтобы найти элемент, нужно в худшем случае пройти все 10000 узлов. Поэтому поиск занимает ~0.027 секунды независимо от того, как расположены данные. Вставка тоже требует прохода до конца списка, что даёт ~4.4 секунды на 10000 элементов.
### Как удаление работает в каждой структуре
Удаление тесно связано с поиском, потому что сначала нужно найти удаляемый элемент. Поэтому время удаления коррелирует со временем поиска:
- В связном списке удаление занимает ~0.012 секунды примерно половину времени поиска (0.027 с), так как операция перелинковки дёшева.
- В хеш‑таблице удаление (~0.0016 с) близко ко времени поиска (~0.004 с), опять же с поправкой на перелинковку в списке корзины.
- В BST на случайных данных удаление (~0.00017 с) даже быстрее поиска (~0.00024 с) из-за особенностей рекурсивной реализации.
- В BST на отсортированных данных удаление (~0.057 с) занимает примерно половину времени поиска (~0.091 с) та же закономерность, что и у списка, потому что вырожденное дерево ведёт себя как список.
## Выводы
**Какую структуру и для каких задач стоит выбирать в реальной жизни?**
1. **Хеш‑таблица** оптимальный выбор для подавляющего большинства сценариев, где нужен быстрый доступ по ключу (словари, кэши, индексы в базах данных). Она стабильна, предсказуема и не зависит от порядка данных. В моём эксперименте она уступила BST на случайных данных, но выиграла у BST на отсортированных и оказалась намного быстрее связного списка. Главный минус отсутствие естественного порядка при обходе.
2. **Сбалансированное дерево** (AVL или красно-чёрное) выбор, когда нужны оба свойства: быстрый доступ (O(log n)) и возможность получать данные в отсортированном порядке без дополнительной сортировки. Обычный BST (как в моей реализации) **использовать не стоит**, если нельзя гарантировать случайный порядок входных данных. Деградация на упорядоченных данных делает его непригодным для реальных систем.
3. **Связный список** практически бесполезен для хранения больших объёмов данных. Единственное оправданное применение очень маленькие коллекции (до сотни элементов), реализация очередей/стеков или учебные цели.
**Рекомендация**: если нужен только быстрый доступ по ключу берите хеш-таблицу. Если нужен отсортированный вывод и вы готовы пожертвовать небольшой долей производительности используйте сбалансированное дерево. От наивного BST и связного списка в реальных проектах лучше отказаться.
**Ключевой вывод эксперимента**: порядок поступления данных критически важен для производительности BST (разница в 400 раз между случайными и отсортированными данными), но почти не влияет на хеш-таблицу и связный список (хотя последний всегда медленный).