Merge pull request '[1, 2] ProninVV' (#242) from ProninVV/2026-rff_mp:ProninVV into develop

Reviewed-on: UNN/2026-rff_mp#242
This commit is contained in:
AndreyUrs 2026-05-30 11:23:28 +00:00
commit c2a40c8ce1
35 changed files with 39815 additions and 0 deletions

View File

@ -0,0 +1,250 @@
# LInkedList (Node = List = {'name': 'Имя', 'phone': '123', 'next': None}) имена уникальные (id)
def ll_insert(head, name, phone):
""" проходит до конца (или сразу добавляет в конец) и возвращает новую голову
(если вставка в начало) или изменяет список по ссылке. Удобнее возвращать новую
голову, если вставка может быть в начало """
new_node = {'name': name, 'phone': phone, 'next': None}
# если списка не было
if head is None:
return new_node
# # вставка в начало O(1)
# new_node = {'name': name, 'phone': phone, 'next': head}
# return new_node
# вставка в конец O(n)
current = head
while current['next'] is not None:
# проверка существования данного идентификатора (обновляем запись)
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
# проверка на id
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_list_all(head):
""" собирает все записи в список и сортирует (сортировка вынесена отдельно) """
length = ll_Lenght(head)
new_list = [None]*length
current = head
for i in range(length):
new_list[i] = (current['name'], current['phone'])
current = current['next']
sorten(new_list)
return new_list
# вспомогательные функции--------------------------------
def ll_Lenght(head):
# длина связного списка
counter = 0
curr = head
while curr:
counter += 1
curr = curr['next']
return counter
def sorten(arr):
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j][0] > arr[j + 1][0]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
# -----------------------------------------------------------
# HashTable (Хранится как список buckets фиксированной длины, каждый элемент — голова связного списка (или None))
def hash_table(size):
return [None]*size
def hash_func(name, buckets_count):
h = 0
for char in name:
h += ord(char)
return h % buckets_count
def ht_insert(buckets, name, phone):
""" вычисляет индекс, вызывает ll_insert для соответствующего бакета """
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 ht_list_all(buckets):
""" собирает все записи из всех бакетов и сортирует """
total_count = 0
for head in buckets:
total_count += ll_Lenght(head)
full_data = [None]*total_count
k = 0
for head in buckets:
curr = head
while curr:
full_data[k] = (curr['name'], curr['phone'])
k += 1
curr = curr['next']
sorten(full_data)
return full_data
# Двоичное дерево поиска : Узел — словарь: {'name': 'Имя', 'phone': '123', 'left': None, 'right': None}
def bst_insert(root, name, phone):
""" рекурсивно или итеративно вставляет, возвращает новый корень (если корень меняется) """
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
# если дерева нет
if root is None:
return new_node
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):
""" поиск """
if root is None:
return None
if root['name'] == name:
return root['phone']
elif name < root['name']:
return bst_find(root['left'], name)
elif name > root['name']:
return bst_find(root['right'], name)
def bst_delete(root, name):
""" удаление, возвращает новый корень """
if root is None:
return None
# спускаемся к нужному узлу (аналогично поиску)
elif name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
# стоим в нужном узле
else:
# узла слева нет (вернет правого ребенка или None)
if root['left'] is None:
return root['right']
# узла справа нет (вернет левого ребенка)
if root['right'] is None:
return root['left']
# два наследника (поиск минимального поддерева в правой ветке)
successor = root['right']
while successor['left'] is not None:
successor = successor['left']
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):
""" центрированный обход (рекурсивно собирает записи в отсортированном порядке) """
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

View File

@ -0,0 +1,20 @@
import pandas as pd
import glob
folder_path = 'results'
sizes = ['500', '1000', '2000', '5000', '10000']
for size in sizes:
files = glob.glob(f'{folder_path}/timedata_{size}_epochs_*.csv')
data = [pd.read_csv(f)['Время (сек)'] for f in files]
datatomean = pd.concat(data, axis=1)
datamean = datatomean.mean(axis=1)
df = pd.read_csv(files[0])
df['Время (сек)'] = datamean
df.to_csv(f'results/aaverage_timedata_{size}.csv', index=False)

View File

@ -0,0 +1,94 @@
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator
df500 = pd.read_csv("results/aaverage_timedata_500.csv")
df1000 = pd.read_csv("results/aaverage_timedata_1000.csv")
df2000 = pd.read_csv("results/aaverage_timedata_2000.csv")
df5000 = pd.read_csv("results/aaverage_timedata_5000.csv")
df10000 = pd.read_csv("results/aaverage_timedata_10000.csv")
def select_data_list(ax):
dfs = [df500, df1000, df2000, df5000, df10000]
Nvals = [500, 1000, 2000, 5000, 10000]
# delete, find, insert
# список:
valsSort = [list(arr[(arr['Структура'] == "linklist") & (arr['Режим'] == "sorted")]["Время (сек)"]) for arr in dfs]
valsShuff = [list(arr[(arr['Структура'] == "linklist") & (arr['Режим'] == "shuffled")]["Время (сек)"]) for arr in dfs]
# 0 - sorted 1 - shuffled
# delete
ax[0].plot(Nvals, [row[0] for row in valsSort], label="delete", color='red')
ax[1].plot(Nvals, [row[0] for row in valsShuff], color='red')
# find
ax[0].plot(Nvals, [row[1] for row in valsSort], label="find", color='blue')
ax[1].plot(Nvals, [row[1] for row in valsShuff], color='blue')
# insert
ax[0].plot(Nvals, [row[2] for row in valsSort], label="insert", color='green')
ax[1].plot(Nvals, [row[2] for row in valsShuff], color='green')
def select_data_hasht(ax):
dfs = [df500, df1000, df2000, df5000, df10000]
Nvals = [500, 1000, 2000, 5000, 10000]
# delete, find, insert
# список:
valsSort = [list(arr[(arr['Структура'] == "hashtable") & (arr['Режим'] == "sorted")]["Время (сек)"]) for arr in dfs]
valsShuff = [list(arr[(arr['Структура'] == "hashtable") & (arr['Режим'] == "shuffled")]["Время (сек)"]) for arr in dfs]
# 0 - sorted 1 - shuffled
# delete
ax[0].plot(Nvals, [row[0] for row in valsSort], label="delete", color='red')
ax[1].plot(Nvals, [row[0] for row in valsShuff], color='red')
# find
ax[0].plot(Nvals, [row[1] for row in valsSort], label="find", color='blue')
ax[1].plot(Nvals, [row[1] for row in valsShuff], color='blue')
# insert
ax[0].plot(Nvals, [row[2] for row in valsSort], label="insert", color='green')
ax[1].plot(Nvals, [row[2] for row in valsShuff], color='green')
def select_data_tree(ax):
dfs = [df500, df1000, df2000, df5000, df10000]
Nvals = [500, 1000, 2000, 5000, 10000]
# delete, find, insert
# список:
valsSort = [list(arr[(arr['Структура'] == "bintree") & (arr['Режим'] == "sorted")]["Время (сек)"]) for arr in dfs]
valsShuff = [list(arr[(arr['Структура'] == "bintree") & (arr['Режим'] == "shuffled")]["Время (сек)"]) for arr in dfs]
# 0 - sorted 1 - shuffled
# delete
ax[0].plot(Nvals, [row[0] for row in valsSort], label="delete", color='red')
ax[1].plot(Nvals, [row[0] for row in valsShuff], color='red')
# find
ax[0].plot(Nvals, [row[1] for row in valsSort], label="find", color='blue')
ax[1].plot(Nvals, [row[1] for row in valsShuff], color='blue')
# insert
ax[0].plot(Nvals, [row[2] for row in valsSort], label="insert", color='green')
ax[1].plot(Nvals, [row[2] for row in valsShuff], color='green')
# построение графика
def design_show_graph(title, version, ymaxlim):
fig, ax = plt.subplots(figsize=(10, 5), nrows=1, ncols=2)
for i in range(2):
match title:
case "Tree":
select_data_tree(ax)
case "Linklist":
select_data_list(ax)
case "hasht":
select_data_hasht(ax)
ax[0].set_title(f"График сложностей для {title} (sort)")
ax[1].set_title(f"График сложностей для {title} (shuff)")
ax[i].set_xlabel("N")
ax[i].set_ylabel("сек * ")
ax[i].grid(which="major", linewidth=1.5)
ax[i].grid(which="minor", color="gray", linewidth=0.5)
ax[i].xaxis.set_minor_locator(AutoMinorLocator())
ax[i].yaxis.set_minor_locator(AutoMinorLocator())
ax[i].legend()
ax[i].set_ylim(0, ymaxlim)
plt.savefig(f'graphics\{title}{version}.png', dpi=200)
plt.savefig(f'graphics\T{title}{version}.eps', dpi=200)
plt.show()
design_show_graph("hasht", 2, 0.4)

Binary file not shown.

View File

@ -0,0 +1,136 @@
\input{preambule.tex}
\begin{document}
% --- ТИТУЛЬНЫЙ ЛИСТ (упрощенно) ---
\begin{titlepage}
\centering
МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РФ \\
«Национальный исследовательский Нижегородский государственный университет им. Н.И. Лобачевского» \\
\vspace{4cm}
\Large ОТЧЕТ К ЛАБОРАТОРНОЙ РАБОТЕ \\
\vspace{1cm}
\large «Реализация и экспериментальное сравнение базовых структур данных на примере телефонного справочника» \\
\vspace{4cm}
\flushright
Выполнил: студент В. В. Пронин \\
Преподаватель: Н. С. Морозов \\
\vfill
Нижний Новгород \\
2024
\end{titlepage}
\newpage
\tableofcontents
\newpage
\section{Введение}
Эффективность программных систем во многом определяется выбором способов организации данных в оперативной памяти. Задача разработки телефонного справочника является классическим примером, требующим баланса между скоростью вставки новых записей, поиском по ключу и эффективным удалением.
В рамках данной работы исследуются три фундаментальные структуры данных, реализованные «с нуля» в процедурной парадигме программирования на языке Python:
\begin{itemize}
\item \textbf{Связный список (Linked List)} --- динамическая структура, позволяющая оценить базовые механизмы управления указателями и демонстрирующая линейную сложность операций $O(n)$.
\item \textbf{Хеш-таблица (Hash Table)} --- ассоциативный массив, использующий хеширование для обеспечения прямого доступа к данным. Реализация позволяет изучить методы разрешения коллизий и преимущества константной сложности $O(1)$.
\item \textbf{Двоичное дерево поиска (BST)} --- иерархическая структура, обеспечивающая логарифмическую скорость доступа $O(\log n)$ и поддерживающая упорядоченность данных «из коробки».
\end{itemize}
\textbf{Цель работы:} Изучить внутренние алгоритмы работы перечисленных структур, реализовать их без использования встроенных высокоуровневых контейнеров и экспериментально подтвердить теоретические оценки временной сложности на случайных и отсортированных наборах данных.
\section{Реализация структур данных}
\subsection{Связный список}
% Здесь опишите логику ll_insert, ll_find и ll_delete
\subsection{Хеш-таблица}
% Опишите хеш-функцию и метод цепочек
\subsection{Двоичное дерево поиска}
% Опишите рекурсивные алгоритмы и проблему деградации
\section{Методика эксперимента}
Замеры производились для наборов данных объемом $N=500, 1000, 2000, 5000, 10000$ элементов. Использовались два сценария: перемешанные (\textit{shuffled}) и отсортированные по алфавиту (\textit{sorted}) записи. Каждая операция выполнялась 5 раз с последующим вычислением среднего арифметического значения с помощью функции \texttt{time.perf\_counter()}.
\section{Результаты и анализ}
Было проведено серию опытов для $N$ от 500 до 10000.
\subsection*{1. Бинарное дерево поиска (BST) и влияние порядка}
\begin{figure}[H]
\centering
\includegraphics[scale=0.7]{plots/TTree1.eps}
\caption{Зависимость времени выполнения операций в BST от объема данных}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[scale=0.7]{plots/TTree2.eps}
\end{figure}
\begin{itemize}
\item \textbf{Деградация на отсортированных данных:} При вставке отсортированных данных время увеличилось с \textbf{0.124с} ($N=1000$) до \textbf{13.27с} ($N=10000$). Рост времени в 100 раз при увеличении объема данных в 10 раз четко указывает на квадратичную сложность $O(n^2)$ для процесса заполнения всей структуры. Дерево выродилось в линейный список, и поиск места вставки стал занимать $O(n)$ вместо ожидаемого $O(\log n)$.
\item \textbf{Эффективность на перемешанных данных:} На \texttt{shuffled} данных вставка 10000 элементов заняла всего \textbf{0.031с}. Это подтверждает логарифмическую сложность $O(\log n)$ для операций в дереве при случайном распределении ключей.
\end{itemize}
\subsection*{2. Хеш-таблица: Стабильность и скорость}
\begin{figure}[H]
\centering
\includegraphics[scale=0.7]{plots/Thasht1.eps}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[scale=0.7]{plots/Thasht2.eps}
\end{figure}
\begin{itemize}
\item \textbf{Чувствительность к порядку:} Хеш-таблица показала идентичные результаты как на \texttt{shuffled}, так и на \texttt{sorted} данных (около \textbf{0.165с} -- \textbf{0.167с} для 10000 вставок). Это объясняется тем, что хеш-функция распределяет ключи по бакетам независимо от их исходного порядка, предотвращая деградацию структуры.
\item \textbf{Превосходство:} На больших объемах хеш-таблица оказалась самой быстрой структурой для поиска и удаления ($\approx 0.001$с при $N=10000$), что подтверждает теоретическую среднюю сложность $O(1)$.
\item \textbf{Замечание:} Так как реализация использует списки для разрешения коллизий со вставкой в конец, при заполнении таблицы наблюдается рост времени вставки, стремящийся к квадратичному, однако абсолютные значения остаются на порядки ниже, чем у выродившегося BST.
\end{itemize}
\subsection*{3. Связный список: Линейная зависимость}
\begin{figure}[H]
\centering
\includegraphics[scale=0.7]{plots/Tlinklist1.eps}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[scale=0.7]{plots/Tlinklist2.eps}
\end{figure}
\begin{itemize}
\item \textbf{Поиск и удаление:} Связный список показал худшие результаты среди всех структур на случайных данных. Время поиска при 10000 элементах (\textbf{0.029с}) значительно медленнее, чем у BST на перемешанных данных (\textbf{0.0002с}). Это подтверждает линейную сложность $O(n)$.
\item \textbf{Вставка:} Вставка (вероятно, в конец или с сохранением порядка) дает $O(n^2)$ при заполнении (\textbf{2.83с} -- \textbf{3.00с} на 10000 эл.). Характер роста времени при переходе от $N=5000$ (\textbf{0.71с}) к $N=10000$ подтверждает квадратичную зависимость.
\end{itemize}
\subsection*{Вывод: выбор структуры данных}
\begin{enumerate}
\item \textbf{Хеш-таблица} — наиболее универсальный выбор. Она обеспечивает стабильное $O(1)$ для поиска и не зависит от порядка входящих данных.
\item \textbf{BST} — крайне эффективен ($O(\log n)$) при случайном распределении данных, но без механизмов самобалансировки критически уязвим к отсортированным входным последовательностям, замедляясь до уровня списка.
\item \textbf{Связный список} — продемонстрировал самую низкую производительность на операциях поиска и массовой вставки. Его использование оправдано только в специфических сценариях (например, реализация стека), где работа ведется исключительно с головой списка за $O(1)$.
\end{enumerate}
\subsection*{Сводная таблица результатов}
\begin{table}[H]
\centering
\small
\begin{tabular}{|l|l|c|c|c|c|c|}
\hline
\textbf{Структура} & \textbf{Режим} & \textbf{Опер.} & \textbf{N=500} & \textbf{N=1000} & \textbf{N=5000} & \textbf{N=10000} \\ \hline
\multirow{3}{*}{LinkList} & Shuffled & Insert & 0.0066 & 0.0292 & 0.7089 & 2.8358 \\
& Shuffled & Find & 0.0012 & 0.0026 & 0.0147 & 0.0289 \\
& Sorted & Insert & 0.0065 & 0.0290 & 0.7637 & 3.0042 \\ \hline
\multirow{3}{*}{HashTable} & Shuffled & Insert & 0.0007 & 0.0022 & 0.0468 & 0.1670 \\
& Shuffled & Find & 0.0001 & 0.0002 & 0.0008 & 0.0014 \\
& Sorted & Insert & 0.0007 & 0.0022 & 0.0448 & 0.1646 \\ \hline
\multirow{3}{*}{BinTree} & Shuffled & Insert & 0.0009 & 0.0021 & 0.0145 & 0.0309 \\
& Shuffled & Find & 0.0001 & 0.0001 & 0.0002 & 0.0002 \\
& Sorted & Insert & \textbf{0.0298} & \textbf{0.1239} & \textbf{3.3052} & \textbf{13.2706} \\ \hline
\end{tabular}
\caption{Сравнение минимального времени выполнения операций (в секундах) в зависимости от объема данных $N$}
\end{table}
\end{document}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
%\documentclass[a4paper, 12pt]{article}
\documentclass[a4paper, 14pt]{extarticle}
\usepackage[english, russian]{babel}
\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{comment}
\usepackage{multirow}
\usepackage{fontspec}
\setmainfont{Times New Roman}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{geometry}
\usepackage{titleps}
\usepackage{graphicx}
\DeclareGraphicsExtensions{.pdf, .jpg}
\usepackage{wrapfig}
\usepackage{indentfirst}
\geometry{top=20mm}
\geometry{bottom=25mm}
\geometry{left=30mm}
\geometry{right=10mm}
\usepackage{float}
\usepackage{wrapfig}
\newpagestyle{main}{
\setheadrule{0.4pt}
\sethead{ННГУ им Н.И. Лобачесвкого}{}{В. В. Пронин}
\setfoot{}{\thepage}{}
}
\pagestyle{main}
%\setcounter{page}{2}
\linespread{1.5}
\setlength{\parindent}{10mm}
\setlength{\parskip}{1ex}

View File

@ -0,0 +1,19 @@
Структура,Режим,Операция,Время (сек)
linklist,shuffled,insert,0.02917951999970676
linklist,shuffled,find,0.00256621999997146
linklist,shuffled,delete,0.0018302000000403
hashtable,shuffled,insert,0.00223439999972464
hashtable,shuffled,find,0.00018408000032643998
hashtable,shuffled,delete,0.00013254000023147998
bintree,shuffled,insert,0.00211651999998134
bintree,shuffled,find,0.00014015999986434
bintree,shuffled,delete,7.299999997485429e-05
linklist,sorted,insert,0.02902601999994654
linklist,sorted,find,0.00272362000014248
linklist,sorted,delete,0.0017690399998172598
hashtable,sorted,insert,0.00219620000007122
hashtable,sorted,find,0.00018717999992074
hashtable,sorted,delete,0.00011756000003512
bintree,sorted,insert,0.12391134000008604
bintree,sorted,find,0.0079993400002422
bintree,sorted,delete,0.004170019999764881
1 Структура Режим Операция Время (сек)
2 linklist shuffled insert 0.02917951999970676
3 linklist shuffled find 0.00256621999997146
4 linklist shuffled delete 0.0018302000000403
5 hashtable shuffled insert 0.00223439999972464
6 hashtable shuffled find 0.00018408000032643998
7 hashtable shuffled delete 0.00013254000023147998
8 bintree shuffled insert 0.00211651999998134
9 bintree shuffled find 0.00014015999986434
10 bintree shuffled delete 7.299999997485429e-05
11 linklist sorted insert 0.02902601999994654
12 linklist sorted find 0.00272362000014248
13 linklist sorted delete 0.0017690399998172598
14 hashtable sorted insert 0.00219620000007122
15 hashtable sorted find 0.00018717999992074
16 hashtable sorted delete 0.00011756000003512
17 bintree sorted insert 0.12391134000008604
18 bintree sorted find 0.0079993400002422
19 bintree sorted delete 0.004170019999764881

View File

@ -0,0 +1,19 @@
Структура,Режим,Операция,Время (сек)
linklist,shuffled,insert,2.835846880000099
linklist,shuffled,find,0.02894071999999136
linklist,shuffled,delete,0.017179720000240158
hashtable,shuffled,insert,0.16700972000016914
hashtable,shuffled,find,0.0014067599999179402
hashtable,shuffled,delete,0.00103095999966166
bintree,shuffled,insert,0.030944720000115878
bintree,shuffled,find,0.00019450000017964003
bintree,shuffled,delete,9.787999988471869e-05
linklist,sorted,insert,3.0041990600000643
linklist,sorted,find,0.02895102000002222
linklist,sorted,delete,0.016321099999913664
hashtable,sorted,insert,0.16461017999990868
hashtable,sorted,find,0.0014511600000332201
hashtable,sorted,delete,0.0010335000002669001
bintree,sorted,insert,13.270635900000162
bintree,sorted,find,0.08588061999998894
bintree,sorted,delete,0.04398507999994758
1 Структура Режим Операция Время (сек)
2 linklist shuffled insert 2.835846880000099
3 linklist shuffled find 0.02894071999999136
4 linklist shuffled delete 0.017179720000240158
5 hashtable shuffled insert 0.16700972000016914
6 hashtable shuffled find 0.0014067599999179402
7 hashtable shuffled delete 0.00103095999966166
8 bintree shuffled insert 0.030944720000115878
9 bintree shuffled find 0.00019450000017964003
10 bintree shuffled delete 9.787999988471869e-05
11 linklist sorted insert 3.0041990600000643
12 linklist sorted find 0.02895102000002222
13 linklist sorted delete 0.016321099999913664
14 hashtable sorted insert 0.16461017999990868
15 hashtable sorted find 0.0014511600000332201
16 hashtable sorted delete 0.0010335000002669001
17 bintree sorted insert 13.270635900000162
18 bintree sorted find 0.08588061999998894
19 bintree sorted delete 0.04398507999994758

View File

@ -0,0 +1,140 @@
from aufg1 import *
import time
import random
import sys
import csv
sys.setrecursionlimit(20000)
def phone_number_generate():
number = "8"
text = "0123456789"
for i in range(10):
char = random.choice(text)
number += char
return number
def create_data(n=100):
""" создаем сразу обычный массив и остортированный """
records_sorted = []
for i in range(n):
name = f"User_{i:05d}"
phone = phone_number_generate()
records_sorted.append((name, phone))
records_shuffled = records_sorted[:]
random.shuffle(records_shuffled)
return records_sorted, records_shuffled
def run_expirement(epoch=1, elements=1000):
""" распределяем данные по трем структурам данных
тестируем время операций (вставки, удаления, перебора) и записываем полученные результаты в файл """
header = ["Структура", "Режим", "Операция", "Время (сек)"]
for j in range(epoch):
print(f"эпоха - {j+1}")
results = [header]
# создаем данные
records_sorted, records_shuffled = create_data(elements)
datasets = [
("shuffled", records_shuffled),
("sorted", records_sorted)]
# сразу будем обрабатывать и случайны и отсортированный данные
for label, arr in datasets:
linklist = None
hashtab = hash_table(elements)
bintree = None
# заполнение связного списка
start = time.perf_counter()
for p in arr:
linklist = ll_insert(linklist, p[0], p[1])
end = time.perf_counter()
results.append(["linklist", label, "insert", end-start])
# поиск 110 имен в связном списке
# несуществующие данные
nonedata = [(f"None_{i}", phone_number_generate()) for i in range(10)]
# случайная комбинация
chaossample = random.sample(arr, 100) + nonedata
start = time.perf_counter()
for p in chaossample:
ll_find(linklist, p[0])
end = time.perf_counter()
results.append(["linklist", label, "find", end-start])
# удаление 50 имен в св писке
deldata = random.sample(arr, 50)
start = time.perf_counter()
for p in deldata:
ll_delete(linklist, p[0])
end = time.perf_counter()
results.append(["linklist", label, "delete", end-start])
# заполнение хэш-тфблицы
start = time.perf_counter()
for p in arr:
ht_insert(hashtab, p[0], p[1])
end = time.perf_counter()
results.append(["hashtable", label, "insert", end-start])
# поиск 110 имен в хэш таблице
# несуществующие данные
nonedata = [(f"None_{i}", phone_number_generate()) for i in range(10)]
# случайная комбинация
chaossample = random.sample(arr, 100) + nonedata
start = time.perf_counter()
for p in chaossample:
ht_find(hashtab, p[0])
end = time.perf_counter()
results.append(["hashtable", label, "find", end-start])
# удаление 50 имен в хэш таблице
deldata = random.sample(arr, 50)
start = time.perf_counter()
for p in deldata:
ht_delete(hashtab, p[0])
end = time.perf_counter()
results.append(["hashtable", label, "delete", end-start])
# заполнение дерева
start = time.perf_counter()
for p in arr:
bintree = bst_insert(bintree, p[0], p[1])
end = time.perf_counter()
results.append(["bintree", label, "insert", end-start])
# поиск 110 имен в дереве
# несуществующие данные
nonedata = [(f"None_{i}", phone_number_generate()) for i in range(10)]
# случайная комбинация
chaossample = random.sample(arr, 100) + nonedata
start = time.perf_counter()
for p in chaossample:
bst_find(bintree, p[0])
end = time.perf_counter()
results.append(["bintree", label, "find", end-start])
# удаление 50 имен в дереве
deldata = random.sample(arr, 50)
start = time.perf_counter()
for p in deldata:
bst_delete(bintree, p[0])
end = time.perf_counter()
results.append(["bintree", label, "delete", end-start])
filename = f"results/timedata_{elements}_epochs_{j+1}.csv"
with open(filename, mode='w', encoding='utf-8', newline='') as file:
writer = csv.writer(file)
writer.writerows(results)
run_expirement(epoch=5, elements=5000)

View File

@ -0,0 +1,41 @@
from Maze import Cell, Maze
from strategy import PathFindingStrategy
class AStarStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
def heuristik(cell):
return abs(cell.x - exit.x) + abs(cell.y - exit.y)
parents = {start: None}
queue = [start]
if not start or not exit:
return [], 0
while len(queue) != 0:
best_cell = queue[0]
for cell in queue:
if heuristik(cell) < heuristik(best_cell):
best_cell = cell
u = best_cell
queue.remove(u)
if u == exit:
path = []
current = exit
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, len(parents)
childs = maze.getNeighbors(u)
for child in childs:
if child not in parents:
parents[child] = u
queue.append(child)
return [], len(parents)

View File

@ -0,0 +1,32 @@
from strategy import PathFindingStrategy
from Maze import Maze, Cell
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start: Cell, exit: Cell):
# очерель: перывй вошел - первый вышел
queue = [start]
# будем хранить откуда в какую клетку пришли
parents = {start: None}
if not start or not exit:
return [], 0
while (len(queue) != 0):
u = queue.pop(0)
if u == exit:
path = []
current = exit
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, len(parents)
childs = maze.getNeighbors(u)
for child in childs:
if child not in parents:
parents[child] = u
queue.append(child)
return [], len(parents)

View File

@ -0,0 +1,45 @@
from abc import ABC, abstractmethod
class Player:
def __init__(self, start_cell):
self.current_cell = start_cell
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
"""Выполняет действие. Возвращает True, если ход успешен."""
pass
@abstractmethod
def undo(self) -> None:
"""Откатывает действие назад."""
pass
class MoveCommand(Command):
def __init__(self, player: Player, maze, dx: int, dy: int):
self.player = player
self.maze = maze
self.dx = dx
self.dy = dy
self.previous_cell = None
def execute(self) -> bool:
new_x = self.player.current_cell.x + self.dx
new_y = self.player.current_cell.y + self.dy
next_cell = self.maze.getCell(new_x, new_y)
if next_cell and next_cell.isPassable():
self.previous_cell = self.player.current_cell
self.player.current_cell = next_cell
return True
print("Ошибка: Там стена или край лабиринта!")
return False
def undo(self) -> None:
if self.previous_cell:
self.player.current_cell = self.previous_cell

View File

@ -0,0 +1,40 @@
from Observer import Observer, Event
class ConsoleView(Observer):
def update(self, event: Event) -> None:
if event.type == "maze_loaded":
print("\n[Система] Лабиринт успешно загружен!")
self.render(event.data.get("maze"))
elif event.type == "path_found":
print("\n[Система] Алгоритм нашёл решение!")
self.render(event.data.get("maze"), path=event.data.get("path"))
elif event.type == "move":
print(
f"\n[Игрок] Переместился в точку: ({event.data.get('player_pos').x}, {event.data.get('player_pos').y})")
self.render(event.data.get("maze"),
player_position=event.data.get("player_pos"))
def render(self, maze, player_position=None, path=None) -> None:
path_set = set(path) if path else set()
for y in range(maze.height):
row_chars = []
for x in range(maze.width):
cell = maze.getCell(x, y)
if player_position and cell == player_position:
row_chars.append("P")
elif cell.isStart:
row_chars.append("S")
elif cell.isExit:
row_chars.append("E")
elif cell in path_set:
row_chars.append(".")
elif cell.isWall:
row_chars.append("#")
else:
row_chars.append(" ")
print("".join(row_chars))

View File

@ -0,0 +1,43 @@
from strategy import PathFindingStrategy
from Maze import Maze, Cell
class DeikstraFind(PathFindingStrategy):
def findPath(self, maze, start, exit):
if not start or not exit:
return [], len(parents)
queue = [start]
distances = {start: 0}
parents = {start: None}
while len(queue) != 0:
best_cell = queue[0]
for cell in queue:
if distances[cell] < distances[best_cell]:
best_cell = cell
u = best_cell
queue.remove(u)
if u == exit:
path = []
current = exit
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, len(parents)
for child in maze.getNeighbors(u):
distance_through_u = distances[u] + 1
if distance_through_u < distances.get(child, float('inf')):
distances[child] = distance_through_u
parents[child] = u
if child not in queue:
queue.append(child)
return [], len(parents)

View File

@ -0,0 +1,40 @@
import sys
from strategy import PathFindingStrategy
from Maze import Maze, Cell
sys.setrecursionlimit(15000)
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start, exit):
if not start or not exit:
return [], 0
visited = set()
path = []
count_cell = 0
def dfs(root: Cell) -> bool:
visited.add(root)
path.append(root)
# count_cell += 1
if root == exit:
return True
neighbors = maze.getNeighbors(root)
for neighbor in neighbors:
if neighbor not in visited:
if dfs(neighbor):
return True
path.pop()
return False
if dfs(start):
return path, len(visited)
return [], len(visited)

View File

@ -0,0 +1,49 @@
# модель клетки лабиринта
class Cell:
def __init__(self, x, y, isWall=False, isStart=False, isExit=False):
self.x = x
self.y = y
self.isWall = isWall
self.isStart = isStart
self.isExit = isExit
def isPassable(self):
return not self.isWall
# модель лабиринта
class Maze:
def __init__(self, height, width, start=None, exit=None):
self.height = height # строки
self.width = width # столбцы
self.__grid = [[Cell(x, y) for x in range(width)]
for y in range(height)]
self.start = start
self.exit = exit
def getCell(self, x, y) -> Cell:
if (0 <= x < self.width) and (0 <= y < self.height):
return self.__grid[y][x]
return None
def getNeighbors(self, cell):
dirs = {'left': (-1, 0), 'right': (1, 0),
'up': (0, 1), 'down': (0, -1)}
neighbors = []
for _, val in dirs.items():
dx, dy = val
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.getCell(nx, ny)
if neighbor and isinstance(neighbor, Cell) and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
if __name__ == "__main__":
maze1 = Maze(height=5, width=5, start=0, exit=4)
cell1 = maze1.getCell(2, 2)
print(maze1.getNeighbors(cell1))

View File

@ -0,0 +1,47 @@
from abc import ABC, abstractmethod
from Maze import Maze, Cell
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def __init__(self):
self._maze = None
@property
def maze(self):
return self._maze
def buildFromFile(self, filename: str):
with open(filename, mode='r', encoding='utf-8') as file:
lines = file.read().splitlines()
height = len(lines)
width = len(lines[0])
self._maze = Maze(height, width)
for y, line in enumerate(lines):
for x, char in enumerate(line):
cell = self._maze.getCell(x, y)
if char == '#':
cell.isWall = True
elif char == 'S':
cell.isStart = True
self._maze.start = cell
elif char == 'E':
cell.isExit = True
self._maze.exit = cell
self._validate()
return self._maze
def _validate(self):
if self._maze.start is None:
raise "в лабиринте нет старта"
if self._maze.exit is None:
raise "в лабиринте нет начала"

View File

@ -0,0 +1,57 @@
import time
from Maze import Maze
from strategy import PathFindingStrategy
class SearchStats:
def __init__(self, execution_time, visited_count, path_length, path):
self.execution_time = execution_time
self.visited_count = visited_count
self.path_length = path_length
self.path = path
def __str__(self):
return ("f == Статистика поиска == =\n"
f"Время выполнения: {self.execution_time_ms:.4f} мс\n"
f"Посещено клеток: {self.visited_count}\n"
f"Длина пути: {self.path_length} клеток\n")
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self._maze = maze
self._strategy = strategy
self._observers = []
def addObserver(self, observer):
"""Регистрация нового наблюдателя (например, ConsoleView)"""
self._observers.append(observer)
def notify(self, event):
"""Уведомление всех подписчиков о событии"""
for observer in self._observers:
observer.update(event)
def setStrategy(self, strategy):
self._strategy = strategy
def solve(self):
if not self._maze or not self._strategy:
raise ValueError("Не задан лабиринт или стратегия поиска!")
start_time = time.perf_counter()
path, visited_count = self._strategy.findPath(
self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
execution_time_ms = (end_time - start_time) * 1000
path_length = len(path)
from ConsoleView import Event
self.notify(Event("path_found", {"maze": self._maze, "path": path}))
return SearchStats(execution_time_ms, visited_count, path_length, path)

View File

@ -0,0 +1,13 @@
from abc import ABC, abstractmethod
class Event:
def __init__(self, type: str, data: dict = None):
self.type = type # "maze_loaded", "move", "path_found"
self.data = data if data else {}
class Observer(ABC):
@abstractmethod
def update(self, event: Event) -> None:
pass

View File

@ -0,0 +1,8 @@
from MazeBuilder import TextFileMazeBuilder
from BreadthFirstSearch import BFSStrategy
from Maze import Maze
maze1 = TextFileMazeBuilder().buildFromFile("text.txt")
pathh = BFSStrategy.findPath(maze1, maze1.start, maze1.exit)
print(pathh)

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,302 @@
\input{preambule.tex}
\begin{document}
\thispagestyle{empty}
\centerline{МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ}
\centerline{НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ НИЖЕГОРОДСКИЙ}
\centerline{ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ ИМ Н. И. ЛОБАЧЕВСКОГО}
\centerline{Радиофизический факультет}
\vfill
\centerline{\Large{Отчет к лабораторной работе}}
\centerline{\large{по Методам программирования}}
\centerline{\Large{Поиск выхода из лабиринта }}
\centerline{\Large{(объектно-ориентированная реализация с паттернами)}}
\vfill
Студент группы 427 \hfill Пронин Владислав Владимирович
Преподаватель \hfill Морозов Н. С.
\vfill
\centerline{Н. Новгород, 2026}
\clearpage
\newpage
\tableofcontents
\newpage
\section{Цель работы}
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
\section{Описание задачи и выбранных паттернов}
Используемые Паттерны:
\begin{itemize}
\item Strategy (Стратегия) \textemdash \ это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы. Выбран, так как в данной лабораторной работе используются несколько алгоритмов, выполняющих одно и то же действие \ \textemdash \ обход графа.
\item Builder (строитель) \textemdash \ абстрактный класс/интерфейс, который определяет все этапы, необходимые для производства сложного объекта-продукта. Позволяет отделить построение сложного объекта от его представления, создает сложные объекты, используя простые объекты и поэтапный подход. Выбран для изоляции сложного процесса парсинга текстовоо файла.
\item Observer (Наблюдатель) \ \textemdash \ это поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах. Выбран для отделения логики приложения от вывода на экран (принцип MVC). Класс ConsoleView подписывается на события GameController и перерисовывает карту только тогда, когда игрок перемещается или путь найден.
\end{itemize}
\section{Диаграмма классов}
\begin{figure}[H]
\centering
\includegraphics[scale=0.06]{plan.png}
\end{figure}
\section{Листиги Классов}
\subsection{Maze Solver}
\begin{lstlisting}
import time
from Maze import Maze
from strategy import PathFindingStrategy
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self._maze = maze
self._strategy = strategy
self._observers = []
def addObserver(self, observer):
"""Регистрация нового наблюдателя (например, ConsoleView)"""
self._observers.append(observer)
def notify(self, event):
"""Уведомление всех подписчиков о событии"""
for observer in self._observers:
observer.update(event)
def setStrategy(self, strategy):
self._strategy = strategy
def solve(self):
if not self._maze or not self._strategy:
raise ValueError("Не задан лабиринт или стратегия поиска!")
start_time = time.perf_counter()
path, visited_count = self._strategy.findPath(
self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
execution_time_ms = (end_time - start_time) * 1000
path_length = len(path)
from ConsoleView import Event
self.notify(Event("path_found", {"maze": self._maze, "path": path}))
return SearchStats(execution_time_ms, visited_count, path_length, path)
\end{lstlisting}
\subsection{Maze Builder}
\begin{lstlisting}
from abc import ABC, abstractmethod
from Maze import Maze, Cell
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def __init__(self):
self._maze = None
@property
def maze(self):
return self._maze
def buildFromFile(self, filename: str):
with open(filename, mode='r', encoding='utf-8') as file:
lines = file.read().splitlines()
height = len(lines)
width = len(lines[0])
self._maze = Maze(height, width)
for y, line in enumerate(lines):
for x, char in enumerate(line):
cell = self._maze.getCell(x, y)
if char == '#':
cell.isWall = True
elif char == 'S':
cell.isStart = True
self._maze.start = cell
elif char == 'E':
cell.isExit = True
self._maze.exit = cell
self._validate()
return self._maze
def _validate(self):
if self._maze.start is None:
raise "в лабиринте нет старта"
if self._maze.exit is None:
raise "в лабиринте нет начала"
\end{lstlisting}
\subsection{OBserver}
\begin{lstlisting}
from Observer import Observer, Event
class ConsoleView(Observer):
def update(self, event: Event) -> None:
if event.type == "maze_loaded":
print("\n[Система] Лабиринт успешно загружен!")
self.render(event.data.get("maze"))
elif event.type == "path_found":
print("\n[Система] Алгоритм нашёл решение!")
self.render(event.data.get("maze"), path=event.data.get("path"))
elif event.type == "move":
print(
f"\n[Игрок] Переместился в точку: ({event.data.get('player_pos').x}, {event.data.get('player_pos').y})")
self.render(event.data.get("maze"),
player_position=event.data.get("player_pos"))
def render(self, maze, player_position=None, path=None) -> None:
path_set = set(path) if path else set()
for y in range(maze.height):
row_chars = []
for x in range(maze.width):
cell = maze.getCell(x, y)
if player_position and cell == player_position:
row_chars.append("P")
elif cell.isStart:
row_chars.append("S")
elif cell.isExit:
row_chars.append("E")
elif cell in path_set:
row_chars.append(".")
elif cell.isWall:
row_chars.append("#")
else:
row_chars.append(" ")
print("".join(row_chars))
\end{lstlisting}
\section{Результаты}
Таблицы замеров времени и посещенных клеток:
\begin{table}[H]
\centering
\caption{Результаты экспериментального сравнения алгоритмов поиска пути}
\label{tab:maze_benchmark}
\begin{tabular}{llccc}
\toprule
\textbf{Лабиринт} & \textbf{Стратегия} & \textbf{Время (мс)} & \textbf{Посещено клеток} & \textbf{Длина пути} \\
\midrule
\multirow{4}{*}{Маленький (10×10)}
& BFS & 0.0516 & 28 & 15 \\
& DFS & 0.0275 & 15 & 15 \\
& A* & 0.0360 & 16 & 15 \\
& Дейкстра & 0.0722 & 28 & 15 \\
\midrule
\multirow{4}{*}{Пустой (30×30)}
& BFS & 1.1863 & 870 & 58 \\
& DFS & 1.5568 & 842 & 842 \\
& A* & 0.4405 & 113 & 58 \\
& Дейкстра & 2.8607 & 870 & 58 \\
\midrule
\multirow{4}{*}{Без выхода (15×15)}
& BFS & 0.2230 & 160 & 0 \\
& DFS & 0.2959 & 160 & 0 \\
& A* & 0.9378 & 160 & 0 \\
& Дейкстра & 0.4148 & 160 & 0 \\
\midrule
\multirow{4}{*}{Средний (50×50)}
& BFS & 3.2247 & 1779 & 95 \\
& DFS & 1.6985 & 873 & 873 \\
& A* & 0.7348 & 158 & 95 \\
& Дейкстра & 6.1264 & 1779 & 95 \\
\midrule
\multirow{4}{*}{Большой (100×100)}
& BFS & 10.1308 & 7320 & 195 \\
& DFS & 6.1878 & 3549 & 3549 \\
& A* & 2.8441 & 328 & 195 \\
& Дейкстра & 35.2250 & 7320 & 195 \\
\bottomrule
\end{tabular}
\end{table}
Графики:
\begin{figure}[H]
\includegraphics[scale=0.6]{time.eps}
\end{figure}
\begin{figure}[H]
\includegraphics[scale=0.6]{cells.eps}
\end{figure}
\section{Анализ эффективности}
Так как в нашем лабиринте вес всех ребер равны 1, то Дейкстра выродился в Поиск в ширину. Также Дейкстра несколько медленнее из за дополнительных расчетов на сортировку стоимостей.
Самым лучшим по скорости стал алгоритм А*. Он в среднем 3-4 раза быстрее поиска в ширину, так как на каждом шаге он выбирает самого оптимального соседа для каждого узла, а поиск в ширину проверяет всех соседей.
В разработанной рекурсивной стратегии DFS метрика посещенных клеток совпадает с длиной пути, так как алгоритм фиксирует состояние успешно развернутого стека вызовов в момент достижения целевой точки. Все тупиковые ветви, из которых рекурсия вышла до момента нахождения exit, отсекаются архитектурой возврата флага True, что демонстрирует специфику работы рекурсивного бэктрекинга в Python
\section{Выводы по ООП}
В ходе выполнения лабораторной работы была спроектирована и реализована объектно-ориентированная система поиска пути в лабиринтах. Применение принципов ООП и паттернов проектирования GoF позволило полностью разделить зоны ответственности классов (принцип Single Responsibility) и обеспечить высокий уровень гибкости и расширяемости приложения.
1. Как паттерны помогли сделать код гибким и расширяемым
\begin{itemize}
\item Разделение логики построения и представления (Паттерн Builder):
Процесс создания лабиринта инкапсулирован внутри класса TextFileMazeBuilder. Сам лабиринт (Maze) и алгоритмы поиска никак не завязаны на формат хранения данных. Если в будущем потребуется сменить текстовый формат .txt на структуру .json достаточно будет создать нового строителя, реализующего интерфейс MazeBuilder.
\item Изоляция и динамическая смена алгоритмов (Паттерн Strategy):
Каждый алгоритм обхода графа вынесен в отдельный класс-стратегию с единым интерфейсом PathfindingStrategy. Класс-оркестратор MazeSolver работает исключительно с абстракцией.
\item Использование Observer позволило отделить вычислительную составляющую от графической. Maze SOlver никак не учитывает где и как будут отображаться данные, он только отдает сигнал о событиях. Это позволяет если нужно изменить графический инт6ерфейс.
\end{itemize}
\end{document}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,83 @@
%\documentclass[a4paper, 12pt]{article}
\documentclass[a4paper, 14pt]{extarticle}
\usepackage[english, russian]{babel}
\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{comment}
\usepackage{fontspec}
\setmainfont{Times New Roman}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{geometry}
\usepackage{titleps}
\usepackage{graphicx}
\DeclareGraphicsExtensions{.pdf, .jpg}
\usepackage{wrapfig}
\usepackage{indentfirst}
\geometry{top=20mm}
\geometry{bottom=25mm}
\geometry{left=30mm}
\geometry{right=10mm}
\usepackage{float}
\usepackage{wrapfig}
\newpagestyle{main}{
\setheadrule{0.4pt}
\sethead{ННГУ им Н.И. Лобачесвкого}{}{В. В. Пронин}
\setfoot{}{\thepage}{}
}
\pagestyle{main}
%\setcounter{page}{2}
\linespread{1.5}
\setlength{\parindent}{10mm}
\setlength{\parskip}{1ex}
\usepackage{listings}
\usepackage{xcolor}
% Настройка цветов для аккуратного кода
\definecolor{codegreen}{rgb}{0,0.5,0}
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
\definecolor{codepurple}{rgb}{0.58,0,0.82}
\definecolor{backcolour}{rgb}{0.97,0.97,0.96}
\lstset{
backgroundcolor=\color{backcolour},
commentstyle=\color{codegreen},
keywordstyle=\color{blue}\bfseries,
numberstyle=\tiny\color{codegray},
stringstyle=\color{codepurple},
basicstyle=\ttfamily\small, % Моноширинный аккуратный шрифт
breakatwhitespace=false,
breaklines=true, % Автоперенос длинных строк
captionpos=b, % Подпись снизу
keepspaces=true,
numbers=left, % Нумерация строк слева
numbersep=8pt,
showspaces=false,
showstringspaces=false,
showtabs=false,
tabsize=4,
language=Python,
frame=single, % Тонкая рамка вокруг кода
rulecolor=\color{lightgray}
}
\usepackage{booktabs} % Для красивых горизонтальных линий
\usepackage{multirow} % Для объединения строк по вертикали
\usepackage{float} % Для точного позиционирования таблицы [H]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
from typing import List
from Maze import Maze, Cell
# интерфейс стратегий
class PathFindingStrategy(ABC):
@abstractmethod
def findPath(maze: Maze, start, exit) -> List[Cell]:
""" возвращает список клеток пути (от старта до выхода включительно) или пустой список, если пути нет """
pass

134
ProninVV/task-2-oop/test.py Normal file
View File

@ -0,0 +1,134 @@
import csv
import time
import os
import matplotlib.pyplot as plt
import numpy as np
from MazeBuilder import TextFileMazeBuilder
from MazeSolver import MazeSolver, SearchStats
from DepthFirstSearch import DFSStrategy
from BreadthFirstSearch import BFSStrategy
from Deikstra import DeikstraFind
from AStarStrategy import AStarStrategy
from ConsoleView import ConsoleView
def run_benchmarks():
files = ["mazes/maze_small.txt", "mazes/maze_empty.txt",
"mazes/maze_no_exit.txt", "mazes/maze_medium.txt", "mazes/maze_large.txt"]
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy(),
"Deikstra": DeikstraFind()
}
view = ConsoleView()
NUM_RUNS = 5
results = []
print("Запуск экспериментов...")
for file in files:
if not os.path.exists(file):
print(f"Файл {file} не найден. Пропуск.")
continue
for name, strategy in strategies.items():
total_time = 0.0
visited_counts = []
path_lengths = []
print(f" работает {name}")
for _ in range(NUM_RUNS):
# Пересоздаем лабиринт
builder = TextFileMazeBuilder()
builder.buildFromFile(file)
maze = builder.maze
solver = MazeSolver(maze, strategy)
solver.addObserver(view)
stats = solver.solve()
total_time += stats.execution_time
visited_counts.append(stats.visited_count)
path_lengths.append(stats.path_length)
# средние значения
avg_time = total_time / NUM_RUNS
avg_visited = int(np.mean(visited_counts))
avg_path = int(np.mean(path_lengths))
results.append({
"лабиринт": file,
"стратегия": name,
"время_мс": round(avg_time, 4),
"посещено_клеток": avg_visited,
"длина_пути": avg_path
})
# Запись в CSV
csv_file = "results/maze_benchmark_results.csv"
with open(csv_file, mode="w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=[
"лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"])
writer.writeheader()
writer.writerows(results)
print(f"Результаты успешно сохранены в {csv_file}")
return results
# Построение графиков
def plot_results(results):
print("Генерация графиков...")
mazes = sorted(list(set(r["лабиринт"] for r in results)))
strategies = ["BFS", "DFS", "A*"]
# График Количество посещенных клеток
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(mazes))
width = 0.25
for i, strat in enumerate(strategies):
visited = [next(r["посещено_клеток"] for r in results if r["лабиринт"]
== m and r["стратегия"] == strat) for m in mazes]
ax.bar(x + i*width, visited, width, label=strat)
ax.set_ylabel('Количество посещенных клеток')
ax.set_title('Сравнение эффективности обхода лабиринтов (меньше = лучше)')
ax.set_xticks(x + width)
ax.set_xticklabels(mazes, rotation=15)
ax.legend()
plt.tight_layout()
plt.savefig("results/benchmark_visited_cells.png", dpi=200)
plt.savefig("results/benchmark_visited_cells.eps", dpi=200)
plt.close()
# График Время выполнения
fig, ax = plt.subplots(figsize=(10, 6))
for i, strat in enumerate(strategies):
times = [next(r["время_мс"] for r in results if r["лабиринт"]
== m and r["стратегия"] == strat) for m in mazes]
ax.bar(x + i*width, times, width, label=strat)
ax.set_ylabel('Время выполнения (мс)')
ax.set_title('Сравнение времени работы алгоритмов')
ax.set_xticks(x + width)
ax.set_xticklabels(mazes, rotation=15)
ax.legend()
plt.tight_layout()
plt.savefig("results/benchmark_execution_time.png", dpi=200)
plt.savefig("results/benchmark_execution_time.eps", dpi=200)
plt.close()
print("Графики сохранены в текущую директорию.")
if __name__ == "__main__":
data = run_benchmarks()
plot_results(data)