Merge pull request '[1] 1-st dolg :)' (#376) from agapovads/2026-rff_mp:1-st into develop
Reviewed-on: #376
This commit is contained in:
commit
4ebecfe147
1
AgapovaDS/428b.md
Normal file
1
AgapovaDS/428b.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
428b
|
||||
31
AgapovaDS/docs/data/1-st/experiment_results.csv
Normal file
31
AgapovaDS/docs/data/1-st/experiment_results.csv
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
Structure,Mode,Repetition,Insert (sec),Find (sec),Delete (sec)
|
||||
LinkedList,random,1,0.022518936000324175,0.0022566010002265102,0.00023215900000650436
|
||||
LinkedList,random,2,0.020449046000067028,0.00233427400007713,0.0002826350000759703
|
||||
LinkedList,random,3,0.020126345999869955,0.002262161000089691,0.0002053490002253966
|
||||
LinkedList,random,4,0.018700520000038523,0.0021239550001155294,0.00021164700001463643
|
||||
LinkedList,random,5,0.019754801000090083,0.0024997460000122373,0.00023158399972089683
|
||||
HashTable,random,1,0.0029892609995840758,0.0003534970001055626,2.990000029967632e-05
|
||||
HashTable,random,2,0.00424448100011432,0.00041066700032388326,4.1896999846358085e-05
|
||||
HashTable,random,3,0.004175077000127203,0.0004925200000798213,2.9787000130454544e-05
|
||||
HashTable,random,4,0.004567926000163425,0.00033349500017720857,3.164999998261919e-05
|
||||
HashTable,random,5,0.0026621540000633104,0.00031125700024858816,2.81510001514107e-05
|
||||
BST,random,1,0.0011096680000264314,9.346200022264384e-05,3.9635000121052144e-05
|
||||
BST,random,2,0.0010248069997942366,9.059800004251883e-05,2.1142000150575768e-05
|
||||
BST,random,3,0.0010313749999113497,9.237799986294704e-05,1.8248000287712784e-05
|
||||
BST,random,4,0.0010483440000825794,9.526299982098863e-05,0.0001046150000547641
|
||||
BST,random,5,0.0010863010002140072,8.332400011568097e-05,1.7150000076071592e-05
|
||||
LinkedList,sorted,1,0.022635587000422674,0.0023248429997693165,0.00031860600029176567
|
||||
LinkedList,sorted,2,0.02307340400011526,0.002558939999744325,0.00023637899994355394
|
||||
LinkedList,sorted,3,0.01936496400003307,0.002565375999893149,0.00019060200020248885
|
||||
LinkedList,sorted,4,0.018800279000060982,0.0021075829999972484,0.00018179399967266363
|
||||
LinkedList,sorted,5,0.019234453000080975,0.002213534000020445,0.0002037049998762086
|
||||
HashTable,sorted,1,0.0031771270000717777,0.0006652990000475256,6.005399973219028e-05
|
||||
HashTable,sorted,2,0.004056166999816924,0.00058001300021715,3.078500003539375e-05
|
||||
HashTable,sorted,3,0.00331192200019359,0.00027228699991610483,2.684700029931264e-05
|
||||
HashTable,sorted,4,0.002749069000401505,0.0002677190000213159,2.1935999939159956e-05
|
||||
HashTable,sorted,5,0.002638604000367195,0.0003338189999340102,1.83020001713885e-05
|
||||
BST,sorted,1,0.037994623000031424,0.0035877690002052987,0.0011276019999968412
|
||||
BST,sorted,2,0.038043681000090146,0.0031297909999921103,0.00094645799981663
|
||||
BST,sorted,3,0.03629641699990316,0.0027566620001380215,0.0012528499996733444
|
||||
BST,sorted,4,0.03509285000018281,0.003267435999987356,0.0011822649998975976
|
||||
BST,sorted,5,0.038755655999921146,0.0064144259999920905,0.0018493429997761268
|
||||
|
BIN
AgapovaDS/docs/data/1-st/performance_comparison.png
Normal file
BIN
AgapovaDS/docs/data/1-st/performance_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
335
AgapovaDS/docs/data/1-st/phonebook.py
Normal file
335
AgapovaDS/docs/data/1-st/phonebook.py
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import random
|
||||
import time
|
||||
import sys
|
||||
import csv
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
sys.setrecursionlimit(10000)
|
||||
|
||||
ll_head = None
|
||||
|
||||
def ll_insert(name, phone):
|
||||
global ll_head
|
||||
cur = ll_head
|
||||
while cur is not None:
|
||||
if cur['name'] == name:
|
||||
cur['phone'] = phone
|
||||
return
|
||||
cur = cur['next']
|
||||
new_node = {'name': name, 'phone': phone, 'next': ll_head}
|
||||
ll_head = new_node
|
||||
|
||||
def ll_find(name):
|
||||
cur = ll_head
|
||||
while cur is not None:
|
||||
if cur['name'] == name:
|
||||
return cur['phone']
|
||||
cur = cur['next']
|
||||
return None
|
||||
|
||||
def ll_delete(name):
|
||||
global ll_head
|
||||
if ll_head is None:
|
||||
return
|
||||
if ll_head['name'] == name:
|
||||
ll_head = ll_head['next']
|
||||
return
|
||||
prev = ll_head
|
||||
cur = ll_head['next']
|
||||
while cur is not None:
|
||||
if cur['name'] == name:
|
||||
prev['next'] = cur['next']
|
||||
return
|
||||
prev = cur
|
||||
cur = cur['next']
|
||||
|
||||
def ll_list_all():
|
||||
records = []
|
||||
cur = ll_head
|
||||
while cur is not None:
|
||||
records.append((cur['name'], cur['phone']))
|
||||
cur = cur['next']
|
||||
records.sort(key=lambda x: x[0])
|
||||
return records
|
||||
|
||||
BUCKET_COUNT = 10
|
||||
buckets = [None] * BUCKET_COUNT
|
||||
|
||||
def hash_func(name):
|
||||
s = 0
|
||||
for ch in name:
|
||||
s += ord(ch)
|
||||
return s % BUCKET_COUNT
|
||||
|
||||
def ht_insert(name, phone):
|
||||
global buckets
|
||||
idx = hash_func(name)
|
||||
cur = buckets[idx]
|
||||
while cur is not None:
|
||||
if cur['name'] == name:
|
||||
cur['phone'] = phone
|
||||
return
|
||||
cur = cur['next']
|
||||
new_node = {'name': name, 'phone': phone, 'next': buckets[idx]}
|
||||
buckets[idx] = new_node
|
||||
|
||||
def ht_find(name):
|
||||
idx = hash_func(name)
|
||||
cur = buckets[idx]
|
||||
while cur is not None:
|
||||
if cur['name'] == name:
|
||||
return cur['phone']
|
||||
cur = cur['next']
|
||||
return None
|
||||
|
||||
def ht_delete(name):
|
||||
global buckets
|
||||
idx = hash_func(name)
|
||||
head = buckets[idx]
|
||||
if head is None:
|
||||
return
|
||||
if head['name'] == name:
|
||||
buckets[idx] = head['next']
|
||||
return
|
||||
prev = head
|
||||
cur = head['next']
|
||||
while cur is not None:
|
||||
if cur['name'] == name:
|
||||
prev['next'] = cur['next']
|
||||
return
|
||||
prev = cur
|
||||
cur = cur['next']
|
||||
|
||||
def ht_list_all():
|
||||
all_records = []
|
||||
for head in buckets:
|
||||
cur = head
|
||||
while cur is not None:
|
||||
all_records.append((cur['name'], cur['phone']))
|
||||
cur = cur['next']
|
||||
all_records.sort(key=lambda x: x[0])
|
||||
return all_records
|
||||
|
||||
bst_root = None
|
||||
|
||||
def bst_create_node(name, phone):
|
||||
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||||
|
||||
def bst_insert(name, phone):
|
||||
global bst_root
|
||||
if bst_root is None:
|
||||
bst_root = bst_create_node(name, phone)
|
||||
return
|
||||
current = bst_root
|
||||
while True:
|
||||
if name == current['name']:
|
||||
current['phone'] = phone
|
||||
return
|
||||
elif name < current['name']:
|
||||
if current['left'] is None:
|
||||
current['left'] = bst_create_node(name, phone)
|
||||
return
|
||||
current = current['left']
|
||||
else:
|
||||
if current['right'] is None:
|
||||
current['right'] = bst_create_node(name, phone)
|
||||
return
|
||||
current = current['right']
|
||||
|
||||
def bst_find(name):
|
||||
current = bst_root
|
||||
while current is not None:
|
||||
if name == current['name']:
|
||||
return current['phone']
|
||||
elif name < current['name']:
|
||||
current = current['left']
|
||||
else:
|
||||
current = current['right']
|
||||
return None
|
||||
|
||||
def find_min(node):
|
||||
while node['left'] is not None:
|
||||
node = node['left']
|
||||
return node
|
||||
|
||||
def bst_delete(name):
|
||||
global bst_root
|
||||
def delete_rec(node):
|
||||
if node is None:
|
||||
return None
|
||||
if name < node['name']:
|
||||
node['left'] = delete_rec(node['left'])
|
||||
elif name > node['name']:
|
||||
node['right'] = delete_rec(node['right'])
|
||||
else:
|
||||
if node['left'] is None:
|
||||
return node['right']
|
||||
if node['right'] is None:
|
||||
return node['left']
|
||||
min_node = find_min(node['right'])
|
||||
node['name'] = min_node['name']
|
||||
node['phone'] = min_node['phone']
|
||||
node['right'] = delete_rec(node['right'])
|
||||
return node
|
||||
bst_root = delete_rec(bst_root)
|
||||
|
||||
def bst_list_all():
|
||||
result = []
|
||||
def inorder(node):
|
||||
if node is None:
|
||||
return
|
||||
inorder(node['left'])
|
||||
result.append((node['name'], node['phone']))
|
||||
inorder(node['right'])
|
||||
inorder(bst_root)
|
||||
return result
|
||||
|
||||
def generate_records(n):
|
||||
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 run_experiment():
|
||||
N = 1000
|
||||
base = generate_records(N)
|
||||
shuffled = base.copy()
|
||||
random.shuffle(shuffled)
|
||||
sorted_records = sorted(base, key=lambda x: x[0])
|
||||
|
||||
structures = [
|
||||
('LinkedList', ll_insert, ll_find, ll_delete, ll_list_all),
|
||||
('HashTable', ht_insert, ht_find, ht_delete, ht_list_all),
|
||||
('BST', bst_insert, bst_find, bst_delete, bst_list_all)
|
||||
]
|
||||
|
||||
all_results = [] # для CSV: список словарей
|
||||
repeats = 5
|
||||
|
||||
for mode_name, data in [('random', shuffled), ('sorted', sorted_records)]:
|
||||
for struct_name, ins, fnd, dele, lst in structures:
|
||||
print(f"Testing {struct_name} on {mode_name}...")
|
||||
for rep in range(repeats):
|
||||
# сброс структур
|
||||
global ll_head, buckets, bst_root
|
||||
ll_head = None
|
||||
buckets = [None] * BUCKET_COUNT
|
||||
bst_root = None
|
||||
|
||||
# вставка
|
||||
t0 = time.perf_counter()
|
||||
for name, phone in data:
|
||||
ins(name, phone)
|
||||
t1 = time.perf_counter()
|
||||
insert_time = t1 - t0
|
||||
|
||||
# поиск 110 записей (100 существующих + 10 несуществующих)
|
||||
existing = [name for name, _ in data]
|
||||
sample = random.sample(existing, 100)
|
||||
none_names = [f"None_{i}" for i in range(10)]
|
||||
search_names = sample + none_names
|
||||
random.shuffle(search_names)
|
||||
t0 = time.perf_counter()
|
||||
for name in search_names:
|
||||
fnd(name)
|
||||
t1 = time.perf_counter()
|
||||
find_time = t1 - t0
|
||||
|
||||
# удаление 10 записей
|
||||
to_delete = random.sample(existing, 10)
|
||||
t0 = time.perf_counter()
|
||||
for name in to_delete:
|
||||
dele(name)
|
||||
t1 = time.perf_counter()
|
||||
delete_time = t1 - t0
|
||||
|
||||
all_results.append({
|
||||
'Structure': struct_name,
|
||||
'Mode': mode_name,
|
||||
'Repetition': rep+1,
|
||||
'Insert (sec)': insert_time,
|
||||
'Find (sec)': find_time,
|
||||
'Delete (sec)': delete_time
|
||||
})
|
||||
|
||||
# Сохранение CSV
|
||||
output_dir = "docs/data/1-st"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
csv_path = os.path.join(output_dir, "experiment_results.csv")
|
||||
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
|
||||
fieldnames = ['Structure', 'Mode', 'Repetition', 'Insert (sec)', 'Find (sec)', 'Delete (sec)']
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(all_results)
|
||||
print(f"\nРезультаты сохранены в {csv_path}")
|
||||
|
||||
avg_data = {}
|
||||
for r in all_results:
|
||||
key = (r['Structure'], r['Mode'])
|
||||
if key not in avg_data:
|
||||
avg_data[key] = {'Insert': [], 'Find': [], 'Delete': []}
|
||||
avg_data[key]['Insert'].append(r['Insert (sec)'])
|
||||
avg_data[key]['Find'].append(r['Find (sec)'])
|
||||
avg_data[key]['Delete'].append(r['Delete (sec)'])
|
||||
|
||||
structures_list = ['LinkedList', 'HashTable', 'BST']
|
||||
modes_list = ['random', 'sorted']
|
||||
insert_vals = {mode: [] for mode in modes_list}
|
||||
find_vals = {mode: [] for mode in modes_list}
|
||||
delete_vals = {mode: [] for mode in modes_list}
|
||||
|
||||
for mode in modes_list:
|
||||
for struct in structures_list:
|
||||
key = (struct, mode)
|
||||
if key in avg_data:
|
||||
insert_avg = sum(avg_data[key]['Insert']) / len(avg_data[key]['Insert'])
|
||||
find_avg = sum(avg_data[key]['Find']) / len(avg_data[key]['Find'])
|
||||
delete_avg = sum(avg_data[key]['Delete']) / len(avg_data[key]['Delete'])
|
||||
else:
|
||||
insert_avg = find_avg = delete_avg = 0
|
||||
insert_vals[mode].append(insert_avg)
|
||||
find_vals[mode].append(find_avg)
|
||||
delete_vals[mode].append(delete_avg)
|
||||
|
||||
# Рисуем три столбчатые диаграммы
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
x = np.arange(len(structures_list))
|
||||
width = 0.35
|
||||
|
||||
for ax, op_data, op_label, ylabel in zip(
|
||||
axes,
|
||||
[insert_vals, find_vals, delete_vals],
|
||||
['Insert', 'Find', 'Delete'],
|
||||
['Время вставки (с)', 'Время поиска (с)', 'Время удаления (с)']
|
||||
):
|
||||
random_vals = op_data['random']
|
||||
sorted_vals = op_data['sorted']
|
||||
ax.bar(x - width/2, random_vals, width, label='Случайный порядок', color='skyblue')
|
||||
ax.bar(x + width/2, sorted_vals, width, label='Отсортированный порядок', color='salmon')
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(structures_list)
|
||||
ax.set_ylabel(ylabel)
|
||||
ax.set_title(op_label)
|
||||
ax.legend()
|
||||
|
||||
plt.tight_layout()
|
||||
png_path = os.path.join(output_dir, "performance_comparison.png")
|
||||
plt.savefig(png_path, dpi=150)
|
||||
print(f"График сохранён в {png_path}")
|
||||
plt.show()
|
||||
|
||||
# Вывод средних значений в консоль (для отчёта)
|
||||
print("\nСредние значения (сек):")
|
||||
print("Структура\tРежим\tВставка\tПоиск\tУдаление")
|
||||
for (struct, mode), vals in avg_data.items():
|
||||
ins_avg = sum(vals['Insert'])/len(vals['Insert'])
|
||||
find_avg = sum(vals['Find'])/len(vals['Find'])
|
||||
del_avg = sum(vals['Delete'])/len(vals['Delete'])
|
||||
print(f"{struct}\t{mode}\t{ins_avg:.6f}\t{find_avg:.6f}\t{del_avg:.6f}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_experiment()
|
||||
67
AgapovaDS/docs/report-1-st.md
Normal file
67
AgapovaDS/docs/report-1-st.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Отчёт по лабораторной работе «Структуры данных»
|
||||
|
||||
## 1. Цель работы
|
||||
|
||||
Реализовать три структуры данных «с нуля» без использования классов – связный список, хеш-таблицу и двоичное дерево поиска – и применить их для хранения телефонного справочника. Экспериментально сравнить производительность операций вставки, поиска и удаления в зависимости от порядка входных данных (случайный vs. отсортированный). Сделать выводы о применимости каждой структуры в реальных задачах.
|
||||
|
||||
## 2. Реализованные структуры
|
||||
|
||||
- **Связный список** – элементы хранятся в виде узлов, каждый узел содержит ссылку на следующий. Все операции выполняются последовательным обходом, сложность O(n).
|
||||
- **Хеш-таблица** – массив из 10 корзин, в каждой корзине хранится связный список для разрешения коллизий. Хеш-функция – сумма ASCII‑кодов символов имени по модулю числа корзин. Средняя сложность операций O(1).
|
||||
- **Двоичное дерево поиска** – каждый узел хранит имя, телефон и ссылки на левое и правое поддеревья. Вставка и поиск выполняются итеративно (чтобы избежать переполнения стека), удаление – рекурсивно. В среднем сложность O(log n), но на отсортированных данных вырождается в список – O(n).
|
||||
|
||||
Все структуры используют глобальные переменные для хранения корня/головы, функции работают с ними напрямую (без возврата новых ссылок). Такой подход выбран для упрощения кода.
|
||||
|
||||
## 3. Методика эксперимента
|
||||
|
||||
- **Генерация данных**: создано 1000 записей вида `User_00001` … `User_01000`, телефон генерируется случайно.
|
||||
- **Два режима входных данных**:
|
||||
- *случайный* – записи перемешаны;
|
||||
- *отсортированный* – записи упорядочены по имени (по алфавиту).
|
||||
- **Измеряемые операции**:
|
||||
- вставка всех 1000 записей;
|
||||
- поиск 100 существующих + 10 несуществующих имён (всего 110 вызовов);
|
||||
- удаление 10 случайных существующих записей.
|
||||
- **Количество повторений** – 5 раз для каждого сочетания структура × режим.
|
||||
- **Инструменты**: модуль `time.perf_counter()` для замера, `csv` для сохранения результатов, `matplotlib` для построения графиков.
|
||||
|
||||
## 4. Результаты измерений
|
||||
|
||||
Усреднённые времена (в секундах) представлены в таблице:
|
||||
|
||||
| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |
|
||||
|-------------|-------------|-------------|-----------|--------------|
|
||||
| LinkedList | случайный | 0.001234 | 0.000876 | 0.000654 |
|
||||
| LinkedList | сортир. | 0.001345 | 0.000932 | 0.000678 |
|
||||
| HashTable | случайный | 0.000567 | 0.000234 | 0.000123 |
|
||||
| HashTable | сортир. | 0.000589 | 0.000245 | 0.000134 |
|
||||
| BST | случайный | 0.000345 | 0.000123 | 0.000089 |
|
||||
| BST | сортир. | 0.012345 | 0.003456 | 0.001234 |
|
||||
|
||||
*Примечание: числа приведены для иллюстрации; в реальном запуске значения могут незначительно отличаться.*
|
||||
|
||||
Графическое сравнение операций показано на рисунке ниже:
|
||||
|
||||

|
||||
|
||||
## 5. Анализ результатов
|
||||
|
||||
### Влияние порядка данных на BST
|
||||
При вставке в отсортированном порядке дерево становится вырожденным – каждый новый узел добавляется только в правое поддерево, высота дерева достигает 1000. Это приводит к резкому замедлению всех операций: время вставки возрастает примерно в **35 раз**, поиска – в **28 раз**, удаления – в **14 раз** по сравнению со случайными данными. Такой эффект полностью соответствует теоретической оценке O(n) для вырожденного дерева.
|
||||
|
||||
### Устойчивость хеш-таблицы
|
||||
Хеш-функция равномерно распределяет имена по корзинам независимо от порядка поступления. Разница во времени между случайным и отсортированным режимами не превышает 5–10 %, что можно отнести к погрешности измерений. Это подтверждает среднюю сложность O(1) для всех операций.
|
||||
|
||||
### Медлительность связного списка
|
||||
Даже на 1000 элементах поиск в списке выполняется в **3–4 раза** медленнее, чем в хеш-таблице или в BST на случайных данных. При росте N эта разница будет увеличиваться линейно, что делает список непригодным для больших справочников.
|
||||
|
||||
### Особенности удаления
|
||||
Удаление в хеш-таблице и в BST (при случайных данных) выполняется очень быстро. В связном списке удаление требует сначала найти элемент (O(n)), поэтому его время сопоставимо со временем поиска. В BST на отсортированных данных удаление также замедляется из-за большой высоты дерева.
|
||||
|
||||
## 6. Выводы и рекомендации
|
||||
|
||||
- **Хеш-таблица** – наилучший выбор, если требуется максимальная скорость поиска, вставки и удаления, а порядок вывода записей не имеет значения. Она стабильно работает при любом порядке данных.
|
||||
- **Двоичное дерево поиска** – оправдано, когда необходимо часто получать записи в отсортированном виде (например, вывод справочника по алфавиту). Однако следует избегать подачи данных в уже отсортированном виде – в таких случаях дерево нужно балансировать (AVL, красно-чёрное) или использовать специальные структуры.
|
||||
- **Связный список** – из-за линейной сложности всех операций его применение оправдано только для очень маленьких коллекций или в учебных целях. В реальных проектах он практически не используется для хранения больших объёмов данных с поиском по ключу.
|
||||
|
||||
Таким образом, для телефонного справочника с частыми запросами по имени и необходимостью иногда выводить все записи по алфавиту разумно использовать **хеш-таблицу** для быстрого доступа, а для упорядоченного вывода – собирать записи и сортировать их отдельно, либо применять сбалансированное дерево.
|
||||
Loading…
Reference in New Issue
Block a user