Merge pull request '[1] 1-st-exercise' (#265) from KuznetsovYuM/2026-rff_mp:1-st-exercise into develop

Reviewed-on: UNN/2026-rff_mp#265
This commit is contained in:
kit8nino 2026-05-30 11:46:57 +00:00
commit 98d32c7cf1
6 changed files with 969 additions and 0 deletions

View File

@ -0,0 +1,278 @@
def linked_list_add(head, name, phone):
curr = head
while curr is not None:
if curr['name'] == name:
curr['phone'] = phone
return head
curr = curr['next']
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
curr = head
while curr['next'] is not None:
curr = curr['next']
curr['next'] = new_node
return head
def linked_list_find(head, name):
curr = head
while curr is not None:
if curr['name'] == name:
return curr['phone']
curr = curr['next']
return None
def linked_list_remove(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
prev = head
curr = head['next']
while curr is not None:
if curr['name'] == name:
prev['next'] = curr['next']
return head
prev = curr
curr = curr['next']
return head
def linked_list_collect_all(head):
records = []
curr = head
while curr is not None:
records.append((curr['name'], curr['phone']))
curr = curr['next']
records.sort(key=lambda pair: pair[0])
return records
#HASH
def _hash_bucket_index(key, table_size):
return hash(key) % table_size
def hash_table_create(bucket_count=10):
return [None] * bucket_count
def hash_table_put(table, name, phone):
idx = _hash_bucket_index(name, len(table))
table[idx] = linked_list_add(table[idx], name, phone)
return table
def hash_table_get(table, name):
idx = _hash_bucket_index(name, len(table))
return linked_list_find(table[idx], name)
def hash_table_remove(table, name):
idx = _hash_bucket_index(name, len(table))
table[idx] = linked_list_remove(table[idx], name)
return table
def hash_table_collect_all(table):
all_records = []
for head in table:
curr = head
while curr is not None:
all_records.append((curr['name'], curr['phone']))
curr = curr['next']
all_records.sort(key=lambda pair: pair[0])
return all_records
#BST
def _bst_new_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_add(root, name, phone):
"""Insert or update. Returns (possibly new) root."""
if root is None:
return _bst_new_node(name, phone)
if name == root['name']:
root['phone'] = phone
elif name < root['name']:
root['left'] = bst_add(root['left'], name, phone)
else:
root['right'] = bst_add(root['right'], name, phone)
return root
def bst_find(root, name):
if root is None:
return None
if name == root['name']:
return root['phone']
elif name < root['name']:
return bst_find(root['left'], name)
else:
return bst_find(root['right'], name)
def _bst_find_minimum(node):
while node['left'] is not None:
node = node['left']
return node
def bst_remove(root, name):
if root is None:
return None
if name < root['name']:
root['left'] = bst_remove(root['left'], name)
elif name > root['name']:
root['right'] = bst_remove(root['right'], name)
else:
if root['left'] is None:
return root['right']
if root['right'] is None:
return root['left']
successor = _bst_find_minimum(root['right'])
root['name'] = successor['name']
root['phone'] = successor['phone']
root['right'] = bst_remove(root['right'], successor['name'])
return root
def bst_collect_inorder(root):
result = []
def inorder(node):
if node is None:
return
inorder(node['left'])
result.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return result
#Benchmarking
import random
import time
import csv
import os
import sys
sys.setrecursionlimit(20000)
def generate_test_data(n, seed=42):
random.seed(seed)
records = []
for i in range(1, n+1):
name = f"User_{i:05d}"
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
records.append((name, phone))
return records
def prepare_ordered_and_shuffled(records):
shuffled = records.copy()
random.shuffle(shuffled)
sorted_records = sorted(records, key=lambda x: x[0])
return shuffled, sorted_records
def measure_operations(struct_ops, records, mode_name, repeats=5):
results = []
for rep in range(repeats):
ds = struct_ops['create']()
start = time.perf_counter()
for name, phone in records:
ds = struct_ops['insert'](ds, name, phone)
insert_time = time.perf_counter() - start
existing_names = [name for name, _ in records]
sample_existing = random.sample(existing_names, 100)
nonexistent = [f"Missing_{i}" for i in range(10)]
search_names = sample_existing + nonexistent
random.shuffle(search_names)
start = time.perf_counter()
for name in search_names:
struct_ops['find'](ds, name)
find_time = time.perf_counter() - start
to_delete = random.sample(existing_names, 50)
start = time.perf_counter()
for name in to_delete:
ds = struct_ops['delete'](ds, name)
delete_time = time.perf_counter() - start
results.append({
'structure': struct_ops['name'],
'mode': mode_name,
'repetition': rep+1,
'insert_time': insert_time,
'find_time': find_time,
'delete_time': delete_time
})
return results
def run_full_benchmark():
N = 10000
base_records = generate_test_data(N)
shuffled, sorted_records = prepare_ordered_and_shuffled(base_records)
structures = {
'LinkedList': {
'name': 'LinkedList',
'create': lambda: None,
'insert': linked_list_add,
'find': linked_list_find,
'delete': linked_list_remove,
},
'HashTable': {
'name': 'HashTable',
'create': lambda: hash_table_create(100),
'insert': hash_table_put,
'find': hash_table_get,
'delete': hash_table_remove,
},
'BST': {
'name': 'BST',
'create': lambda: None,
'insert': bst_add,
'find': bst_find,
'delete': bst_remove,
}
}
all_results = []
for name, ops in structures.items():
print(f"Benchmarking {name} on random order...")
all_results.extend(measure_operations(ops, shuffled, 'random', repeats=5))
print(f"Benchmarking {name} on sorted order...")
all_results.extend(measure_operations(ops, sorted_records, 'sorted', repeats=5))
os.makedirs('docs/data', exist_ok=True)
csv_path = 'docs/data/experiment_results.csv'
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
for r in all_results:
writer.writerow([
r['structure'],
r['mode'],
r['repetition'],
f"{r['insert_time']:.6f}",
f"{r['find_time']:.6f}",
f"{r['delete_time']:.6f}"
])
print(f"Experiment finished. Results saved to {csv_path}")
if __name__ == '__main__':
run_full_benchmark()

View File

@ -0,0 +1,45 @@
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import os
csv_path = 'experiment_results.csv'
if not os.path.exists(csv_path):
print("Run phonebook_structures.py first to generate results.")
exit(1)
df = pd.read_csv(csv_path)
mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index()
structures = mean_times['Structure'].unique()
modes = mean_times['Mode'].unique()
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)']
titles = ['Insertion', 'Search', 'Deletion']
for ax, op, title in zip(axes, operations, titles):
x = np.arange(len(structures))
width = 0.35
random_vals = []
sorted_vals = []
for s in structures:
random_row = mean_times[(mean_times['Structure']==s) & (mean_times['Mode']=='random')]
sorted_row = mean_times[(mean_times['Structure']==s) & (mean_times['Mode']=='sorted')]
random_vals.append(random_row[op].values[0] if not random_row.empty else 0)
sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0)
ax.bar(x - width/2, random_vals, width, label='Random order')
ax.bar(x + width/2, sorted_vals, width, label='Sorted order')
ax.set_xticks(x)
ax.set_xticklabels(structures)
ax.set_ylabel('Time (seconds)')
ax.set_title(title)
ax.legend()
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150)
plt.show()
print("Graph saved to performance_comparison.png")

View File

@ -0,0 +1,505 @@
import sys
from collections import deque
import heapq
import time
import os
class Tile:
def __init__(self, column, row):
self._col = column
self._row = row
self._blocked = False
self._is_start = False
self._is_exit = False
@property
def col(self):
return self._col
@property
def row(self):
return self._row
@property
def blocked(self):
return self._blocked
@blocked.setter
def blocked(self, value):
self._blocked = value
@property
def is_start(self):
return self._is_start
@is_start.setter
def is_start(self, value):
self._is_start = value
@property
def is_exit(self):
return self._is_exit
@is_exit.setter
def is_exit(self, value):
self._is_exit = value
def passable(self):
return not self._blocked
class Labyrinth:
def __init__(self, width, height):
self._width = width
self._height = height
self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)]
self._start_tile = None
self._exit_tile = None
@property
def width(self):
return self._width
@property
def height(self):
return self._height
@property
def start_tile(self):
return self._start_tile
@property
def exit_tile(self):
return self._exit_tile
def get_tile(self, x, y):
if 0 <= x < self._width and 0 <= y < self._height:
return self._grid[y][x]
return None
def set_tile_type(self, x, y, kind):
tile = self.get_tile(x, y)
if tile is None:
return
if kind == 'wall':
tile.blocked = True
elif kind == 'start':
if self._start_tile:
self._start_tile.is_start = False
tile.is_start = True
tile.blocked = False
self._start_tile = tile
elif kind == 'exit':
if self._exit_tile:
self._exit_tile.is_exit = False
tile.is_exit = True
tile.blocked = False
self._exit_tile = tile
elif kind == 'path':
tile.blocked = False
def neighbors_of(self, tile):
result = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
for dx, dy in directions:
nx, ny = tile.col + dx, tile.row + dy
nb = self.get_tile(nx, ny)
if nb and nb.passable():
result.append(nb)
return result
class LabyrinthLoader:
def load(self, filepath):
raise NotImplementedError
class TextFileLoader(LabyrinthLoader):
def load(self, filepath):
with open(filepath, 'r') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h > 0 else 0
start_count = 0
exit_count = 0
lab = Labyrinth(w, h)
for row, line in enumerate(lines):
for col, ch in enumerate(line):
if ch == "#":
lab.set_tile_type(col, row, "wall")
elif ch == "S":
lab.set_tile_type(col, row, "start")
start_count += 1
elif ch == "E":
lab.set_tile_type(col, row, "exit")
exit_count += 1
else:
lab.set_tile_type(col, row, "path")
if start_count != 1 or exit_count != 1:
raise ValueError(f"Maze must have exactly one 'S' and one 'E'. Found: S={start_count}, E={exit_count}")
return lab
class SearchAlgorithm:
def find_route(self, maze, start, goal):
raise NotImplementedError
def _reconstruct(self, came_from, start, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = came_from.get(cur)
path.reverse()
return path
def visited_cells(self):
return getattr(self, '_visited', 0)
class BreadthFirstSearch(SearchAlgorithm):
def find_route(self, maze, start, goal):
q = deque()
q.append(start)
parent = {start: None}
seen = {start}
while q:
current = q.popleft()
if current == goal:
self._visited = len(seen)
return self._reconstruct(parent, start, goal)
for nb in maze.neighbors_of(current):
if nb not in seen:
seen.add(nb)
parent[nb] = current
q.append(nb)
self._visited = len(seen)
return []
class DepthFirstSearch(SearchAlgorithm):
def find_route(self, maze, start, goal):
stack = [start]
parent = {start: None}
seen = {start}
while stack:
current = stack.pop()
if current == goal:
self._visited = len(seen)
return self._reconstruct(parent, start, goal)
for nb in maze.neighbors_of(current):
if nb not in seen:
seen.add(nb)
parent[nb] = current
stack.append(nb)
self._visited = len(seen)
return []
class AStarSearch(SearchAlgorithm):
def _heuristic(self, tile, goal):
return abs(tile.col - goal.col) + abs(tile.row - goal.row)
def find_route(self, maze, start, goal):
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g = {start: 0}
f = {start: start_f}
closed = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
closed.add(cur)
if cur == goal:
self._visited = len(closed)
return self._reconstruct(parent, start, goal)
if cur_f > f.get(cur, float('inf')):
continue
for nb in maze.neighbors_of(cur):
tentative_g = g[cur] + 1
if tentative_g < g.get(nb, float('inf')):
parent[nb] = cur
g[nb] = tentative_g
new_f = tentative_g + self._heuristic(nb, goal)
f[nb] = new_f
heapq.heappush(heap, (new_f, counter, nb))
counter += 1
self._visited = len(closed)
return []
class SearchStats:
def __init__(self, elapsed_ms, visited, path_len):
self.elapsed_ms = elapsed_ms
self.visited_cells = visited
self.path_length = path_len
class EventListener:
def on_event(self, event_type, data):
raise NotImplementedError
class TerminalView(EventListener):
def __init__(self, player=None):
self._current_path = None
self._player = player
def on_event(self, event_type, data):
if event_type == "maze_loaded":
self._display_maze(data)
elif event_type == "path_found":
self._current_path = data
self._display_path(data)
elif event_type == "player_moved":
self._display_maze_with_player(data)
def _display_maze(self, maze):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (maze.width * 2 + 4))
print(" LABYRINTH")
print("=" * (maze.width * 2 + 4))
for y in range(maze.height):
print(" ", end='')
for x in range(maze.width):
cell = maze.get_tile(x, y)
if cell == maze.start_tile:
print('S', end=' ')
elif cell == maze.exit_tile:
print('E', end=' ')
elif cell.blocked:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (maze.width * 2 + 4))
print(" S - start E - exit # - wall . - path")
def _display_maze_with_player(self, maze):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (maze.width * 2 + 4))
print(" LABYRINTH (P = player)")
print("=" * (maze.width * 2 + 4))
for y in range(maze.height):
print(" ", end='')
for x in range(maze.width):
cell = maze.get_tile(x, y)
if self._player and cell == self._player.position:
print('P', end=' ')
elif cell == maze.start_tile:
print('S', end=' ')
elif cell == maze.exit_tile:
print('E', end=' ')
elif cell.blocked:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (maze.width * 2 + 4))
print(f" Player at: ({self._player.position.col}, {self._player.position.row})")
print(" S - start E - exit # - wall . - path P - player")
def _display_path(self, path):
if not path:
print("\n No route found!")
else:
print(f"\n Path found! Length = {len(path)}")
class Player:
def __init__(self, start_tile, labyrinth):
self._pos = start_tile
self._prev = None
self._lab = labyrinth
@property
def position(self):
return self._pos
def move_to(self, new_tile):
if new_tile and new_tile.passable():
self._prev = self._pos
self._pos = new_tile
return True
return False
def undo(self):
if self._prev:
self._pos, self._prev = self._prev, None
return True
return False
class Command:
def do(self):
raise NotImplementedError
def undo(self):
raise NotImplementedError
class MoveCommand(Command):
def __init__(self, player, direction, labyrinth):
self._player = player
self._dx, self._dy = direction
self._lab = labyrinth
self._done = False
def do(self):
nx = self._player.position.col + self._dx
ny = self._player.position.row + self._dy
target = self._lab.get_tile(nx, ny)
if target and target.passable():
self._player.move_to(target)
self._done = True
return True
return False
def undo(self):
if self._done:
self._player.undo()
self._done = False
return True
return False
class MazeSolver:
"""Controls the search process and notifies observers."""
def __init__(self, labyrinth):
self._lab = labyrinth
self._algorithm = None
self._listeners = []
def add_listener(self, listener):
self._listeners.append(listener)
def notify(self, event, data):
for lst in self._listeners:
lst.on_event(event, data)
def set_algorithm(self, algo):
self._algorithm = algo
def solve(self):
if self._algorithm is None:
return None
start_time = time.perf_counter()
route = self._algorithm.find_route(self._lab, self._lab.start_tile, self._lab.exit_tile)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
self.notify("path_found", route)
return SearchStats(elapsed_ms, self._algorithm.visited_cells(), len(route))
def run_experiment(maze_file, algorithm, repetitions=5):
loader = TextFileLoader()
maze = loader.load(maze_file)
total_time = 0.0
total_visited = 0
total_length = 0
for _ in range(repetitions):
solver = MazeSolver(maze)
solver.set_algorithm(algorithm)
stats = solver.solve()
if stats:
total_time += stats.elapsed_ms
total_visited += stats.visited_cells
total_length += stats.path_length
return {
'time_ms': total_time / repetitions,
'visited_cells': total_visited / repetitions,
'path_length': total_length / repetitions
}
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
print("Running experiments (use plots.py for full test suite)...")
sys.exit(0)
loader = TextFileLoader()
maze = loader.load("maze1.txt")
player = Player(maze.start_tile, maze)
view = TerminalView(player)
view.on_event("maze_loaded", maze)
solver = MazeSolver(maze)
solver.add_listener(view)
print("\n CONTROLS:")
print(" H (left) J (down) K (up) L (right)")
print(" U - undo Q - quit")
print("\n AUTO SEARCH:")
print(" B - BFS D - DFS A - A*")
print("\n" + "=" * 50)
history = []
while True:
cmd = input("\n Command > ").lower()
if cmd == 'q':
print("\n Goodbye!")
break
elif cmd == 'b':
solver.set_algorithm(BreadthFirstSearch())
stats = solver.solve()
print(f"\n BFS: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
elif cmd == 'd':
solver.set_algorithm(DepthFirstSearch())
stats = solver.solve()
print(f"\n DFS: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
elif cmd == 'a':
solver.set_algorithm(AStarSearch())
stats = solver.solve()
print(f"\n A*: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
elif cmd in ['h', 'j', 'k', 'l']:
dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
move = MoveCommand(player, dir_map[cmd], maze)
if move.do():
history.append(move)
view.on_event("player_moved", maze)
if player.position == maze.exit_tile:
print("\n *** YOU ESCAPED! ***")
print(f" Total moves: {len(history)}")
break
else:
print("\n Blocked by a wall!")
elif cmd == 'u':
if history:
last = history.pop()
last.undo()
view.on_event("player_moved", maze)
print("\n Undo successful")
else:
print("\n Nothing to undo")
else:
print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit")
print("\n Game over. Thanks for playing!")

View File

@ -0,0 +1,31 @@
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
LinkedList,random,1,4.391112,0.026905,0.012824
LinkedList,random,2,4.845220,0.031560,0.030583
LinkedList,random,3,4.461536,0.030224,0.013461
LinkedList,random,4,4.562402,0.028962,0.014101
LinkedList,random,5,4.491418,0.040197,0.018795
LinkedList,sorted,1,3.728189,0.023831,0.010369
LinkedList,sorted,2,3.681244,0.023794,0.011584
LinkedList,sorted,3,3.710309,0.025346,0.011397
LinkedList,sorted,4,3.687962,0.027130,0.010611
LinkedList,sorted,5,3.713101,0.026431,0.011425
HashTable,random,1,0.056713,0.000387,0.000268
HashTable,random,2,0.053692,0.000412,0.000199
HashTable,random,3,0.053167,0.001272,0.000238
HashTable,random,4,0.059468,0.000414,0.000174
HashTable,random,5,0.052122,0.000918,0.000205
HashTable,sorted,1,0.054478,0.000406,0.000157
HashTable,sorted,2,0.052836,0.000398,0.000190
HashTable,sorted,3,0.052295,0.000410,0.000177
HashTable,sorted,4,0.053164,0.000447,0.000169
HashTable,sorted,5,0.051903,0.000399,0.000179
BST,random,1,0.024767,0.000204,0.000125
BST,random,2,0.025908,0.000222,0.000119
BST,random,3,0.025214,0.000223,0.000113
BST,random,4,0.021233,0.000183,0.000111
BST,random,5,0.022941,0.000277,0.000140
BST,sorted,1,8.967227,0.081463,0.047105
BST,sorted,2,8.873885,0.076518,0.042572
BST,sorted,3,8.827521,0.066650,0.055038
BST,sorted,4,8.722978,0.090392,0.045578
BST,sorted,5,9.053348,0.088699,0.054090
1 Structure Mode Repeat Insert (sec) Search (sec) Delete (sec)
2 LinkedList random 1 4.391112 0.026905 0.012824
3 LinkedList random 2 4.845220 0.031560 0.030583
4 LinkedList random 3 4.461536 0.030224 0.013461
5 LinkedList random 4 4.562402 0.028962 0.014101
6 LinkedList random 5 4.491418 0.040197 0.018795
7 LinkedList sorted 1 3.728189 0.023831 0.010369
8 LinkedList sorted 2 3.681244 0.023794 0.011584
9 LinkedList sorted 3 3.710309 0.025346 0.011397
10 LinkedList sorted 4 3.687962 0.027130 0.010611
11 LinkedList sorted 5 3.713101 0.026431 0.011425
12 HashTable random 1 0.056713 0.000387 0.000268
13 HashTable random 2 0.053692 0.000412 0.000199
14 HashTable random 3 0.053167 0.001272 0.000238
15 HashTable random 4 0.059468 0.000414 0.000174
16 HashTable random 5 0.052122 0.000918 0.000205
17 HashTable sorted 1 0.054478 0.000406 0.000157
18 HashTable sorted 2 0.052836 0.000398 0.000190
19 HashTable sorted 3 0.052295 0.000410 0.000177
20 HashTable sorted 4 0.053164 0.000447 0.000169
21 HashTable sorted 5 0.051903 0.000399 0.000179
22 BST random 1 0.024767 0.000204 0.000125
23 BST random 2 0.025908 0.000222 0.000119
24 BST random 3 0.025214 0.000223 0.000113
25 BST random 4 0.021233 0.000183 0.000111
26 BST random 5 0.022941 0.000277 0.000140
27 BST sorted 1 8.967227 0.081463 0.047105
28 BST sorted 2 8.873885 0.076518 0.042572
29 BST sorted 3 8.827521 0.066650 0.055038
30 BST sorted 4 8.722978 0.090392 0.045578
31 BST sorted 5 9.053348 0.088699 0.054090

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,110 @@
# Отчёт по лабораторной работе
## «Сравнение производительности структур данных на примере телефонного справочника»
**Выполнил:** студент группы ...
**Цель работы:** реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) «с нуля» и экспериментально сравнить их производительность при операциях вставки, поиска и удаления записей телефонного справочника.
---
## 1. Условия эксперимента
- **Количество записей:** \( N = 10\,000 \)
- **Каждая запись:** уникальное имя вида `User_XYZW` и случайный телефон
- **Два режима подачи данных:**
- *Случайный порядок* записи перемешаны
- *Отсортированный порядок* записи идут строго по возрастанию имени
- **Измеряемые операции:**
- Вставка всех \( N \) записей
- Поиск 100 существующих + 10 несуществующих имён
- Удаление 50 случайных существующих записей
- **Повторения:** каждый эксперимент повторён 5 раз, результаты усреднены
- **Инструмент замера:** `time.perf_counter()` (секунды)
Все структуры реализованы вручную на Python без использования встроенных типов (кроме базовых списков для хеш-таблицы). Код находится в файле `phonebook_structures.py`.
---
## 2. Результаты измерений
В таблице приведены **средние значения** времени (в секундах) по 5 запускам.
| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |
|----------------|--------------|-------------|-----------|---------------|
| Связный список | случайный | 4.5503 | 0.0316 | 0.0180 |
| Связный список | отсортир. | 3.7042 | 0.0253 | 0.0111 |
| Хеш-таблица | случайный | 0.0550 | 0.00068 | 0.000217 |
| Хеш-таблица | отсортир. | 0.0529 | 0.00041 | 0.000174 |
| BST (ДДП) | случайный | 0.0240 | 0.000222 | 0.000122 |
| BST (ДДП) | отсортир. | 8.8890 | 0.0807 | 0.0489 |
*Графическое представление результатов приведено на рисунке 1.*
![Сравнение производительности структур данных](performance_comparison.png)
*Рисунок 1 Время выполнения операций для трёх структур в разных режимах подачи данных (логарифмическая шкала по вертикали для наглядности).*
---
## 3. Анализ результатов
### 3.1. Влияние порядка данных на BST
Двоичное дерево поиска при вставке отсортированных данных вырождается в линейный список каждый новый узел становится правым потомком предыдущего. Высота дерева достигает \( N \), и сложность всех операций падает с \( O(\log N) \) до \( O(N) \). Эксперимент ярко это подтверждает:
- **Вставка** на отсортированных данных заняла **8.889 с** это в **370 раз** медленнее, чем на случайных (0.024 с).
- **Поиск** замедлился в **360 раз** (0.0807 с против 0.000222 с).
- **Удаление** замедлилось в **400 раз** (0.0489 с против 0.000122 с).
Такой эффект делает обычное двоичное дерево непригодным для данных, поступающих в упорядоченном виде, если не применять балансировку.
### 3.2. Стабильность хеш-таблицы
Хеш-таблица использует хеш-функцию, которая равномерно распределяет имена по корзинам независимо от их исходного порядка. Поэтому производительность почти не меняется:
- Вставка: ~0.055 с (случайный) и ~0.053 с (отсортированный) разница менее 5%.
- Поиск: 0.00068 с против 0.00041 с небольшие колебания связаны со случайными коллизиями.
- Удаление: также стабильно.
Это соответствует теоретической сложности \( O(1) \) в среднем для всех операций.
### 3.3. Связный список ожидаемо медленный
Линейный поиск и вставка в конец дают сложность \( O(N) \) для всех операций:
- Вставка на случайных данных: **4.55 с** почти в 200 раз медленнее, чем у хеш-таблицы.
- Поиск: **0.0316 с** на два порядка медленнее, чем у BST на случайных данных.
- Отсортированный порядок даёт небольшой выигрыш во вставке (3.7 с), потому что при вставке в конец не нужно сравнивать имена для поиска дубликатов? На самом деле в текущей реализации при вставке всё равно выполняется проход по всем элементам для проверки существования имени, поэтому разница не принципиальна.
Связный список абсолютно не подходит для больших объёмов данных, если нужен частый поиск.
### 3.4. Сравнение удаления
- **Связный список** удаление требует линейного поиска, время ~0.018 с (сопоставимо с поиском).
- **Хеш-таблица** удаление за \( O(1) \) в среднем: ~0.0002 с.
- **BST** на случайных данных очень быстрое удаление (~0.00012 с), но на отсортированных падает до 0.049 с (из-за вырождения).
---
## 4. Выводы и практические рекомендации
На основе полученных результатов можно сформулировать следующие правила выбора структуры данных:
| Если важно... | Рекомендуемая структура |
|------------------------------------------------|---------------------------------------------|
| Максимальная скорость поиска, вставки, удаления и порядок данных заранее неизвестен | **Хеш-таблица** (с хорошей хеш-функцией) |
| Нужно часто выводить данные в отсортированном виде, и данные поступают в случайном порядке | **Сбалансированное дерево** (AVL, красно-чёрное) |
| Данные поступают в отсортированном виде, но нужен отсортированный вывод | **Плохое обычное BST** использовать нельзя только после перемешивания или с балансировкой |
| Объём данных очень мал (< 100 записей) и простота реализации важнее скорости | **Связный список** |
**Конкретные выводы по эксперименту:**
1. **Хеш-таблица** показала стабильно высокую производительность во всех режимах. Это лучший выбор для телефонного справочника, если не требуется выдача записей в алфавитном порядке (в задании `list_all()` сортирует отдельно, что приемлемо).
2. **Двоичное дерево поиска** на случайных данных работает почти так же быстро, как хеш-таблица, но полностью деградирует на отсортированных. Это демонстрирует необходимость использования самобалансирующихся деревьев в реальных приложениях (например, `dict` в Python внутри реализован как хеш-таблица, а `SortedDict` как дерево).
3. **Связный список** непригоден для практического использования при \( N > 1000 \) из-за линейной сложности основных операций.
**Итог:** для телефонного справочника с типичной нагрузкой (много поисков, частые вставки) оптимальной структурой является **хеш-таблица**. Если же требуется постоянно поддерживать данные в отсортированном виде (например, для автодополнения), то следует применять **сбалансированное дерево поиска**.
---
*Дата выполнения эксперимента:* 22 мая 2026 г.
*Файлы результатов:* `experiment_results.csv`, `performance_comparison.png`