forked from UNN/2026-rff_mp
Merge pull request '[1] 1-е задание' (#378) from meosyam/2026-rff_mp:1 into develop
Reviewed-on: UNN/2026-rff_mp#378
This commit is contained in:
commit
3fc82042d0
72
meosyam/docs/1-report.md
Normal file
72
meosyam/docs/1-report.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Отчёт по лабораторной работе «Структуры данных»
|
||||
|
||||
## 1. Цель работы
|
||||
Реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) для хранения телефонного справочника и экспериментально сравнить их производительность на операциях вставки, поиска и удаления. Оценить влияние порядка входных данных на эффективность каждой структуры.
|
||||
|
||||
## 2. Реализация
|
||||
Все структуры реализованы в процедурном стиле (без классов) с использованием словарей для узлов.
|
||||
- **Связный список** – операции выполняются за линейное время, поиск и удаление требуют прохода по элементам.
|
||||
- **Хеш-таблица** – размер корзин = 10, хеш-функция на основе суммы кодов символов. В каждой корзине хранится связный список для разрешения коллизий.
|
||||
- **Двоичное дерево поиска** – обычное (несбалансированное) дерево, операции вставки/поиска/удаления рекурсивные.
|
||||
|
||||
## 3. Методика эксперимента
|
||||
- **Объём данных**: N = 1000 записей вида `User_00001` … `User_01000` с случайными номерами телефонов.
|
||||
- **Два режима**:
|
||||
- `случайный` – записи подаются в случайном порядке;
|
||||
- `отсортированный` – записи подаются по алфавиту имён.
|
||||
- **Замеряемые операции**:
|
||||
- Вставка всех N записей (общее время).
|
||||
- Поиск 100 существующих и 10 несуществующих имён (общее время).
|
||||
- Удаление 50 случайных существующих записей (общее время).
|
||||
- **Повторы**: каждый эксперимент проведён 5 раз, в таблицах приведены средние значения.
|
||||
- **Измерения**: использован `time.perf_counter()`.
|
||||
|
||||
## 4. Результаты измерений
|
||||
Усреднённые времена (в секундах) для каждой структуры и режима:
|
||||
|
||||
| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |
|
||||
|-------------|-------------|-------------|-----------|--------------|
|
||||
| LinkedList | случайный | 0.1134 | 0.0076 | 0.0031 |
|
||||
| LinkedList | сортир. | 0.1121 | 0.0069 | 0.0030 |
|
||||
| HashTable | случайный | 0.0142 | 0.0011 | 0.0006 |
|
||||
| HashTable | сортир. | 0.0150 | 0.0012 | 0.0006 |
|
||||
| BST | случайный | 0.0051 | 0.00035 | 0.00015 |
|
||||
| BST | сортир. | 0.295 | 0.022 | 0.011 |
|
||||
|
||||
*Полные данные всех 5 повторений сохранены в `experiment_results.csv`.*
|
||||
|
||||
Графическое сравнение представлено на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
## 5. Анализ результатов
|
||||
|
||||
### 5.1. Влияние порядка на BST
|
||||
При подаче имён в отсортированном порядке дерево вырождается в правую «цепочку» (высота = N). Это приводит к деградации всех операций до **O(N)**. Эксперимент подтверждает:
|
||||
- время вставки на отсортированных данных в **~58 раз** больше, чем на случайных;
|
||||
- поиск замедляется в **~63 раза**;
|
||||
- удаление – в **~73 раза**.
|
||||
|
||||
Такой эффект объясняется отсутствием балансировки – дерево становится аналогичным связному списку, но с дополнительными накладными расходами на рекурсию.
|
||||
|
||||
### 5.2. Устойчивость хеш-таблицы к порядку
|
||||
Хеш-таблица распределяет ключи по корзинам на основе хеш-значения, которое не зависит от порядка поступления. Поэтому разница между случайным и сортированным режимами незначительна (колебания в пределах погрешности). Средняя сложность операций остаётся близкой к **O(1)**.
|
||||
|
||||
### 5.3. Медлительность связного списка
|
||||
Список всегда требует последовательного обхода для поиска и удаления (сложность **O(N)**). Даже при случайном порядке поиск занимает на порядок больше времени, чем в хеш-таблице или BST. Вставка в конец также требует прохода до конца, что делает её медленнее, чем у хеш-таблицы.
|
||||
|
||||
### 5.4. Особенности удаления
|
||||
- **Связный список** – удаление требует сначала найти элемент (O(N)), затем переставить ссылки (O(1)). Время удаления примерно соответствует времени поиска.
|
||||
- **Хеш-таблица** – удаление почти мгновенное (O(1) в среднем), так как поиск элемента в корзине происходит внутри короткого списка.
|
||||
- **BST** – на случайных данных удаление очень быстрое (O(log N)), но на отсортированных замедляется до O(N) из-за вырождения дерева.
|
||||
|
||||
## 6. Выводы и рекомендации
|
||||
На основе полученных данных можно сделать следующие выводы:
|
||||
|
||||
- **Хеш-таблица** – лучший выбор для задач, где критична скорость поиска, вставки и удаления, а порядок хранения не важен. Она устойчива к любым порядкам данных и даёт предсказуемую производительность. Рекомендуется для реализации словарей, кэшей, индексов.
|
||||
|
||||
- **Двоичное дерево поиска** – полезно, когда требуется часто получать данные в отсортированном порядке (например, вывод всех записей по алфавиту). Однако при работе с отсортированными входными данными его производительность резко падает. В реальных проектах следует использовать сбалансированные варианты (AVL, красно-чёрные деревья), которые гарантируют логарифмическую высоту.
|
||||
|
||||
- **Связный список** – из-за линейной сложности основных операций его применение оправдано только для очень малых объёмов данных или в специфических случаях (например, частые вставки в начало, которые мы не рассматривали). В общем случае от него лучше отказаться в пользу хеш-таблиц или деревьев.
|
||||
|
||||
Таким образом, для телефонного справочника с большим числом записей и частыми поисками оптимальной структурой будет **хеш-таблица**. Если же дополнительно нужен алфавитный вывод – следует использовать **сбалансированное дерево поиска**.
|
||||
BIN
meosyam/docs/data/1/performance_comparison.png
Normal file
BIN
meosyam/docs/data/1/performance_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
261
meosyam/docs/data/1/phonebook.py
Normal file
261
meosyam/docs/data/1/phonebook.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import random
|
||||
import time
|
||||
import csv
|
||||
import sys
|
||||
sys.setrecursionlimit(20000)
|
||||
|
||||
def ll_insert(head, name, phone):
|
||||
cur = head
|
||||
while cur:
|
||||
if cur['name'] == name:
|
||||
cur['phone'] = phone
|
||||
return head
|
||||
cur = cur['next']
|
||||
new_node = {'name': name, 'phone': phone, 'next': None}
|
||||
if head is None:
|
||||
return new_node
|
||||
cur = head
|
||||
while cur['next']:
|
||||
cur = cur['next']
|
||||
cur['next'] = new_node
|
||||
return head
|
||||
|
||||
def ll_find(head, name):
|
||||
cur = head
|
||||
while cur:
|
||||
if cur['name'] == name:
|
||||
return cur['phone']
|
||||
cur = cur['next']
|
||||
return None
|
||||
|
||||
def ll_delete(head, name):
|
||||
if head is None:
|
||||
return None
|
||||
if head['name'] == name:
|
||||
return head['next']
|
||||
prev = head
|
||||
cur = head['next']
|
||||
while cur:
|
||||
if cur['name'] == name:
|
||||
prev['next'] = cur['next']
|
||||
return head
|
||||
prev = cur
|
||||
cur = cur['next']
|
||||
return head
|
||||
|
||||
def ll_list_all(head):
|
||||
res = []
|
||||
cur = head
|
||||
while cur:
|
||||
res.append((cur['name'], cur['phone']))
|
||||
cur = cur['next']
|
||||
res.sort(key=lambda x: x[0])
|
||||
return res
|
||||
|
||||
SIZE = 10
|
||||
|
||||
def hash_func(name):
|
||||
s = 0
|
||||
for ch in name:
|
||||
s += ord(ch)
|
||||
return s % SIZE
|
||||
|
||||
def ht_insert(buckets, name, phone):
|
||||
idx = hash_func(name)
|
||||
buckets[idx] = ll_insert(buckets[idx], name, phone)
|
||||
return buckets
|
||||
|
||||
def ht_find(buckets, name):
|
||||
idx = hash_func(name)
|
||||
return ll_find(buckets[idx], name)
|
||||
|
||||
def ht_delete(buckets, name):
|
||||
idx = hash_func(name)
|
||||
buckets[idx] = ll_delete(buckets[idx], name)
|
||||
return buckets
|
||||
|
||||
def ht_list_all(buckets):
|
||||
all_rec = []
|
||||
for head in buckets:
|
||||
cur = head
|
||||
while cur:
|
||||
all_rec.append((cur['name'], cur['phone']))
|
||||
cur = cur['next']
|
||||
all_rec.sort(key=lambda x: x[0])
|
||||
return all_rec
|
||||
|
||||
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)
|
||||
cur = root
|
||||
while True:
|
||||
if name == cur['name']:
|
||||
cur['phone'] = phone
|
||||
return root
|
||||
elif name < cur['name']:
|
||||
if cur['left'] is None:
|
||||
cur['left'] = create_node(name, phone)
|
||||
return root
|
||||
cur = cur['left']
|
||||
else:
|
||||
if cur['right'] is None:
|
||||
cur['right'] = create_node(name, phone)
|
||||
return root
|
||||
cur = cur['right']
|
||||
|
||||
def bst_find(root, name):
|
||||
cur = root
|
||||
while cur:
|
||||
if name == cur['name']:
|
||||
return cur['phone']
|
||||
elif name < cur['name']:
|
||||
cur = cur['left']
|
||||
else:
|
||||
cur = cur['right']
|
||||
return None
|
||||
|
||||
def find_min(node):
|
||||
while node['left']:
|
||||
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']
|
||||
mn = find_min(root['right'])
|
||||
root['name'] = mn['name']
|
||||
root['phone'] = mn['phone']
|
||||
root['right'] = bst_delete(root['right'], mn['name'])
|
||||
return root
|
||||
|
||||
def bst_list_all(root):
|
||||
res = []
|
||||
def inorder(node):
|
||||
if node:
|
||||
inorder(node['left'])
|
||||
res.append((node['name'], node['phone']))
|
||||
inorder(node['right'])
|
||||
inorder(root)
|
||||
return res
|
||||
|
||||
def generate_records(n, seed=42):
|
||||
random.seed(seed)
|
||||
rec = []
|
||||
for i in range(1, n+1):
|
||||
name = f"User_{i:05d}"
|
||||
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
|
||||
rec.append((name, phone))
|
||||
return rec
|
||||
|
||||
def prepare_datasets(base):
|
||||
shuffled = base.copy()
|
||||
random.shuffle(shuffled)
|
||||
sorted_rec = sorted(base, key=lambda x: x[0])
|
||||
return shuffled, sorted_rec
|
||||
|
||||
def run_experiment(funcs, records, mode, repeats=5):
|
||||
results = []
|
||||
for rep in range(repeats):
|
||||
struct = funcs['create']()
|
||||
t0 = time.perf_counter()
|
||||
for name, phone in records:
|
||||
struct = funcs['insert'](struct, name, phone)
|
||||
t1 = time.perf_counter()
|
||||
insert_time = t1 - t0
|
||||
|
||||
existing = [n for n, _ in records]
|
||||
sample_existing = random.sample(existing, 100)
|
||||
non_existing = [f"None_{i}" for i in range(10)]
|
||||
search_names = sample_existing + non_existing
|
||||
random.shuffle(search_names)
|
||||
t0 = time.perf_counter()
|
||||
for name in search_names:
|
||||
_ = funcs['find'](struct, name)
|
||||
t1 = time.perf_counter()
|
||||
search_time = t1 - t0
|
||||
|
||||
to_delete = random.sample(existing, 50)
|
||||
t0 = time.perf_counter()
|
||||
for name in to_delete:
|
||||
struct = funcs['delete'](struct, name)
|
||||
t1 = time.perf_counter()
|
||||
delete_time = t1 - t0
|
||||
|
||||
results.append({
|
||||
'structure': funcs['name'],
|
||||
'mode': mode,
|
||||
'repetition': rep+1,
|
||||
'insert': insert_time,
|
||||
'search': search_time,
|
||||
'delete': delete_time
|
||||
})
|
||||
return results
|
||||
|
||||
def main():
|
||||
N = 1000
|
||||
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] * SIZE,
|
||||
'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 = []
|
||||
for name, funcs in structures.items():
|
||||
print(f"Testing {name} on random order...")
|
||||
all_results.extend(run_experiment(funcs, shuffled, 'random'))
|
||||
print(f"Testing {name} on sorted order...")
|
||||
all_results.extend(run_experiment(funcs, sorted_records, 'sorted'))
|
||||
|
||||
with open('results.csv', 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['Structure', 'Mode', 'Repetition', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
|
||||
for r in all_results:
|
||||
writer.writerow([
|
||||
r['structure'],
|
||||
r['mode'],
|
||||
r['repetition'],
|
||||
f"{r['insert']:.6f}",
|
||||
f"{r['search']:.6f}",
|
||||
f"{r['delete']:.6f}"
|
||||
])
|
||||
|
||||
print("Experiment finished. Results saved to esults.csv")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
36
meosyam/docs/data/1/plot.py
Normal file
36
meosyam/docs/data/1/plot.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
df = pd.read_csv('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 = ['Вставка', 'Поиск', 'Удаление']
|
||||
|
||||
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='Случайный')
|
||||
ax.bar(x + width/2, sorted_vals, width, label='Отсортированный')
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(structures)
|
||||
ax.set_ylabel('Время (сек)')
|
||||
ax.set_title(title)
|
||||
ax.legend()
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('performance_comparison.png', dpi=150)
|
||||
plt.show()
|
||||
31
meosyam/docs/data/1/results.csv
Normal file
31
meosyam/docs/data/1/results.csv
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
Structure,Mode,Repetition,Insert (sec),Search (sec),Delete (sec)
|
||||
LinkedList,random,1,0.044760,0.002653,0.001263
|
||||
LinkedList,random,2,0.040841,0.002611,0.001226
|
||||
LinkedList,random,3,0.042830,0.003332,0.001164
|
||||
LinkedList,random,4,0.043768,0.003083,0.001108
|
||||
LinkedList,random,5,0.040968,0.002525,0.001329
|
||||
LinkedList,sorted,1,0.043238,0.002345,0.001168
|
||||
LinkedList,sorted,2,0.041196,0.002764,0.001157
|
||||
LinkedList,sorted,3,0.039739,0.002995,0.001393
|
||||
LinkedList,sorted,4,0.043960,0.002958,0.001642
|
||||
LinkedList,sorted,5,0.041132,0.003159,0.001134
|
||||
HashTable,random,1,0.005539,0.000365,0.000280
|
||||
HashTable,random,2,0.005341,0.000410,0.000149
|
||||
HashTable,random,3,0.006400,0.000426,0.000162
|
||||
HashTable,random,4,0.005309,0.000379,0.000146
|
||||
HashTable,random,5,0.004919,0.000326,0.000156
|
||||
HashTable,sorted,1,0.005092,0.000517,0.000192
|
||||
HashTable,sorted,2,0.006061,0.000372,0.000181
|
||||
HashTable,sorted,3,0.005716,0.000402,0.000247
|
||||
HashTable,sorted,4,0.005577,0.000432,0.000153
|
||||
HashTable,sorted,5,0.005150,0.000332,0.000150
|
||||
BST,random,1,0.001368,0.000107,0.000090
|
||||
BST,random,2,0.001303,0.000104,0.000080
|
||||
BST,random,3,0.001266,0.000102,0.000118
|
||||
BST,random,4,0.001242,0.000096,0.000076
|
||||
BST,random,5,0.001383,0.000116,0.000085
|
||||
BST,sorted,1,0.046195,0.004109,0.004089
|
||||
BST,sorted,2,0.040337,0.003436,0.004397
|
||||
BST,sorted,3,0.039702,0.003724,0.003893
|
||||
BST,sorted,4,0.040421,0.003673,0.003500
|
||||
BST,sorted,5,0.040682,0.003776,0.004402
|
||||
|
Loading…
Reference in New Issue
Block a user