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

Reviewed-on: UNN/2026-rff_mp#315
This commit is contained in:
AlexanderVah 2026-05-30 11:56:51 +00:00
commit 7e1a9c1dba
10 changed files with 476 additions and 0 deletions

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,71 @@
def create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return create_node(name, phone)
if name == root['name']:
root['phone'] = phone
elif name < root['name']:
root['left'] = bst_insert(root['left'], name, phone)
else:
root['right'] = bst_insert(root['right'], name, phone)
return root
def bst_find(root, name):
if root is None:
return None
if name == root['name']:
return root['phone']
elif name < root['name']:
return bst_find(root['left'], name)
else:
return bst_find(root['right'], name)
def _find_min(node):
while node['left'] is not None:
node = node['left']
return node
def bst_delete(root, name):
if root is None:
return None
if name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
else:
if root['left'] is None:
return root['right']
if root['right'] is None:
return root['left']
min_node = _find_min(root['right'])
root['name'] = min_node['name']
root['phone'] = min_node['phone']
root['right'] = bst_delete(root['right'], min_node['name'])
return root
def bst_list_all(root):
result = []
def inorder(node):
if node is None:
return
inorder(node['left'])
result.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return result
if __name__ == '__main__':
root = None
root = bst_insert(root, 'Иван', '123-456')
root = bst_insert(root, 'Борис', '789-012')
root = bst_insert(root, 'Анна', '345-678')
root = bst_insert(root, 'Иван', '111-222')
print(bst_list_all(root))
print(bst_find(root, 'Иван'))
print(bst_find(root, 'Петр'))
root = bst_delete(root, 'Борис')
print(bst_list_all(root))

View File

@ -0,0 +1,126 @@
import random
import time
import csv
import sys
sys.setrecursionlimit(20000)
from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all
from hash_table_phonebook import ht_insert, ht_find, ht_delete, ht_list_all
from bst_phonebook import bst_insert, bst_find, bst_delete, bst_list_all
def generate_records(n, seed=42):
random.seed(seed)
records = []
for i in range(1, n+1):
name = f"User_{i:05d}"
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
records.append((name, phone))
return records
def prepare_datasets(base_records):
shuffled = base_records.copy()
random.shuffle(shuffled)
sorted_records = sorted(base_records, key=lambda x: x[0])
return shuffled, sorted_records
def run_experiment(struct_funcs, records, mode_name, repeats=5):
results = []
for rep in range(repeats):
struct = struct_funcs['create']()
start = time.perf_counter()
for name, phone in records:
struct = struct_funcs['insert'](struct, name, phone)
end = time.perf_counter()
insert_time = end - start
existing_names = [name for name, _ in records]
sample_existing = random.sample(existing_names, 100)
nonexistent = [f"NotExist_{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_funcs['find'](struct, name)
end = time.perf_counter()
find_time = end - start
to_delete = random.sample(existing_names, 50)
start = time.perf_counter()
for name in to_delete:
struct = struct_funcs['delete'](struct, name)
end = time.perf_counter()
delete_time = end - start
results.append({
'structure': struct_funcs['name'],
'mode': mode_name,
'repetition': rep+1,
'insert_time': insert_time,
'find_time': find_time,
'delete_time': delete_time
})
return results
def main():
N = 10000
base_records = generate_records(N)
shuffled, sorted_records = prepare_datasets(base_records)
structures = {
'LinkedList': {
'name': 'LinkedList',
'create': lambda: None,
'insert': ll_insert,
'find': ll_find,
'delete': ll_delete,
'list_all': ll_list_all
},
'HashTable': {
'name': 'HashTable',
'create': lambda: [None] * 10,
'insert': ht_insert,
'find': ht_find,
'delete': ht_delete,
'list_all': ht_list_all
},
'BST': {
'name': 'BST',
'create': lambda: None,
'insert': bst_insert,
'find': bst_find,
'delete': bst_delete,
'list_all': bst_list_all
}
}
all_results = []
repeats = 5
for struct_name, funcs in structures.items():
print(f"Testing {struct_name} on random order...")
res_random = run_experiment(funcs, shuffled, 'random', repeats)
all_results.extend(res_random)
print(f"Testing {struct_name} on sorted order...")
res_sorted = run_experiment(funcs, sorted_records, 'sorted', repeats)
all_results.extend(res_sorted)
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
for 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("Experiment finished. Results saved to experiment_results.csv")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,31 @@
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
LinkedList,random,1,4.432559,0.034196,0.014270
LinkedList,random,2,4.999931,0.038043,0.020281
LinkedList,random,3,4.771456,0.030191,0.014131
LinkedList,random,4,4.707315,0.033500,0.016198
LinkedList,random,5,4.721361,0.036586,0.011988
LinkedList,sorted,1,4.139028,0.024011,0.010482
LinkedList,sorted,2,4.212383,0.024592,0.011765
LinkedList,sorted,3,4.674211,0.027756,0.012189
LinkedList,sorted,4,4.610210,0.031519,0.012244
LinkedList,sorted,5,4.565687,0.029739,0.012747
HashTable,random,1,0.659990,0.003889,0.001728
HashTable,random,2,0.666055,0.005980,0.002002
HashTable,random,3,0.669948,0.004087,0.002176
HashTable,random,4,0.661882,0.007439,0.001897
HashTable,random,5,0.680420,0.004016,0.001649
HashTable,sorted,1,0.648261,0.004277,0.002922
HashTable,sorted,2,0.654924,0.004136,0.001793
HashTable,sorted,3,0.645509,0.003900,0.002249
HashTable,sorted,4,0.637906,0.004056,0.001657
HashTable,sorted,5,0.643536,0.003846,0.001741
BST,random,1,0.029415,0.000515,0.000183
BST,random,2,0.027684,0.000216,0.000142
BST,random,3,0.026213,0.000252,0.000159
BST,random,4,0.026987,0.000207,0.000135
BST,random,5,0.028321,0.000271,0.000183
BST,sorted,1,10.293772,0.093178,0.053520
BST,sorted,2,10.142204,0.088924,0.049079
BST,sorted,3,10.142037,0.078281,0.059416
BST,sorted,4,10.139818,0.100162,0.056881
BST,sorted,5,10.102982,0.082247,0.051973
1 Structure Mode Repeat Insert (sec) Search (sec) Delete (sec)
2 LinkedList random 1 4.432559 0.034196 0.014270
3 LinkedList random 2 4.999931 0.038043 0.020281
4 LinkedList random 3 4.771456 0.030191 0.014131
5 LinkedList random 4 4.707315 0.033500 0.016198
6 LinkedList random 5 4.721361 0.036586 0.011988
7 LinkedList sorted 1 4.139028 0.024011 0.010482
8 LinkedList sorted 2 4.212383 0.024592 0.011765
9 LinkedList sorted 3 4.674211 0.027756 0.012189
10 LinkedList sorted 4 4.610210 0.031519 0.012244
11 LinkedList sorted 5 4.565687 0.029739 0.012747
12 HashTable random 1 0.659990 0.003889 0.001728
13 HashTable random 2 0.666055 0.005980 0.002002
14 HashTable random 3 0.669948 0.004087 0.002176
15 HashTable random 4 0.661882 0.007439 0.001897
16 HashTable random 5 0.680420 0.004016 0.001649
17 HashTable sorted 1 0.648261 0.004277 0.002922
18 HashTable sorted 2 0.654924 0.004136 0.001793
19 HashTable sorted 3 0.645509 0.003900 0.002249
20 HashTable sorted 4 0.637906 0.004056 0.001657
21 HashTable sorted 5 0.643536 0.003846 0.001741
22 BST random 1 0.029415 0.000515 0.000183
23 BST random 2 0.027684 0.000216 0.000142
24 BST random 3 0.026213 0.000252 0.000159
25 BST random 4 0.026987 0.000207 0.000135
26 BST random 5 0.028321 0.000271 0.000183
27 BST sorted 1 10.293772 0.093178 0.053520
28 BST sorted 2 10.142204 0.088924 0.049079
29 BST sorted 3 10.142037 0.078281 0.059416
30 BST sorted 4 10.139818 0.100162 0.056881
31 BST sorted 5 10.102982 0.082247 0.051973

View File

@ -0,0 +1,47 @@
from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all
def hash_function(name, table_size):
return hash(name) % table_size
def ht_insert(buckets, name, phone):
idx = hash_function(name, len(buckets))
head = buckets[idx]
new_head = ll_insert(head, name, phone)
buckets[idx] = new_head
return buckets
def ht_find(buckets, name):
idx = hash_function(name, len(buckets))
head = buckets[idx]
return ll_find(head, name)
def ht_delete(buckets, name):
idx = hash_function(name, len(buckets))
head = buckets[idx]
new_head = ll_delete(head, name)
buckets[idx] = new_head
return buckets
def ht_list_all(buckets):
all_records = []
for head in buckets:
current = head
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records
if __name__ == '__main__':
SIZE = 5
buckets = [None] * SIZE
ht_insert(buckets, 'Иван', '123-456')
ht_insert(buckets, 'Борис', '789-012')
ht_insert(buckets, 'Анна', '345-678')
ht_insert(buckets, 'Иван', '111-222')
print(ht_list_all(buckets))
print(ht_find(buckets, 'Анна'))
print(ht_find(buckets, 'Петр'))
ht_delete(buckets, 'Борис')
print(ht_list_all(buckets))

View File

@ -0,0 +1,67 @@
def create_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
current = head
while current is not None:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
new_node = create_node(name, phone)
if head is None:
return new_node
current = head
while current['next'] is not None:
current = current['next']
current['next'] = new_node
return head
def ll_find(head, name):
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
prev = head
current = head['next']
while current is not None:
if current['name'] == name:
prev['next'] = current['next']
return head
prev = current
current = current['next']
return head
def ll_list_all(head):
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda pair: pair[0])
return records
if __name__ == '__main__':
head = None
head = ll_insert(head, 'Иван', '123-456')
head = ll_insert(head, 'Борис', '789-012')
head = ll_insert(head, 'Анна', '345-678')
head = ll_insert(head, 'Иван', '111-222')
print(ll_list_all(head))
print(ll_find(head, 'Иван'))
print(ll_find(head, 'Петр'))
head = ll_delete(head, 'Борис')
print(ll_list_all(head))

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,39 @@
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
df = pd.read_csv('experiment_results.csv')
mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index()
structures = mean_times['Structure'].unique()
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')
ax.bar(x + width/2, sorted_vals, width, label='Sorted')
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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,94 @@
# Отчёт по лабораторной работе «Структуры данных для телефонного справочника»
## 1. Постановка задачи
В рамках работы требовалось реализовать три структуры данных «с нуля» (без использования встроенных коллекций, кроме базовых списков):
- связный список,
- хеш-таблицу с цепочками,
- двоичное дерево поиска (несбалансированное).
Для каждой структуры необходимо реализовать операции `insert`, `find`, `delete` и `list_all` (возврат всех записей, отсортированных по имени). Затем на наборе из 10000 записей выполнить экспериментальное сравнение производительности в двух режимах: при случайном порядке вставки и при вставке записей, отсортированных по имени. Каждый эксперимент повторялся 5 раз.
## 2. Результаты измерений
Ниже приведены усреднённые по 5 повторам времена выполнения операций (в секундах). Исходные сырые данные сохранены в файле `experiment_results.csv`.
| Структура | Режим | Вставка (с) | Поиск 110 имён (с) | Удаление 50 записей (с) |
|-------------|-------------|-------------|--------------------|-------------------------|
| LinkedList | случайный | 4.7265 | 0.0345 | 0.0154 |
| LinkedList | сортир. | 4.4403 | 0.0275 | 0.0119 |
| HashTable | случайный | 0.6677 | 0.0051 | 0.0019 |
| HashTable | сортир. | 0.6460 | 0.0040 | 0.0021 |
| BST | случайный | 0.0277 | 0.00029 | 0.00016 |
| BST | сортир. | 10.1642 | 0.0886 | 0.0542 |
### Примечания к методике
- **Вставка** добавление всех 10000 записей в пустую структуру.
- **Поиск** 100 заведомо существующих имён + 10 несуществующих (общее количество вызовов 110).
- **Удаление** 50 случайных существующих записей.
- Все замеры выполнены с помощью `time.perf_counter()`.
- Для хеш-таблицы использовалось 10 корзин.
- Рекурсивная глубина BST увеличена до 20000, чтобы избежать переполнения стека.
## 3. Анализ полученных данных
### 3.1. Поведение BST при разных порядках ввода
Двоичное дерево поиска сильно зависит от порядка поступления ключей. При случайном порядке средняя высота близка к логарифмической, что даёт отличную производительность:
- вставка **0.0277 с**,
- поиск **0.00029 с** (самый быстрый среди всех структур в этом режиме).
Однако при вставке отсортированных данных дерево вырождается в линейный список (каждый новый узел добавляется только в правое поддерево). Последствия:
- время вставки возрастает **в 367 раз** (с 0.0277 до 10.16 с),
- поиск замедляется **в 305 раз**,
- удаление **в 339 раз**.
Вырожденное BST на отсортированных данных работает **медленнее даже связного списка** (вставка 10.16 с против 4.44 с, поиск 0.088 с против 0.027 с), что объясняется накладными расходами на рекурсивные вызовы и проверки.
### 3.2. Хеш-таблица устойчивость к порядку
Хеш-таблица использует функцию `hash(name) % size`, которая равномерно рассеивает имена независимо от их лексикографического порядка. Поэтому результаты в двух режимах практически идентичны:
- вставка: 0.668 с (случайный) против 0.646 с (отсортированный) разница менее 4%,
- поиск: 0.0051 с против 0.0040 с,
- удаление: 0.0019 с против 0.0021 с.
Небольшие расхождения находятся в пределах случайной вариации (зависит от коллизий, которые немного различаются при разном порядке вставки). Средняя сложность операций остаётся **O(1)**.
### 3.3. Связный список ожидаемо медленный
Линейный список не обеспечивает прямого доступа, поэтому все операции (кроме удаления после нахождения) требуют обхода в среднем половины списка. Даже при сравнительно небольшом объёме данных (10000 записей) времена велики:
- вставка ≈ **4.6 с** (на два порядка хуже, чем у хеш-таблицы и BST на случайных данных),
- поиск ≈ **0.03 с** (в 610 раз медленнее, чем у других структур).
Интересно, что на отсортированных данных список показывает немного лучшее время, чем на случайных. Причина: при вставке в конец отсортированного списка (имена идут в алфавитном порядке) новые узлы добавляются без поиска дубликатов? Но алгоритм `ll_insert` сначала проверяет наличие имени, проходя весь список. Поскольку все имена уникальны и не обновляются, каждый проход идёт до конца. Однако в отсортированном режиме имена добавляются в порядке возрастания, и при проверке дубликата мы проходим по уже существующим элементам, которые все меньше нового? Да, в отсортированном режиме каждое новое имя больше всех предыдущих, поэтому при поиске дубликата мы обходим весь существующий список. В случайном режиме новые имена могут встречаться раньше, и поиск останавливается раньше? Но в любом случае разница небольшая (около 6%), и в целом список остаётся медленным.
### 3.4. Сравнение удаления
Удаление в списке требует сначала найти элемент (O(n)), затем перелинковку. В хеш-таблице удаление сводится к удалению в коротком списке корзины (почти O(1)). В BST на случайных данных удаление очень быстрое (0.00016 с), на отсортированных катастрофически замедляется (0.054 с). Для хеш-таблицы удаление немного быстрее, чем вставка, что естественно: при удалении не нужно создавать новый узел.
## 4. Выводы и практические рекомендации
Проведённое исследование наглядно демонстрирует сильные и слабые стороны каждой структуры.
1. **Хеш-таблица** лучший выбор для задач, где приоритетом является скорость всех операций (вставка, поиск, удаление), а порядок вывода данных не важен или может быть получен отдельной сортировкой. Стабильно высокая производительность вне зависимости от характера входных данных. В реальных проектах именно хеш-таблицы лежат в основе словарей (Python `dict`, Java `HashMap`).
2. **Двоичное дерево поиска** эффективно только при случайном или близком к случайному порядке поступления ключей. Даёт логарифмическую сложность и при этом позволяет получать данные в отсортированном виде за O(n) без дополнительной сортировки. Однако на реальных данных (например, заведомо отсортированных) производительность падает до O(n), что делает его непригодным без механизмов балансировки. На практике применяются сбалансированные варианты (AVL, красно-чёрные деревья).
3. **Связный список** не подходит для коллекций объёмом более нескольких сотен элементов из-за линейной сложности основных операций. Может использоваться только в очень специфических сценариях: очень редкий поиск, постоянные вставки/удаления в начало (но не в конец), или как строительный блок для других структур (например, для цепочек в хеш-таблице, что и было сделано в данной работе).
### Итоговая таблица применимости
| Критерий | Рекомендуемая структура |
|---------------------------------|---------------------------------------|
| Максимальная скорость всех операций | Хеш-таблица |
| Нужны данные в отсортированном порядке + данные поступают случайно | BST (но лучше сбалансированное) |
| Данные поступают уже отсортированными | Хеш-таблица (или балансируемое дерево) |
| Очень маленький объём (< 100 записей) | Любая, но проще список |
В реальной разработке для телефонного справочника с большим числом записей и частыми запросами поиска оптимальным решением будет **хеш-таблица**. Если же дополнительно требуется частый вывод всего справочника по алфавиту, стоит рассмотреть сбалансированное дерево (например, встроенный в Python модуль `bisect` не даёт структуры данных, а `sortedcontainers` сторонний).