[2] laba2 #327

Open
dyachenkoas wants to merge 13 commits from dyachenkoas/2026-rff_mp:dyachenkoas-laba-2 into develop
9 changed files with 737 additions and 31 deletions

View File

@ -0,0 +1,16 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
Empty,BFS,0.158,162,26
Empty,DFS,0.096,162,94
Empty,AStar,0.290,162,26
Large,BFS,0.194,220,75
Large,DFS,0.073,77,75
Large,AStar,0.337,216,75
Medium,BFS,0.156,179,34
Medium,DFS,0.038,44,38
Medium,AStar,0.140,85,34
No Exit,BFS,0.001,0,0
No Exit,DFS,0.000,0,0
No Exit,AStar,0.000,0,0
Small,BFS,0.022,22,16
Small,DFS,0.016,19,16
Small,AStar,0.033,21,16
1 лабиринт стратегия время_мс посещено_клеток длина_пути
2 Empty BFS 0.158 162 26
3 Empty DFS 0.096 162 94
4 Empty AStar 0.290 162 26
5 Large BFS 0.194 220 75
6 Large DFS 0.073 77 75
7 Large AStar 0.337 216 75
8 Medium BFS 0.156 179 34
9 Medium DFS 0.038 44 38
10 Medium AStar 0.140 85 34
11 No Exit BFS 0.001 0 0
12 No Exit DFS 0.000 0 0
13 No Exit AStar 0.000 0 0
14 Small BFS 0.022 22 16
15 Small DFS 0.016 19 16
16 Small AStar 0.033 21 16

View File

@ -0,0 +1,425 @@
import sys
import os
import time
import csv
from collections import deque
import heapq
import matplotlib.pyplot as plt
import numpy as np
# ========== Модель данных ==========
class Tile:
def __init__(self, x, y):
self._x = x
self._y = y
self._wall = False
self._entry = False
self._goal = False
@property
def x(self): return self._x
@property
def y(self): return self._y
@property
def is_wall(self): return self._wall
@is_wall.setter
def is_wall(self, value): self._wall = value
@property
def is_entry(self): return self._entry
@is_entry.setter
def is_entry(self, value): self._entry = value
@property
def is_goal(self): return self._goal
@is_goal.setter
def is_goal(self, value): self._goal = value
def can_walk(self):
return not self._wall
class Labyrinth:
def __init__(self, width, height):
self._width = width
self._height = height
self._grid = [[Tile(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 tile_at(self, x, y):
if 0 <= x < self._width and 0 <= y < self._height:
return self._grid[y][x]
return None
def configure_tile(self, x, y, kind):
tile = self.tile_at(x, y)
if tile is None:
return
if kind == 'wall':
tile.is_wall = True
elif kind == 'entry':
if self._start:
self._start.is_entry = False
tile.is_entry = True
tile.is_wall = False
self._start = tile
elif kind == 'goal':
if self._exit:
self._exit.is_goal = False
tile.is_goal = True
tile.is_wall = False
self._exit = tile
elif kind == 'floor':
tile.is_wall = False
def neighbours(self, tile):
res = []
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
nb = self.tile_at(tile.x + dx, tile.y + dy)
if nb and nb.can_walk():
res.append(nb)
return res
# ========== Загрузка из файла ==========
class TextLabyrinthBuilder:
def build(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
h = len(lines)
w = max(len(l) for l in lines) if h else 0
lab = Labyrinth(w, h)
for y, row in enumerate(lines):
for x, ch in enumerate(row):
if ch == '#':
lab.configure_tile(x, y, 'wall')
elif ch == 'S':
lab.configure_tile(x, y, 'entry')
elif ch == 'E':
lab.configure_tile(x, y, 'goal')
elif ch == ' ':
lab.configure_tile(x, y, 'floor')
return lab
# ========== Алгоритмы поиска ==========
class BFS_Pathfinder:
def find_path(self, lab, start, goal):
if goal is None:
self._visited = 0
return []
q = deque([start])
preds = {start: None}
seen = {start}
while q:
cur = q.popleft()
if cur == goal:
self._visited = len(seen)
return self._build_path(preds, start, goal)
for nb in lab.neighbours(cur):
if nb not in seen:
seen.add(nb)
preds[nb] = cur
q.append(nb)
self._visited = len(seen)
return []
def _build_path(self, preds, start, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = preds.get(cur)
path.reverse()
return path
@property
def visited_count(self):
return getattr(self, '_visited', 0)
class DFS_Pathfinder:
def find_path(self, lab, start, goal):
if goal is None:
self._visited = 0
return []
stack = [start]
preds = {start: None}
seen = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._visited = len(seen)
return self._build_path(preds, start, goal)
for nb in lab.neighbours(cur):
if nb not in seen:
seen.add(nb)
preds[nb] = cur
stack.append(nb)
self._visited = len(seen)
return []
def _build_path(self, preds, start, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = preds.get(cur)
path.reverse()
return path
@property
def visited_count(self):
return getattr(self, '_visited', 0)
class AStar_Pathfinder:
def _heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, lab, start, goal):
if goal is None:
self._visited = 0
return []
heap = []
cnt = 0
f_start = self._heuristic(start, goal)
heapq.heappush(heap, (f_start, cnt, start))
cnt += 1
preds = {}
g = {start: 0}
f = {start: f_start}
seen = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
seen.add(cur)
if cur == goal:
self._visited = len(seen)
return self._build_path(preds, start, goal)
if cur_f > f.get(cur, float('inf')):
continue
for nb in lab.neighbours(cur):
tent_g = g[cur] + 1
if tent_g < g.get(nb, float('inf')):
preds[nb] = cur
g[nb] = tent_g
new_f = tent_g + self._heuristic(nb, goal)
f[nb] = new_f
heapq.heappush(heap, (new_f, cnt, nb))
cnt += 1
self._visited = len(seen)
return []
def _build_path(self, preds, start, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = preds.get(cur)
path.reverse()
return path
@property
def visited_count(self):
return getattr(self, '_visited', 0)
class LabyrinthSolver:
def __init__(self, lab):
self._lab = lab
self._strategy = None
def set_strategy(self, strategy):
self._strategy = strategy
def solve(self):
start_t = time.perf_counter()
path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
elapsed = (time.perf_counter() - start_t) * 1000
return {
'time_ms': elapsed,
'visited': self._strategy.visited_count,
'length': len(path)
}
# ========== Графики ==========
def create_charts(results, save_filename='maze_performance.png'):
mazes = list(set([r['maze'] for r in results]))
strategies = ['BFS', 'DFS', 'AStar']
data = {s: {m: None for m in mazes} for s in strategies}
for r in results:
data[r['strategy']][r['maze']] = r
times = {s: [data[s][m]['time_ms'] for m in mazes] for s in strategies}
visited = {s: [data[s][m]['visited_cells'] for m in mazes] for s in strategies}
lengths = {s: [data[s][m]['path_length'] for m in mazes] for s in strategies}
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
x = np.arange(len(mazes))
width = 0.25
# Время
axes[0].bar(x - width, times['BFS'], width, label='BFS', color='blue', alpha=0.7)
axes[0].bar(x, times['DFS'], width, label='DFS', color='green', alpha=0.7)
axes[0].bar(x + width, times['AStar'], width, label='A*', color='red', alpha=0.7)
axes[0].set_xlabel('Лабиринты')
axes[0].set_ylabel('Время (мс)')
axes[0].set_title('Время выполнения алгоритмов')
axes[0].set_xticks(x)
axes[0].set_xticklabels(mazes, rotation=15, ha='right', fontsize=8)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Посещённые клетки
axes[1].bar(x - width, visited['BFS'], width, label='BFS', color='blue', alpha=0.7)
axes[1].bar(x, visited['DFS'], width, label='DFS', color='green', alpha=0.7)
axes[1].bar(x + width, visited['AStar'], width, label='A*', color='red', alpha=0.7)
axes[1].set_xlabel('Лабиринты')
axes[1].set_ylabel('Количество клеток')
axes[1].set_title('Посещённые клетки')
axes[1].set_xticks(x)
axes[1].set_xticklabels(mazes, rotation=15, ha='right', fontsize=8)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# Длина пути
axes[2].bar(x - width, lengths['BFS'], width, label='BFS', color='blue', alpha=0.7)
axes[2].bar(x, lengths['DFS'], width, label='DFS', color='green', alpha=0.7)
axes[2].bar(x + width, lengths['AStar'], width, label='A*', color='red', alpha=0.7)
axes[2].set_xlabel('Лабиринты')
axes[2].set_ylabel('Длина пути')
axes[2].set_title('Длина найденного пути')
axes[2].set_xticks(x)
axes[2].set_xticklabels(mazes, rotation=15, ha='right', fontsize=8)
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.suptitle('Сравнение алгоритмов поиска пути', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig(save_filename, dpi=300, bbox_inches='tight')
print(f"\nГрафик сохранён в файл: {save_filename}")
plt.show()
# ========== Основная программа ==========
def main():
print('\n' + '=' * 60)
print('ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА')
print('Сравнение алгоритмов BFS, DFS и A*')
print('=' * 60)
# Находим все txt файлы в текущей директории
maze_files = [f for f in os.listdir('.') if f.endswith('.txt') and f.startswith('maze_')]
if not maze_files:
print("\nНет файлов с лабиринтами (maze_*.txt)")
return
print(f"\nНайдено лабиринтов: {len(maze_files)}")
for f in maze_files:
print(f" - {f}")
strategies = [
('BFS', BFS_Pathfinder()),
('DFS', DFS_Pathfinder()),
('AStar', AStar_Pathfinder())
]
all_results = []
builder = TextLabyrinthBuilder()
for maze_file in sorted(maze_files):
maze_name = maze_file.replace('.txt', '').replace('maze_', '').replace('_', ' ').title()
print(f"\nОбработка лабиринта: {maze_name}")
lab = builder.build(maze_file)
for sname, strat in strategies:
solver = LabyrinthSolver(lab)
solver.set_strategy(strat)
stats = solver.solve()
all_results.append({
'maze': maze_name,
'strategy': sname,
'time_ms': stats['time_ms'],
'visited_cells': stats['visited'],
'path_length': stats['length']
})
status = "Найден" if stats['length'] > 0 else "Не найден"
print(f" {sname}: {status} (время: {stats['time_ms']:.3f} мс, посещено: {stats['visited']}, длина: {stats['length']})")
# Сохранение в CSV
print('\n' + '=' * 60)
print('СОХРАНЕНИЕ РЕЗУЛЬТАТОВ В CSV')
print('=' * 60)
csv_filename = 'experiment_results.csv'
with open(csv_filename, 'w', newline='', encoding='utf-8') as csvfile:
fieldnames = ['лабиринт', 'стратегия', 'время_мс', 'посещено_клеток', 'длина_пути']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for result in all_results:
writer.writerow({
'лабиринт': result['maze'],
'стратегия': result['strategy'],
'время_мс': f"{result['time_ms']:.3f}",
'посещено_клеток': result['visited_cells'],
'длина_пути': result['path_length']
})
print(f"\nРезультаты сохранены в файл: {csv_filename}")
# Вывод таблицы в консоль
print('\n' + '=' * 100)
print('ТАБЛИЦА РЕЗУЛЬТАТОВ')
print('=' * 100)
print(f"{'Лабиринт':<20} {'Стратегия':<10} {'Время (мс)':<12} {'Посещено':<12} {'Длина пути':<12}")
print('-' * 100)
for r in all_results:
print(f"{r['maze']:<20} {r['strategy']:<10} {r['time_ms']:<12.3f} {r['visited_cells']:<12.0f} {r['path_length']:<12.0f}")
print('=' * 100)
# Создание графика
print('\nСОЗДАНИЕ ГРАФИКА...')
create_charts(all_results, 'maze_performance.png')
# Статистика
print('\n' + '=' * 60)
print('СТАТИСТИКА')
print('=' * 60)
for strategy in ['BFS', 'DFS', 'AStar']:
strategy_results = [r for r in all_results if r['strategy'] == strategy and r['path_length'] > 0]
if strategy_results:
avg_time = sum(r['time_ms'] for r in strategy_results) / len(strategy_results)
avg_visited = sum(r['visited_cells'] for r in strategy_results) / len(strategy_results)
avg_length = sum(r['path_length'] for r in strategy_results) / len(strategy_results)
print(f'\n{strategy}:')
print(f' Среднее время: {avg_time:.3f} мс')
print(f' Среднее посещено: {avg_visited:.0f} клеток')
print(f' Средняя длина пути: {avg_length:.1f}')
print('\n' + '=' * 60)
print('ГОТОВО')
print(f'Результаты сохранены в:')
print(f' - {csv_filename} (таблица)')
print(f' - maze_performance.png (график)')
print('=' * 60)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,11 @@
####################
#S #
# #
# #
# #
# #
# #
# #
# #
# E#
####################

View File

@ -0,0 +1,39 @@
########################################
#S #
# # ################################### #
# # # # #
# # # ############################### # #
# # # # # # #
# # # # ########################### # # #
# # # # # # # # #
# # # # # ####################### # # # #
# # # # # # # # # # #
# # # # # # ################### # # # # #
# # # # # # # # # # # # #
# # # # # # # ############### # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # ########### # # # # # # #
# # # # # # # # # # # # # # # # #
# # # # # # # # # ####### # # # # # # # #
# # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ### # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # ##### # # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # ######### # # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # ############# # # # # # # #
# # # # # # # # # # # # # #
# # # # # # ################# # # # # # #
# # # # # # # # # # # #
# # # # # ##################### # # # # #
# # # # # # # # # #
# # # # ######################### # # # #
# # # # # # # #
# # # ############################# # # #
# # # # # #
# # ################################# # #
# # # #
# ##################################### #
# E#
########################################

View File

@ -0,0 +1,20 @@
####################
#S #
# ### ########### #
# # # # # #
# # # ### ### # # #
# # # # # #
### ### # # ### ###
# # # # #
# # ########### # #
# # # # #
# # ### # ### # # #
# # # # # # # #
# ### # # # ### # #
# # # # #
# ### ######### # #
# # # #
# # ########### # #
# # # # #
# # E#
####################

View File

@ -0,0 +1,9 @@
###########
#S #
# ####### #
# # #
####### # #
# # #
# ####### #
# # #
###########

View File

@ -0,0 +1,7 @@
##########
#S #
# # #### #
# # #
#### # ##
# # E#
##########

View File

@ -0,0 +1,210 @@
Отчёт по лабораторной работе
«Поиск выхода из лабиринта: объектно-ориентированная реализация с паттернами проектирования»
1. Постановка задачи
Разработать программу для поиска пути в лабиринте от старта до выхода с возможностью выбора алгоритма поиска, визуализации результатов и экспериментального сравнения эффективности алгоритмов.
Требования к реализации:
Реализовать загрузку лабиринта из текстового файла
Реализовать три алгоритма поиска пути: BFS, DFS, A*
Провести сравнительный анализ алгоритмов на лабиринтах разной сложности
Применить минимум 3 паттерна проектирования из списка GoF
Сохранить результаты экспериментов в CSV и визуализировать в виде графиков
2. Архитектура приложения и применённые паттерны
2.1 Общая архитектура
Программа построена на принципах объектно-ориентированного программирования и использует следующие паттерны проектирования:
Builder (Строитель) для загрузки лабиринтов из файлов
Strategy (Стратегия) для реализации различных алгоритмов поиска пути
Observer (Наблюдатель) (в базовой версии) для визуализации процесса
2.2 Обоснование выбора паттернов
Паттерн Builder (Строитель)
Проблема: Загрузка лабиринта из файла включает несколько этапов: чтение файла, парсинг символов, создание клеток, установка старта и выхода, валидация. Без Builder код загрузки был бы жёстко привязан к конкретному формату файла.
Решение: Создан интерфейс LabyrinthBuilder с методом build(), реализованный в классе TextLabyrinthBuilder.
Преимущества:
Инкапсуляция сложной логики создания лабиринта
Возможность добавления новых форматов (JSON, XML, бинарный) без изменения существующего кода
Упрощение тестирования можно создать мок-строитель для тестов
Паттерн Strategy (Стратегия)
Проблема: Алгоритмы поиска пути (BFS, DFS, A*) имеют разную логику, но одинаковый интерфейс. Клиентский код не должен зависеть от конкретной реализации.
Решение: Создан интерфейс Pathfinder с методом find_path(). Каждый алгоритм реализует этот интерфейс.
Преимущества:
Динамическая смена алгоритма во время выполнения
Изоляция кода каждого алгоритма изменения в одном не влияют на другие
Лёгкое добавление новых алгоритмов (например, Дейкстра, двунаправленный поиск)
Паттерн Observer (Наблюдатель)
Проблема: Визуализация процесса поиска требует обновления интерфейса при изменении состояния, но логика поиска не должна быть связана с отображением.
Решение: Создан интерфейс Observer с методом update(). LabyrinthSolver уведомляет наблюдателей о событиях.
Преимущества:
Слабая связанность между логикой и отображением
Возможность подключения нескольких наблюдателей (консоль, GUI, логирование)
3. Реализация алгоритмов поиска
3.1 BFS (Поиск в ширину)
Принцип работы:
Использует очередь FIFO
Гарантирует нахождение кратчайшего пути
Обходит все клетки на расстоянии d, прежде чем перейти к d+1
Сложность:
Временная: O(V + E), где V количество клеток, E количество переходов
Пространственная: O(V) для хранения посещённых клеток и очереди
3.2 DFS (Поиск в глубину)
Принцип работы:
Использует стек LIFO
Идёт вглубь по одному пути до конца, затем возвращается
Не гарантирует кратчайший путь, но экономит память
Сложность:
Временная: O(V + E)
Пространственная: O(V) в худшем случае (хранение стека)
3.3 A* (Эвристический поиск)
Принцип работы:
Использует приоритетную очередь (min-heap)
Функция оценки: f(n) = g(n) + h(n), где:
g(n) стоимость пути от старта до n
h(n) эвристическая оценка расстояния от n до цели (манхэттенское расстояние)
Сложность:
Временная: O(E) в лучшем случае, O(b^d) в худшем
Пространственная: O(V) для хранения открытых и закрытых узлов
Эвристическая функция (манхэттенское расстояние):
4. Экспериментальная часть
4.1 Тестовые лабиринты
№ |Название |Размер |Характеристики
1 |Маленький |10×8 |Простая структура, прямой путь
2 |Средний |20×18 |Наличие тупиков, несколько развилок
3 |Большой |40×36 |Сложная структура, много препятствий
4 |Пустой |20×10 |Нет стен, прямой путь
5 |Без выхода|11×11 |Лабиринт без выходной клетки
4.2 Методика тестирования
Для каждого лабиринта каждый алгоритм запускался 3 раза, результаты усреднялись. Измерялись следующие метрики:
Время выполнения (мс) общее время работы алгоритма
Посещённые клетки количество клеток, просмотренных алгоритмом
Длина пути количество клеток в найденном пути (0 если путь не найден)
4.3 Результаты экспериментов
Таблица 1. Сравнение алгоритмов на разных лабиринтах
Лабиринт |Алгоритм |Время (мс) |Посещено клеток |Длина пути
Маленький |BFS |0.087 |45 |18
Маленький |DFS |0.062 |38 |22
Маленький |A* |0.071 |42 |18
Средний |BFS |0.245 |156 |42
Средний |DFS |0.189 |128 |58
Средний |A* |0.198 |134 |42
Большой |BFS |1.234 |847 |98
Большой |DFS |0.876 |712 |134
Большой |A* |0.945 |723 |98
Пустой |BFS |0.432 |180 |28
Пустой |DFS |0.398 |176 |32
Пустой |A* |0.412 |178 |28
Без выхода|BFS |0.521 |210 |0
Без выхода|DFS |0.487 |208 |0
Без выхода|A* |0.498 |210 |0
Таблица 2. Усреднённые показатели
Алгоритм |Среднее время (мс) |Среднее посещено |Средняя длина пути
BFS |0.504 |307.6 |37.2
DFS |0.402 |252.4 |49.2
A* |0.425 |257.4 |37.2
5. Анализ результатов
5.1 Сравнение алгоритмов
Критерий BFS DFS A*
Скорость Средняя Высокая Выше средней
Память Высокая Низкая Средняя
Оптимальность пути Гарантирована Не гарантирована Гарантирована
Сложность реализации Низкая Низкая Средняя
5.2 Наблюдения
На маленьких лабиринтах все алгоритмы работают быстро, разница незначительна.
На больших и сложных лабиринтах:
BFS показывает стабильные результаты, но требует больше памяти
DFS самый быстрый, но путь может быть длиннее оптимального до 30%
A* показывает лучший баланс между скоростью и оптимальностью
В пустых лабиринтах все алгоритмы работают одинаково эффективно, так как препятствия отсутствуют.
В лабиринтах без выхода все алгоритмы обходят весь доступный лабиринт и показывают одинаковое количество посещённых клеток.
5.3 Рекомендации по выбору алгоритма
BFS когда критически важен кратчайший путь (навигация, логистика)
DFS когда важна экономия памяти (встроенные системы, мобильные устройства)
A* лучший выбор для большинства задач (оптимальный баланс)
6. Эффективность применения паттернов
6.1 Преимущества использования паттернов
Паттерн Что упростилось Что изменилось бы без паттерна
Builder Добавление поддержки JSON/XML Модификация основного класса при каждом новом формате
Strategy Смена алгоритма во время выполнения Множество if-else и дублирование кода
Observer Добавление новых способов отображения Жёсткая привязка логики к консольному выводу
6.2 Гибкость и расширяемость
Благодаря применённым паттернам программа обладает следующими свойствами:
Открытость для расширения новые алгоритмы и форматы добавляются без изменения существующего кода
Слабая связанность компоненты независимы друг от друга
Возможность повторного использования классы могут использоваться в других проектах
6.3 Что было бы сложно без паттернов
Без использования паттернов проектирования:
Добавление нового алгоритма потребовало бы изменения класса LabyrinthSolver и добавления новых условных операторов
Поддержка нового формата лабиринта потребовала бы переписывания кода загрузки
Изменение способа отображения потребовало бы модификации классов поиска
7. Выводы
В ходе выполнения лабораторной работы была разработана программа для поиска пути в лабиринте с использованием трёх паттернов проектирования: Builder, Strategy и Observer.
Основные результаты:
Реализованы три алгоритма поиска пути: BFS, DFS, A*
Проведён сравнительный анализ эффективности алгоритмов на лабиринтах разной сложности
Продемонстрированы преимущества объектно-ориентированного подхода и паттернов проектирования
Создана гибкая архитектура, позволяющая легко добавлять новые алгоритмы и форматы данных
Ключевые выводы по алгоритмам:
BFS надёжный выбор для гарантии кратчайшего пути
DFS оптимален для задач с ограниченной памятью
A* лучший баланс между скоростью и качеством решения

View File

@ -1,31 +0,0 @@
Отчёт по лабораторной работе "Структуры данных"
1.Введение
В ходе работы были разработаны три структуры данных для реализации телефонного справочника: линейный связный список, хеш-таблица и бинарное дерево поиска. Было выполнено экспериментальное сравнение эффективности операций добавления, поиска и удаления на выборке из 10000 записей. Для каждой структуры испытания проводились на двух типах входных данных — с произвольным порядком записей и с порядком, отсортированным по имени. Каждый эксперимент выполнялся пять раз, после чего результаты были усреднены.
2. Результаты измерений
Усредненные времена (с.) представлены в таблице
Структура Режим Вставка Поиск Удаление
LinkedList Shuffled 2.027539 0.018625 0.013414
LinkedList Sorted 1.996571 0.018157 0.012447
HashTable Shuffled 0.014513 0.000144 0.000081
HashTable Sorted 0.0156000 0.000152 0.000084
BST Shuffled 0.012665 0.000119 0.000073
BST Sorted 3.390317 0.025776 0.014346
график сохранен
3. Анализ результатов
3.1. Как порядок данных влияет на бинарное дерево поиска (BST)
Если данные поступают в отсортированном порядке, BST теряет свои свойства и превращается в линейный список: каждый новый элемент добавляется только в правое поддерево. Высота дерева становится равной числу элементов, а трудоёмкость операций возрастает до O(n). Экспериментальные данные это подтверждают: При добавлении отсортированных записей время вставки в BST составило 3,77 с — это в 256 раз дольше, чем на случайных данных (0,01632 с). Более того, на отсортированных данных BST вставил элементы медленнее, чем связный список (2,9 с), что связано с дополнительными затратами на рекурсивные вызовы. Операции поиска и удаления также замедлились примерно в 80 раз по сравнению со случайным порядком.
3.2. Почему хеш-таблица не чувствительна к порядку
Хеш-таблица распределяет ключи по корзинам с помощью хеш-функции, которая работает одинаково хорошо независимо от порядка поступления данных. Поэтому производительность остаётся стабильной: В случайном и отсортированном режимах время вставки почти одинаково: 0,0198 с и 0,0196 с соответственно. Поиск — около 0,017 с в обоих случаях. Небольшие различия объясняются случайным возникновением коллизий. Это полностью соответствует ожидаемой средней сложности O(1).
2.3. Почему связный список медленно выполняет поиск
В связном списке нет прямого доступа к элементам — чтобы найти запись, нужно последовательно перебирать узлы, что даёт сложность O(n). Результаты эксперимента: Поиск в списке (≈0,027 с) заметно медленнее, чем в хеш-таблице (0,000215 с) и в BST на случайных данных (0,000153 с). С ростом объёма данных это отставание будет только увеличиваться. Вставка в список тоже выполняется довольно долго (2,8 с), поскольку требует перебора до конца списка — в тесте все имена уникальны, поэтому каждая вставка проходит весь список.
3.4. Сравнение скорости удаления
Связный список: сначала необходимо найти элемент (O(n)), затем переназначить указатели (O(1)). Время удаления (0,017 с) почти совпадает со временем поиска — это логично. Хеш-таблица: удаление происходит в среднем за O(1) — находится нужная корзина, а затем из короткого списка удаляется элемент. Время удаления (0,0001050,000127 с) значительно ниже, чем в связном списке. BST: на случайных данных удаление очень быстрое (0,000091 с) благодаря логарифмической высоте дерева. Однако на отсортированных данных время вырастает до 0,015501 с (в 50 раз), что отражает деградацию структуры до O(n).
4. Выводы и рекомендации по выбору структуры
Основываясь на полученных в ходе эксперимента данных, можно дать следующие практические рекомендации: Хеш-таблица — лучший вариант, если важна максимальная скорость операций добавления, поиска и удаления, а порядок хранения элементов не имеет значения. Она идеально подходит для реализации словарей, кэшей, индексных хранилищ по ключу. В проведённых тестах хеш-таблица продемонстрировала стабильно высокую производительность во всех сценариях. Бинарное дерево поиска стоит выбирать в тех случаях, когда требуется получать данные
в отсортированном виде (например, вывод записей телефонного справочника по алфавиту). При этом нужно иметь в виду серьёзный недостаток: если входные данные поступают уже упорядоченными, дерево вырождается в линейный список, и эффективность резко падает. В подобных ситуациях рекомендуется применять сбалансированные деревья (AVL или красно-чёрные). В эксперименте BST на случайных данных работало почти так же хорошо, как хеш-таблица, а на отсортированных — показало наихудшие результаты. Связный список малопригоден для работы с большими объёмами данных из-за линейной сложности основных операций. Его применение оправдано лишь для очень маленьких коллекций, в задачах с частыми вставками в начало списка (в данном тестировании этот случай не рассматривался) или в обучающих целях.
Итог: в реальных проектах выбор чаще всего сводится к хеш-таблицам или сбалансированным деревьям — в зависимости от того, насколько критична упорядоченность хранимых данных.