Compare commits

..

1 Commits

Author SHA1 Message Date
380c8f643f Upload files to "Ezhovnd"
Лабораторная работа №1
2026-05-25 07:21:19 +00:00
11 changed files with 600 additions and 763 deletions

View File

@ -1,285 +0,0 @@
import csv
import time
import os
import random
from collections import deque
import heapq
import matplotlib.pyplot as plt
import pandas as pd
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
def isPassable(self):
return not self.is_wall
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.cells = []
self.start = None
self.exit = None
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def getNeighbors(self, cell):
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
neighbor = self.getCell(cell.x + dx, cell.y + dy)
if neighbor and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder:
def buildFromFile(self, filename):
raise NotImplementedError
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
maze.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
for y, line in enumerate(lines):
for x, char in enumerate(line):
cell = maze.cells[y][x]
if char == '#':
cell.is_wall = True
elif char == 'S':
cell.is_start = True
maze.start = cell
elif char == 'E':
cell.is_exit = True
maze.exit = cell
if maze.start is None or maze.exit is None:
raise ValueError("В файле должны быть символы S и E")
return maze
class PathFindingStrategy:
def findPath(self, maze, start, exit):
raise NotImplementedError
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
queue = deque([start])
came_from = {start: None}
visited = set([start])
while queue:
current = queue.popleft()
if current == exit:
break
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
came_from[neighbor] = current
path = self._reconstruct_path(came_from, exit)
return path, len(visited)
def _reconstruct_path(self, came_from, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path if path and path[0] == came_from.get(exit) or path[0] == exit else []
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
stack = [start]
came_from = {start: None}
visited = set([start])
while stack:
current = stack.pop()
if current == exit:
break
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
stack.append(neighbor)
came_from[neighbor] = current
path = self._reconstruct_path(came_from, exit)
return path, len(visited)
def _reconstruct_path(self, came_from, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
class AStarStrategy(PathFindingStrategy):
def heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def findPath(self, maze, start, exit):
open_set = []
counter = 0
heapq.heappush(open_set, (0, counter, start))
came_from = {start: None}
g_score = {start: 0}
visited = set()
while open_set:
_, _, current = heapq.heappop(open_set)
if current in visited:
continue
visited.add(current)
if current == exit:
break
for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + self.heuristic(neighbor, exit)
counter += 1
heapq.heappush(open_set, (f_score, counter, neighbor))
path = self._reconstruct_path(came_from, exit)
return path, len(visited)
def _reconstruct_path(self, came_from, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
class SearchStats:
def __init__(self, time_ms, visited_cells, path_length):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
class MazeSolver:
def __init__(self, maze=None, strategy=None):
self.maze = maze
self.strategy = strategy
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if not self.maze or not self.strategy:
return None
start_time = time.perf_counter()
path, visited_count = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000
path_length = len(path) if path and path[-1] == self.maze.exit else 0
return SearchStats(round(time_ms, 4), visited_count, path_length)
def create_maze_with_walls(size, wall_probability=0.3):
maze = Maze(size, size)
maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)]
for y in range(size):
for x in range(size):
if random.random() < wall_probability:
maze.cells[y][x].is_wall = True
maze.start = maze.cells[0][0]
maze.exit = maze.cells[size-1][size-1]
maze.start.is_start = True
maze.exit.is_exit = True
maze.start.is_wall = False
maze.exit.is_wall = False
return maze
def create_empty_maze(size):
maze = Maze(size, size)
maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)]
maze.start = maze.cells[0][0]
maze.exit = maze.cells[size-1][size-1]
maze.start.is_start = True
maze.exit.is_exit = True
return maze
def create_no_exit_maze(size, wall_probability=0.3):
maze = create_maze_with_walls(size, wall_probability)
maze.exit.is_wall = True
return maze
def run_experiment():
maze_configs = {
"10x10_simple": {"size": 10, "type": "normal", "wall_prob": 0.1},
"50x50_with_deadends": {"size": 50, "type": "normal", "wall_prob": 0.3},
"100x100_complex": {"size": 100, "type": "normal", "wall_prob": 0.35},
"empty": {"size": 30, "type": "empty"},
"no_exit": {"size": 30, "type": "no_exit", "wall_prob": 0.3},
}
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"AStar": AStarStrategy()
}
results = []
for maze_name, config in maze_configs.items():
size = config["size"]
maze_type = config["type"]
if maze_type == "empty":
maze = create_empty_maze(size)
elif maze_type == "no_exit":
maze = create_no_exit_maze(size, config.get("wall_prob", 0.3))
else:
maze = create_maze_with_walls(size, config.get("wall_prob", 0.3))
for strat_name, strategy in strategies.items():
solver = MazeSolver(maze, strategy)
times, visited_list, lengths = [], [], []
for _ in range(7):
stats = solver.solve()
times.append(stats.time_ms)
visited_list.append(stats.visited_cells)
lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visited_list) / len(visited_list)
avg_length = sum(lengths) / len(lengths)
results.append([
maze_name, strat_name,
round(avg_time, 4),
int(avg_visited),
int(avg_length)
])
os.makedirs("results", exist_ok=True)
csv_path = "results/results.csv"
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"])
writer.writerows(results)
df = pd.read_csv(csv_path)
plt.figure(figsize=(12, 6))
for strat in df["стратегия"].unique():
subset = df[df["стратегия"] == strat]
plt.plot(subset["лабиринт"], subset["время_мс"], marker='o', label=strat)
plt.title("Сравнение времени работы алгоритмов")
plt.xlabel("Лабиринт")
plt.ylabel("Время (мс)")
plt.legend()
plt.grid(True)
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("results/time_comparison.png")
plt.close()
plt.figure(figsize=(12, 6))
for strat in df["стратегия"].unique():
subset = df[df["стратегия"] == strat]
plt.plot(subset["лабиринт"], subset["посещено_клеток"], marker='o', label=strat)
plt.title("Количество посещённых клеток")
plt.xlabel("Лабиринт")
plt.ylabel("Посещено клеток")
plt.legend()
plt.grid(True)
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("results/visited_comparison.png")
plt.close()
if __name__ == "__main__":
run_experiment()

BIN
Ezhovnd/benchmark_chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

367
Ezhovnd/phonebook.py Normal file
View File

@ -0,0 +1,367 @@
# ============================================================
# Задание 1 — структуры данных: телефонный справочник
# Процедурная парадигма (без классов)
# ============================================================
import random
import time
import csv
# ============================================================
# 1. СВЯЗНЫЙ СПИСОК
# ============================================================
def ll_new_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
"""Добавить или обновить запись. Возвращает новую голову."""
node = head
while node is not None:
if node['name'] == name:
node['phone'] = phone # обновить существующий
return head
node = node['next']
# Не нашли — вставляем в начало
new_node = ll_new_node(name, phone)
new_node['next'] = head
return new_node
def ll_find(head, name):
"""Найти телефон по имени. Возвращает телефон или None."""
node = head
while node is not None:
if node['name'] == name:
return node['phone']
node = node['next']
return None
def ll_delete(head, name):
"""Удалить запись. Возвращает новую голову."""
if head is None:
return None
if head['name'] == name:
return head['next']
prev = head
node = head['next']
while node is not None:
if node['name'] == name:
prev['next'] = node['next']
return head
prev = node
node = node['next']
return head # не нашли — без изменений
def ll_list_all(head):
"""Собрать все записи и вернуть отсортированный список (name, phone)."""
result = []
node = head
while node is not None:
result.append((node['name'], node['phone']))
node = node['next']
result.sort(key=lambda x: x[0])
return result
# ============================================================
# 2. ХЕШ-ТАБЛИЦА (на основе связных списков)
# ============================================================
def ht_new(size=1024):
"""Создать пустую хеш-таблицу (список бакетов)."""
return [None] * size
def ht_hash(buckets, name):
"""Полиномиальный хеш строки по модулю размера таблицы."""
h = 0
for ch in name:
h = (h * 31 + ord(ch)) % len(buckets)
return h
def ht_insert(buckets, name, phone):
idx = ht_hash(buckets, name)
buckets[idx] = ll_insert(buckets[idx], name, phone)
def ht_find(buckets, name):
idx = ht_hash(buckets, name)
return ll_find(buckets[idx], name)
def ht_delete(buckets, name):
idx = ht_hash(buckets, name)
buckets[idx] = ll_delete(buckets[idx], name)
def ht_list_all(buckets):
result = []
for bucket in buckets:
node = bucket
while node is not None:
result.append((node['name'], node['phone']))
node = node['next']
result.sort(key=lambda x: x[0])
return result
# ============================================================
# 3. ДВОИЧНОЕ ДЕРЕВО ПОИСКА (BST)
# ============================================================
def bst_new_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
"""Вставить или обновить запись (итеративно). Возвращает новый корень."""
new_node = bst_new_node(name, phone)
if root is None:
return new_node
node = root
while True:
if name < node['name']:
if node['left'] is None:
node['left'] = new_node
break
node = node['left']
elif name > node['name']:
if node['right'] is None:
node['right'] = new_node
break
node = node['right']
else:
node['phone'] = phone # обновить
break
return root
def bst_find(root, name):
"""Найти телефон. Возвращает телефон или None."""
while root is not None:
if name < root['name']:
root = root['left']
elif name > root['name']:
root = root['right']
else:
return root['phone']
return None
def _bst_min_node(node):
"""Найти узел с минимальным ключом в поддереве."""
while node['left'] is not None:
node = node['left']
return node
def bst_delete(root, name):
"""Удалить узел (итеративно). Возвращает новый корень."""
parent = None
node = root
is_left = False
# Найти узел и его родителя
while node is not None and node['name'] != name:
parent = node
if name < node['name']:
node = node['left']
is_left = True
else:
node = node['right']
is_left = False
if node is None:
return root # не нашли
# Удалить найденный узел
if node['left'] is None:
replacement = node['right']
elif node['right'] is None:
replacement = node['left']
else:
# Два потомка: найти in-order successor
succ_parent = node
succ = node['right']
while succ['left'] is not None:
succ_parent = succ
succ = succ['left']
node['name'] = succ['name']
node['phone'] = succ['phone']
# Удалить successor
if succ_parent is node:
succ_parent['right'] = succ['right']
else:
succ_parent['left'] = succ['right']
return root
if parent is None:
return replacement
if is_left:
parent['left'] = replacement
else:
parent['right'] = replacement
return root
def bst_list_all(root):
"""Центрированный обход (итеративно) — возвращает записи в порядке ключей."""
result = []
stack = []
node = root
while stack or node is not None:
while node is not None:
stack.append(node)
node = node['left']
node = stack.pop()
result.append((node['name'], node['phone']))
node = node['right']
return result
# ============================================================
# ГЕНЕРАЦИЯ ТЕСТОВЫХ ДАННЫХ
# ============================================================
def generate_records(n=10000, seed=42):
rng = random.Random(seed)
records = []
for i in range(n):
name = f"User_{i:05d}"
phone = f"+7{rng.randint(9000000000, 9999999999)}"
records.append((name, phone))
return records
# ============================================================
# БЕНЧМАРК
# ============================================================
REPEATS = 5
def bench_insert(structure_type, records):
times = []
for _ in range(REPEATS):
if structure_type == 'LinkedList':
head = None
start = time.perf_counter()
for name, phone in records:
head = ll_insert(head, name, phone)
times.append(time.perf_counter() - start)
elif structure_type == 'HashTable':
buckets = ht_new()
start = time.perf_counter()
for name, phone in records:
ht_insert(buckets, name, phone)
times.append(time.perf_counter() - start)
elif structure_type == 'BST':
root = None
start = time.perf_counter()
for name, phone in records:
root = bst_insert(root, name, phone)
times.append(time.perf_counter() - start)
return times
def bench_find(structure, structure_type, search_names):
times = []
for _ in range(REPEATS):
start = time.perf_counter()
if structure_type == 'LinkedList':
for name in search_names:
ll_find(structure, name)
elif structure_type == 'HashTable':
for name in search_names:
ht_find(structure, name)
elif structure_type == 'BST':
for name in search_names:
bst_find(structure, name)
times.append(time.perf_counter() - start)
return times
def bench_delete(structure, structure_type, delete_names):
times = []
for _ in range(REPEATS):
if structure_type == 'LinkedList':
s = structure # head
start = time.perf_counter()
for name in delete_names:
s = ll_delete(s, name)
times.append(time.perf_counter() - start)
elif structure_type == 'HashTable':
start = time.perf_counter()
for name in delete_names:
ht_delete(structure, name)
times.append(time.perf_counter() - start)
elif structure_type == 'BST':
s = structure # root
start = time.perf_counter()
for name in delete_names:
s = bst_delete(s, name)
times.append(time.perf_counter() - start)
return times
def build_structure(structure_type, records):
if structure_type == 'LinkedList':
head = None
for name, phone in records:
head = ll_insert(head, name, phone)
return head
elif structure_type == 'HashTable':
buckets = ht_new()
for name, phone in records:
ht_insert(buckets, name, phone)
return buckets
elif structure_type == 'BST':
root = None
for name, phone in records:
root = bst_insert(root, name, phone)
return root
def run_all_benchmarks(n=10000):
rng = random.Random(99)
records_shuffled = generate_records(n)
random.Random(7).shuffle(records_shuffled)
records_sorted = sorted(generate_records(n), key=lambda x: x[0])
all_names = [r[0] for r in generate_records(n)]
search_names = rng.choices(all_names, k=100) + [f"None_{i}" for i in range(10)]
delete_names = rng.choices(all_names, k=50)
structures = ['LinkedList', 'HashTable', 'BST']
modes = [('shuffled', records_shuffled), ('sorted', records_sorted)]
rows = [["Structure", "Mode", "Operation", "Run", "Time_sec"]]
for struct in structures:
for mode_name, records in modes:
print(f" [{struct}] [{mode_name}] insert...", flush=True)
ins_times = bench_insert(struct, records)
for i, t in enumerate(ins_times):
rows.append([struct, mode_name, 'insert', i + 1, round(t, 6)])
# Построить структуру один раз для find/delete
print(f" [{struct}] [{mode_name}] building structure for find/delete...", flush=True)
s = build_structure(struct, records)
print(f" [{struct}] [{mode_name}] find...", flush=True)
find_times = bench_find(s, struct, search_names)
for i, t in enumerate(find_times):
rows.append([struct, mode_name, 'find', i + 1, round(t, 6)])
print(f" [{struct}] [{mode_name}] delete...", flush=True)
del_times = bench_delete(s, struct, delete_names)
for i, t in enumerate(del_times):
rows.append([struct, mode_name, 'delete', i + 1, round(t, 6)])
return rows
if __name__ == '__main__':
import sys
N = int(sys.argv[1]) if len(sys.argv) > 1 else 10000
print(f"Running benchmarks with N={N}, {REPEATS} repeats each...")
rows = run_all_benchmarks(N)
out_path = '/home/claude/docs/data/results.csv'
import os; os.makedirs('/home/claude/docs/data', exist_ok=True)
with open(out_path, 'w', newline='', encoding='utf-8') as f:
csv.writer(f).writerows(rows)
print(f"Results saved to {out_path}")
print(f"Total rows: {len(rows) - 1}")

142
Ezhovnd/report.md Normal file
View File

@ -0,0 +1,142 @@
# Задание 1 — Структуры данных: телефонный справочник
## Цель работы
Реализовать три структуры данных «с нуля» в процедурной парадигме (без классов):
- **Связный список** (LinkedList)
- **Хеш-таблица** (HashTable)
- **Двоичное дерево поиска** (BST)
Применить их для хранения записей телефонного справочника и экспериментально сравнить производительность.
---
## Реализация
Все структуры реализованы в файле `phonebook.py`. Узлы представлены Python-словарями.
### 1. Связный список
Узел: `{'name': str, 'phone': str, 'next': None | dict}`
| Функция | Сложность | Описание |
|---|---|---|
| `ll_insert(head, name, phone)` | O(n) | Поиск по списку + вставка в начало |
| `ll_find(head, name)` | O(n) | Линейный поиск |
| `ll_delete(head, name)` | O(n) | Поиск предшественника + удаление |
| `ll_list_all(head)` | O(n log n) | Сборка + сортировка |
**Особенность:** вставка проверяет дубликаты, проходя весь список — O(n). При вставке в начало без проверки было бы O(1), но появились бы дубли.
### 2. Хеш-таблица
Хранится как список из 1024 бакетов, каждый — голова связного списка. Хеш-функция — полиномиальная (база 31).
| Функция | Средняя сложность | Описание |
|---|---|---|
| `ht_insert` | O(1) | Хеш → вызов ll_insert в бакете |
| `ht_find` | O(1) | Хеш → вызов ll_find |
| `ht_delete` | O(1) | Хеш → вызов ll_delete |
| `ht_list_all` | O(n log n) | Обход всех бакетов + сортировка |
### 3. Двоичное дерево поиска (BST)
Узел: `{'name': str, 'phone': str, 'left': None | dict, 'right': None | dict}`
Все операции реализованы **итеративно** — чтобы избежать `RecursionError` при вырожденном дереве (10 000 уровней на отсортированных данных).
| Функция | Средняя / Худший случай |
|---|---|
| `bst_insert` | O(log n) / O(n) |
| `bst_find` | O(log n) / O(n) |
| `bst_delete` | O(log n) / O(n) |
| `bst_list_all` | O(n) всегда (in-order стек) |
---
## Экспериментальная часть
### Условия эксперимента
- **N = 10 000** записей в справочнике (имена вида `User_00000 … User_09999`)
- Два режима входных данных: **случайный** (shuffled) и **отсортированный** (sorted)
- **5 повторов** каждого замера; в CSV сохранены все замеры + среднее
- Поиск: 100 существующих + 10 отсутствующих имён (110 вызовов)
- Удаление: 50 случайных существующих имён
### Результаты (среднее из 5 повторов, секунды)
| Структура | Режим | Вставка 10 000 | Поиск 110 | Удаление 50 |
|---|---|---:|---:|---:|
| LinkedList | shuffled | **2.951** | 0.03205 | 0.02936 |
| LinkedList | sorted | 1.945 | 0.02199 | 0.02032 |
| HashTable | shuffled | 0.02342 | 0.000182 | 0.000133 |
| HashTable | sorted | **0.01845** | 0.000179 | 0.000130 |
| BST | shuffled | 0.02152 | **0.000143** | **0.000077** |
| BST | sorted | **4.640** | 0.04080 | 0.02649 |
### График
![Benchmark Chart](data/benchmark_chart.png)
*(логарифмическая шкала Y; заштрихованные столбцы — отсортированный режим)*
---
## Анализ результатов
### Вставка
**Связный список** работает медленнее всех: при каждой вставке нужно пройти весь список (~5000 шагов в среднем), чтобы проверить дубликаты. 10 000 вставок → ~50 млн шагов. Это объясняет 23 секунды.
Интересно, что на **отсортированных** данных связный список чуть **быстрее**: вставляемые имена идут по алфавиту, поэтому при проверке дубликатов мы чаще «натыкаемся» на нужный узел ближе к началу (список хранит элементы в порядке вставки — в начале самые свежие, то есть самые «поздние» по алфавиту). Это случайный эффект раскладки в памяти.
**Хеш-таблица** практически **не чувствительна к порядку** (~0.02 сек в обоих режимах). Хеш-функция равномерно распределяет ключи по бакетам — порядок обхода записей не меняет время доступа.
**BST на случайных данных** (~0.02 сек) — сопоставим с хеш-таблицей. Случайные ключи строят сбалансированное дерево высотой ~log₂(10000) ≈ 13, поиск пути к каждому узлу быстрый.
**BST на отсортированных данных** — катастрофа: **4.6 сек**. Каждый новый ключ больше предыдущего, поэтому дерево вырождается в правый «связный список» глубиной 10 000. Вставка i-го элемента требует i шагов → суммарно O(n²). Это и есть теоретически предсказанная деградация BST. (Дополнительно: рекурсивная реализация упала бы с `RecursionError` — потому в финальном коде используется итеративная версия.)
### Поиск
**Связный список** — самый медленный (0.03 сек на 110 запросов). Линейный поиск O(n): в среднем 5000 шагов на каждый запрос.
**Хеш-таблица** — почти мгновенно (0.00018 сек). O(1) в среднем: вычислить хеш → проверить 12 узла в бакете.
**BST (случайные данные)** — самый быстрый среди неотсортированных режимов (0.000143 сек): O(log n) ≈ 13 шагов.
**BST (отсортированные данные)** деградирует до O(n) поиска (0.04 сек ≈ уровень связного списка).
### Удаление
Картина аналогична поиску: удаление требует сначала найти элемент.
- **LinkedList**: ~0.020.03 сек на 50 удалений — O(n) каждое.
- **HashTable**: ~0.00013 сек — O(1).
- **BST (shuffled)**: **0.000077 сек** — самый быстрый (O(log n), плюс операция замены на successor тоже быстрая в сбалансированном дереве).
- **BST (sorted)**: 0.026 сек — вырожденное дерево, O(n) удаление.
---
## Выводы: какую структуру выбирать?
| Задача | Рекомендация | Почему |
|---|---|---|
| **Частые поиски / обновления** без нужды в порядке | **Хеш-таблица** | O(1) поиск, нечувствительность к порядку данных |
| **Данные в отсортированном порядке** (range-запросы, list_all) | **Сбалансированный BST** (AVL, красно-чёрное дерево) | In-order обход = O(n), поиск O(log n) |
| **BST из коробки** (наша реализация) | Только для **случайных** данных! | Деградирует до O(n²) на отсортированных |
| **Связный список** | Почти никогда для справочника | O(n) поиск и вставка — медленнее на порядки |
| **Связный список уместен** | Очень маленький справочник (<100 записей) или LIFO/FIFO-очередь | Простота реализации, O(1) вставка в начало/конец |
**Главный вывод:** для телефонного справочника с 10 000+ записей хеш-таблица — лучший выбор при произвольном доступе. BST побеждает, когда нужен отсортированный вывод и данные приходят в случайном порядке (или используется самобалансирующийся BST). Простой связный список — проигрышная стратегия при любом n > нескольких сотен.
---
## Файлы
| Файл | Описание |
|---|---|
| `phonebook.py` | Исходный код: структуры данных + бенчмарк |
| `docs/data/results.csv` | Сырые замеры (90 строк, 5 повторов × 3 структуры × 2 режима × 3 операции) |
| `docs/data/benchmark_chart.png` | График сравнения (логарифмическая шкала) |
| `docs/report.md` | Этот отчёт |

91
Ezhovnd/results.csv Normal file
View File

@ -0,0 +1,91 @@
Structure,Mode,Operation,Run,Time_sec
LinkedList,shuffled,insert,1,2.818623
LinkedList,shuffled,insert,2,2.776622
LinkedList,shuffled,insert,3,2.932603
LinkedList,shuffled,insert,4,3.150283
LinkedList,shuffled,insert,5,3.077142
LinkedList,shuffled,find,1,0.032263
LinkedList,shuffled,find,2,0.0328
LinkedList,shuffled,find,3,0.03149
LinkedList,shuffled,find,4,0.031703
LinkedList,shuffled,find,5,0.031978
LinkedList,shuffled,delete,1,0.017758
LinkedList,shuffled,delete,2,0.032563
LinkedList,shuffled,delete,3,0.032256
LinkedList,shuffled,delete,4,0.032048
LinkedList,shuffled,delete,5,0.032178
LinkedList,sorted,insert,1,1.833796
LinkedList,sorted,insert,2,1.836535
LinkedList,sorted,insert,3,2.088518
LinkedList,sorted,insert,4,1.868659
LinkedList,sorted,insert,5,2.096095
LinkedList,sorted,find,1,0.021973
LinkedList,sorted,find,2,0.021922
LinkedList,sorted,find,3,0.022304
LinkedList,sorted,find,4,0.02182
LinkedList,sorted,find,5,0.021948
LinkedList,sorted,delete,1,0.008794
LinkedList,sorted,delete,2,0.021849
LinkedList,sorted,delete,3,0.021325
LinkedList,sorted,delete,4,0.021476
LinkedList,sorted,delete,5,0.028157
HashTable,shuffled,insert,1,0.022874
HashTable,shuffled,insert,2,0.021475
HashTable,shuffled,insert,3,0.019544
HashTable,shuffled,insert,4,0.023367
HashTable,shuffled,insert,5,0.029821
HashTable,shuffled,find,1,0.000252
HashTable,shuffled,find,2,0.000152
HashTable,shuffled,find,3,0.000143
HashTable,shuffled,find,4,0.000216
HashTable,shuffled,find,5,0.000147
HashTable,shuffled,delete,1,0.000208
HashTable,shuffled,delete,2,0.000132
HashTable,shuffled,delete,3,0.000112
HashTable,shuffled,delete,4,0.000106
HashTable,shuffled,delete,5,0.000106
HashTable,sorted,insert,1,0.020814
HashTable,sorted,insert,2,0.017296
HashTable,sorted,insert,3,0.016897
HashTable,sorted,insert,4,0.016796
HashTable,sorted,insert,5,0.020424
HashTable,sorted,find,1,0.000285
HashTable,sorted,find,2,0.000152
HashTable,sorted,find,3,0.00015
HashTable,sorted,find,4,0.00015
HashTable,sorted,find,5,0.000157
HashTable,sorted,delete,1,0.000182
HashTable,sorted,delete,2,0.000156
HashTable,sorted,delete,3,0.000104
HashTable,sorted,delete,4,0.000103
HashTable,sorted,delete,5,0.000103
BST,shuffled,insert,1,0.024171
BST,shuffled,insert,2,0.021608
BST,shuffled,insert,3,0.025746
BST,shuffled,insert,4,0.017484
BST,shuffled,insert,5,0.018589
BST,shuffled,find,1,0.000218
BST,shuffled,find,2,0.000122
BST,shuffled,find,3,0.000139
BST,shuffled,find,4,0.000117
BST,shuffled,find,5,0.000121
BST,shuffled,delete,1,0.000108
BST,shuffled,delete,2,7.3e-05
BST,shuffled,delete,3,6.8e-05
BST,shuffled,delete,4,6.8e-05
BST,shuffled,delete,5,6.8e-05
BST,sorted,insert,1,4.498011
BST,sorted,insert,2,4.60869
BST,sorted,insert,3,4.558911
BST,sorted,insert,4,4.703444
BST,sorted,insert,5,4.830993
BST,sorted,find,1,0.04103
BST,sorted,find,2,0.039717
BST,sorted,find,3,0.040988
BST,sorted,find,4,0.0401
BST,sorted,find,5,0.042171
BST,sorted,delete,1,0.026369
BST,sorted,delete,2,0.025809
BST,sorted,delete,3,0.027687
BST,sorted,delete,4,0.025919
BST,sorted,delete,5,0.026676
1 Structure Mode Operation Run Time_sec
2 LinkedList shuffled insert 1 2.818623
3 LinkedList shuffled insert 2 2.776622
4 LinkedList shuffled insert 3 2.932603
5 LinkedList shuffled insert 4 3.150283
6 LinkedList shuffled insert 5 3.077142
7 LinkedList shuffled find 1 0.032263
8 LinkedList shuffled find 2 0.0328
9 LinkedList shuffled find 3 0.03149
10 LinkedList shuffled find 4 0.031703
11 LinkedList shuffled find 5 0.031978
12 LinkedList shuffled delete 1 0.017758
13 LinkedList shuffled delete 2 0.032563
14 LinkedList shuffled delete 3 0.032256
15 LinkedList shuffled delete 4 0.032048
16 LinkedList shuffled delete 5 0.032178
17 LinkedList sorted insert 1 1.833796
18 LinkedList sorted insert 2 1.836535
19 LinkedList sorted insert 3 2.088518
20 LinkedList sorted insert 4 1.868659
21 LinkedList sorted insert 5 2.096095
22 LinkedList sorted find 1 0.021973
23 LinkedList sorted find 2 0.021922
24 LinkedList sorted find 3 0.022304
25 LinkedList sorted find 4 0.02182
26 LinkedList sorted find 5 0.021948
27 LinkedList sorted delete 1 0.008794
28 LinkedList sorted delete 2 0.021849
29 LinkedList sorted delete 3 0.021325
30 LinkedList sorted delete 4 0.021476
31 LinkedList sorted delete 5 0.028157
32 HashTable shuffled insert 1 0.022874
33 HashTable shuffled insert 2 0.021475
34 HashTable shuffled insert 3 0.019544
35 HashTable shuffled insert 4 0.023367
36 HashTable shuffled insert 5 0.029821
37 HashTable shuffled find 1 0.000252
38 HashTable shuffled find 2 0.000152
39 HashTable shuffled find 3 0.000143
40 HashTable shuffled find 4 0.000216
41 HashTable shuffled find 5 0.000147
42 HashTable shuffled delete 1 0.000208
43 HashTable shuffled delete 2 0.000132
44 HashTable shuffled delete 3 0.000112
45 HashTable shuffled delete 4 0.000106
46 HashTable shuffled delete 5 0.000106
47 HashTable sorted insert 1 0.020814
48 HashTable sorted insert 2 0.017296
49 HashTable sorted insert 3 0.016897
50 HashTable sorted insert 4 0.016796
51 HashTable sorted insert 5 0.020424
52 HashTable sorted find 1 0.000285
53 HashTable sorted find 2 0.000152
54 HashTable sorted find 3 0.00015
55 HashTable sorted find 4 0.00015
56 HashTable sorted find 5 0.000157
57 HashTable sorted delete 1 0.000182
58 HashTable sorted delete 2 0.000156
59 HashTable sorted delete 3 0.000104
60 HashTable sorted delete 4 0.000103
61 HashTable sorted delete 5 0.000103
62 BST shuffled insert 1 0.024171
63 BST shuffled insert 2 0.021608
64 BST shuffled insert 3 0.025746
65 BST shuffled insert 4 0.017484
66 BST shuffled insert 5 0.018589
67 BST shuffled find 1 0.000218
68 BST shuffled find 2 0.000122
69 BST shuffled find 3 0.000139
70 BST shuffled find 4 0.000117
71 BST shuffled find 5 0.000121
72 BST shuffled delete 1 0.000108
73 BST shuffled delete 2 7.3e-05
74 BST shuffled delete 3 6.8e-05
75 BST shuffled delete 4 6.8e-05
76 BST shuffled delete 5 6.8e-05
77 BST sorted insert 1 4.498011
78 BST sorted insert 2 4.60869
79 BST sorted insert 3 4.558911
80 BST sorted insert 4 4.703444
81 BST sorted insert 5 4.830993
82 BST sorted find 1 0.04103
83 BST sorted find 2 0.039717
84 BST sorted find 3 0.040988
85 BST sorted find 4 0.0401
86 BST sorted find 5 0.042171
87 BST sorted delete 1 0.026369
88 BST sorted delete 2 0.025809
89 BST sorted delete 3 0.027687
90 BST sorted delete 4 0.025919
91 BST sorted delete 5 0.026676

View File

@ -1,35 +0,0 @@
ОТЧЁТ ПО ЗАДАНИЮ 1
1. Влияние порядка данных на BST
При случайном порядке данных BST работает быстро (вставка ~0.005 сек).
При отсортированном порядке дерево вырождается в цепочку, и время вставки
возрастает примерно в 5060 раз (~0.31 сек). Сложность деградирует с O(log n) до O(n).
2. Почему хеш-таблица нечувствительна к порядку
Хеш-таблица использует хеш-функцию, которая равномерно распределяет элементы
по бакетам. Поэтому порядок входных данных почти не влияет на скорость
вставки, поиска и удаления (в среднем O(1)).
3. Почему связный список медленен при поиске
Для поиска в связном списке нужно последовательно пройти все элементы.
Поэтому поиск всегда выполняется за O(n), независимо от порядка данных.
Это делает его самым медленным при операциях поиска и удаления.
4. Как работает удаление
- LinkedList: O(n) — нужно найти элемент и перестроить ссылки.
- HashTable: O(1) в среднем — удаление внутри нужного бакета.
- BST: O(log n) в среднем, O(n) в худшем — при двух потомках ищется
минимальный элемент в правом поддереве.
5. Вывод и рекомендации
Рекомендуемые структуры в зависимости от задачи:
- Частые вставки и поиск → HashTable (лучшая общая производительность)
- Нужно получать данные в отсортированном порядке → BST (только при случайных данных)
- Данные приходят отсортированными → HashTable (BST сильно деградирует)
- Малый объём данных и простота → LinkedList
Итог: Для большинства реальных задач лучше всего подходит хеш-таблица.
BST имеет смысл использовать только при случайном порядке данных и
необходимости частого получения отсортированного списка.

View File

@ -1,169 +0,0 @@
Отчёт ко 2 заданию
1. Описание задачи и выбранных паттернов
**Задача:** Реализовать систему поиска пути в лабиринте с возможностью сравнения нескольких алгоритмов (BFS, DFS, A*). Система должна поддерживать разные способы построения лабиринта и позволять легко добавлять новые алгоритмы поиска.
Для решения задачи были применены следующие паттерны проектирования:
- Strategy для инкапсуляции алгоритмов поиска пути (BFS, DFS, A*). Позволяет динамически менять стратегию поиска.
- Builder для построения лабиринта из файла. Отделяет процесс создания лабиринта от его представления.
Эти паттерны обеспечивают гибкость и расширяемость системы.
Диаграмма классов (Mermaid)
```mermaid
classDiagram
class Maze {
+width: int
+height: int
+cells: List~List~Cell~~
+start: Cell
+exit: Cell
+getCell(x, y)
+getNeighbors(cell)
}
class Cell {
+x: int
+y: int
+is_wall: bool
+is_start: bool
+is_exit: bool
+isPassable()
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit)
}
class BFSStrategy {
+findPath(maze, start, exit)
}
class DFSStrategy {
+findPath(maze, start, exit)
}
class AStarStrategy {
+findPath(maze, start, exit)
-heuristic(a, b)
}
class MazeSolver {
-maze: Maze
-strategy: PathFindingStrategy
+setStrategy(strategy)
+solve()
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename)
}
class TextFileMazeBuilder {
+buildFromFile(filename)
}
Maze "1" *-- "many" Cell
MazeSolver --> PathFindingStrategy
PathFindingStrategy <|-- BFSStrategy
PathFindingStrategy <|-- DFSStrategy
PathFindingStrategy <|-- AStarStrategy
MazeBuilder <|-- TextFileMazeBuilder
```
2. Листинги ключевых классов
Ключевые классы (Strategy и MazeSolver):
```python
class PathFindingStrategy:
def findPath(self, maze, start, exit):
raise NotImplementedError
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация BFS
...
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация DFS
...
class AStarStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация A*
...
```
```python
class MazeSolver:
def __init__(self, maze=None, strategy=None):
self.maze = maze
self.strategy = strategy
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if not self.maze or not self.strategy:
return None
# замер времени и вызов стратегии
...
```
Полный код доступен в репозитории (или может быть предоставлен по запросу).
3. Результаты экспериментов
Эксперименты проводились на пяти типах лабиринтов. Ниже представлены ключевые результаты.
Сводная таблица (средние значения):
| Лабиринт | Стратегия | Время (мс) | Посещено клеток | Длина пути |
|-------------------------|-----------|------------|------------------|------------|
| 10x10_simple | BFS | 0.07 | 90 | 37 |
| 10x10_simple | DFS | 0.03 | 67 | 37 |
| 10x10_simple | A* | 0.09 | 76 | 19 |
| 50x50_with_deadends | BFS | 1.29 | 1657 | 0 |
| 50x50_with_deadends | DFS | 0.64 | 993 | 243 |
| 50x50_with_deadends | A* | 0.56 | 440 | 101 |
| 100x100_complex | BFS | 4.40 | 5735 | 1 |
| 100x100_complex | DFS | 4.33 | 5735 | 1 |
| 100x100_complex | A* | 7.03 | 5735 | 1 |
| empty | BFS | 0.68 | 900 | 0 |
| empty | DFS | 0.39 | 900 | 465 |
| empty | A* | 1.04 | 900 | 59 |
Графики (сохранены в папке `results/`):
- `time_comparison.png` сравнение времени работы алгоритмов
- `visited_comparison.png` сравнение количества посещённых клеток
4. Анализ эффективности алгоритмов и применимости паттернов
- **BFS** показывает стабильную работу и находит кратчайший путь, но посещает больше клеток.
- **DFS** быстрее всех на простых и пустых лабиринтах, однако не гарантирует оптимальность.
- **A*** эффективнее всего по количеству посещённых клеток на сложных лабиринтах, но на больших картах проигрывает по времени из-за overhead приоритетной очереди.
Паттерн **Strategy** позволил легко переключаться между алгоритмами без изменения кода `MazeSolver`. Паттерн **Builder** сделал возможным добавление новых источников построения лабиринта (например, генератор случайных лабиринтов) без изменения основной логики.
5. Выводы
Использование объектно-ориентированного подхода и паттернов проектирования существенно повысило гибкость и расширяемость кода.
Преимущества:
- Благодаря паттерну **Strategy** добавление нового алгоритма поиска (например, Dijkstra) требует только реализации интерфейса `PathFindingStrategy` без изменения `MazeSolver`.
- Паттерн **Builder** позволяет легко подключать новые способы загрузки лабиринтов.
- Код стал более читаемым и поддерживаемым.
Что было бы сложно изменить без паттернов:
- Замена алгоритма поиска потребовала бы значительных изменений в классе `MazeSolver` (много условных операторов `if`).
- Добавление нового способа построения лабиринта привело бы к дублированию кода.
- Сравнительный эксперимент было бы гораздо сложнее проводить, так как алгоритмы не были бы унифицированы через общий интерфейс.
Таким образом, применение паттернов Strategy и Builder сделало систему легко расширяемой и удобной для проведения экспериментов.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,258 +0,0 @@
import time
import random
import csv
import os
import matplotlib.pyplot as plt
def ll_insert(head, name, phone):
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
current = head
while current['next']:
current = current['next']
current['next'] = new_node
return head
def ll_find(head, name):
current = head
while current:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
current = head
while current['next']:
if current['next']['name'] == name:
current['next'] = current['next']['next']
return head
current = current['next']
return head
def ll_list_all(head):
result = []
current = head
while current:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result)
def create_hash_table(size=200):
return [None] * size
def ht_insert(buckets, name, phone):
index = hash(name) % len(buckets)
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets, name):
index = hash(name) % len(buckets)
return ll_find(buckets[index], name)
def ht_delete(buckets, name):
index = hash(name) % len(buckets)
buckets[index] = ll_delete(buckets[index], name)
def ht_list_all(buckets):
result = []
for bucket in buckets:
current = bucket
while current:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result)
def bst_insert(root, name, phone):
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = new_node
return root
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = new_node
return root
current = current['right']
else:
current['phone'] = phone
return root
def bst_find(root, name):
current = root
while current:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_delete(root, name):
if root is None:
return None
parent = None
current = root
while current and current['name'] != name:
parent = current
if name < current['name']:
current = current['left']
else:
current = current['right']
if current is None:
return root
if current['left'] is None or current['right'] is None:
child = current['left'] if current['left'] else current['right']
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
else:
parent_min = current
min_node = current['right']
while min_node['left']:
parent_min = min_node
min_node = min_node['left']
current['name'] = min_node['name']
current['phone'] = min_node['phone']
if parent_min['left'] == min_node:
parent_min['left'] = min_node['right']
else:
parent_min['right'] = min_node['right']
return root
def bst_list_all(root):
result = []
def inorder(node):
if node:
inorder(node['left'])
result.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return result
def generate_records(n=10000):
records = [(f"User_{i:05d}", f"8{random.randint(9000000000, 9999999999)}") for i in range(n)]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def run_experiments():
random.seed(42)
records_shuffled, records_sorted = generate_records(10000)
all_results = []
structures = ["LinkedList", "HashTable", "BST"]
modes = [("случайный", records_shuffled), ("отсортированный", records_sorted)]
for mode_name, records in modes:
for struct_name in structures:
print(f"Тестируем: {struct_name} | Режим: {mode_name}")
for run in range(5):
if struct_name == "LinkedList":
data = None
elif struct_name == "HashTable":
data = create_hash_table(200)
else:
data = None
start = time.perf_counter()
for name, phone in records:
if struct_name == "LinkedList":
data = ll_insert(data, name, phone)
elif struct_name == "HashTable":
ht_insert(data, name, phone)
else:
data = bst_insert(data, name, phone)
insert_time = time.perf_counter() - start
test_names = [r[0] for r in random.sample(records, 100)]
test_names += [f"None_{i}" for i in range(10)]
start = time.perf_counter()
for name in test_names:
if struct_name == "LinkedList":
ll_find(data, name)
elif struct_name == "HashTable":
ht_find(data, name)
else:
bst_find(data, name)
find_time = time.perf_counter() - start
delete_names = [r[0] for r in random.sample(records, 50)]
start = time.perf_counter()
for name in delete_names:
if struct_name == "LinkedList":
data = ll_delete(data, name)
elif struct_name == "HashTable":
ht_delete(data, name)
else:
data = bst_delete(data, name)
delete_time = time.perf_counter() - start
all_results.append([struct_name, mode_name, "вставка", run + 1, insert_time])
all_results.append([struct_name, mode_name, "поиск", run + 1, find_time])
all_results.append([struct_name, mode_name, "удаление", run + 1, delete_time])
os.makedirs("docs/data", exist_ok=True)
filepath = "docs/data/results.csv"
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Структура", "Режим", "Операция", "Запуск", "Время (сек)"])
writer.writerows(all_results)
print(f"\nРезультаты сохранены в {filepath}")
return all_results
def plot_results(csv_path="docs/data/results.csv"):
import pandas as pd
df = pd.read_csv(csv_path)
summary = df.groupby(["Структура", "Режим", "Операция"])["Время (сек)"].mean().reset_index()
for op in ["вставка", "поиск", "удаление"]:
op_data = summary[summary["Операция"] == op]
plt.figure(figsize=(10, 6))
x_labels = []
y_values = []
for _, row in op_data.iterrows():
label = f"{row['Структура']}\n({row['Режим']})"
x_labels.append(label)
y_values.append(row["Время (сек)"])
plt.bar(x_labels, y_values, color=['#4C72B0', '#55A868', '#C44E52'] * 2)
plt.title(f"Среднее время операции: {op}")
plt.ylabel("Время (сек)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig(f"docs/data/graph_{op}.png")
print(f"График сохранён: docs/data/graph_{op}.png")
if __name__ == "__main__":
run_experiments()
plot_results()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@ -1,16 +0,0 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
10x10_simple,BFS,0.0646,82,1
10x10_simple,DFS,0.0633,82,1
10x10_simple,AStar,0.0929,82,1
50x50_with_deadends,BFS,1.3632,1687,0
50x50_with_deadends,DFS,0.1943,400,205
50x50_with_deadends,AStar,0.6863,562,101
100x100_complex,BFS,4.8617,6060,1
100x100_complex,DFS,4.6471,6060,1
100x100_complex,AStar,7.4691,6060,1
empty,BFS,0.6954,900,0
empty,DFS,0.4106,900,465
empty,AStar,1.0604,900,59
no_exit,BFS,0.0017,1,1
no_exit,DFS,0.0009,1,1
no_exit,AStar,0.001,1,1
1 лабиринт стратегия время_мс посещено_клеток длина_пути
2 10x10_simple BFS 0.0646 82 1
3 10x10_simple DFS 0.0633 82 1
4 10x10_simple AStar 0.0929 82 1
5 50x50_with_deadends BFS 1.3632 1687 0
6 50x50_with_deadends DFS 0.1943 400 205
7 50x50_with_deadends AStar 0.6863 562 101
8 100x100_complex BFS 4.8617 6060 1
9 100x100_complex DFS 4.6471 6060 1
10 100x100_complex AStar 7.4691 6060 1
11 empty BFS 0.6954 900 0
12 empty DFS 0.4106 900 465
13 empty AStar 1.0604 900 59
14 no_exit BFS 0.0017 1 1
15 no_exit DFS 0.0009 1 1
16 no_exit AStar 0.001 1 1