Merge pull request '[1]' (#294) from novikovsd/2026-rff_mp:lab1 into develop
Reviewed-on: #294
This commit is contained in:
commit
34eade7e8d
25
novikovsd/answers.txt
Normal file
25
novikovsd/answers.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
В двоичном дереве поиска (BST) порядок добавления элементов определяет форму дерева. Если данные поступают в случайном порядке, дерево получается примерно сбалансированным: высота ~ O(log N), и вставка выполняется за O(log N) в среднем.
|
||||
|
||||
Если же данные отсортированы (по возрастанию или убыванию), каждый новый элемент становится либо самым правым, либо самым левым потомком. Дерево вырождается в линейный связный список (так называемое "вырожденное дерево"). В этом случае вставка каждого нового элемента требует прохода по всем уже вставленным узлам, то есть O(N) на операцию. Суммарная вставка N элементов – O(N²). В эксперименте для отсортированного режима BST будет работать значительно медленнее, чем для случайного.
|
||||
|
||||
В моей реализации bst_insert итеративная, но алгоритм сохраняет эту зависимость: при отсортированных именах (User_00000, User_00001, …) каждый новый ключ больше всех предыдущих, поэтому поиск места вставки каждый раз обходит всю текущую цепочку правых потомков, что приводит к квадратичной сложности.
|
||||
|
||||
Хеш‑таблица вычисляет индекс (хеш) от имени и сразу помещает запись в соответствующую корзину. Порядок поступления данных никак не влияет на значение хеша и распределение по корзинам. Даже если имена идут подряд (отсортированы), хеш‑функция (например, (h*31 + ord(ch)) % size) рассеивает их по разным индексам почти равномерно. Поэтому время вставки, поиска и удаления остаётся O(1) в среднем (с учётом разрешения коллизий цепочками), независимо от того, отсортированы данные или перемешаны.
|
||||
|
||||
В связном списке поиск элемента по имени требует последовательного просмотра узлов от головы до тех пор, пока не найдётся нужный или не достигнут конец. В худшем случае (элемент отсутствует или находится в конце) нужно проверить все N узлов. Сложность поиска – O(N) в среднем. Никакая предобработка или порядок вставки не улучшают этот показатель, потому что структура не поддерживает эффективного индексирования. Даже если список отсортировать вручную (но у нас нет сортировки при вставке), поиск останется линейным, так как нельзя выполнить бинарный поиск без возможности прямого доступа по индексу.
|
||||
|
||||
· Связный список: удаление узла по имени требует линейного поиска (O(N)). Найденный узел исключается перенаправлением указателя предыдущего узла на следующий. Для удаления головы – особая обработка. В моей реализации ll_delete возвращает новую голову. Время – O(N).
|
||||
· Хеш‑таблица: удаление сводится к вычислению индекса корзины (O(1)) и вызову ll_delete для связного списка этой корзины. Средняя длина цепочки – N / bucket_count (обычно небольшая константа). Поэтому удаление в среднем O(1). В коде: buckets[idx] = ll_delete(buckets[idx], name).
|
||||
· BST: удаление сложнее. Сначала ищется узел (O(log N) в сбалансированном дереве, O(N) – в вырожденном). Если узел найден, то:
|
||||
· Нет потомков – просто удаляем.
|
||||
· Один потомок – заменяем удаляемый узел на потомка.
|
||||
· Два потомка – находим минимальный узел в правом поддереве (или максимальный в левом), копируем его данные в удаляемый узел, затем рекурсивно удаляем этот минимальный узел.
|
||||
Моя реализация bst_delete рекурсивна, сложность совпадает со сложностью поиска. Для сбалансированного дерева – O(log N), для вырожденного – O(N).
|
||||
|
||||
· Связный список – использовать, только если нужны частые вставки/удаления в начало/конец (например, очередь или стек) и поиск почти не требуется. Для телефонного справочника он не пригоден из-за линейного поиска.
|
||||
· Хеш‑таблица – идеальный выбор для задач, где важны быстрые вставка, поиск и удаление по ключу (O(1) в среднем) и не требуется получать записи в отсортированном порядке. Примеры: словари, кэши, таблицы символов, базы данных «ключ‑значение». Телефонный справочник с частыми поисками по имени – отличное
|
||||
|
||||
|
||||
· BST (особенно самобалансирующиеся варианты, такие как AVL или красно‑чёрное дерево) – выбирают, когда нужны обе возможности: быстрый поиск (O(log N)) и возможность прохода по данным в отсортированном порядке без дополнительной сортировки. Также дерево может поддерживать операции поиска диапазона («все имена между A и B»). Но для простого справочника без требования сортировки на лету хеш‑таблица обычно предпочтительнее из-за константного времени. В данной реализации простой BST деградирует на упорядоченных данных, поэтому в реальной жизни используют сбалансированные деревья.
|
||||
|
||||
Вывод: Если нужен только доступ по ключу – хеш‑таблица. Если нужен отсортированный вывод или диапазонные запросы – сбалансированное дерево. Связный список для этой задачи неприменим.
|
||||
BIN
novikovsd/docs/comparison.png
Normal file
BIN
novikovsd/docs/comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
91
novikovsd/docs/data/results.csv
Normal file
91
novikovsd/docs/data/results.csv
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
Structure,Mode,Operation,Repeat,Time_sec
|
||||
LinkedList,shuffled,insert,1,2.7093895999996676
|
||||
LinkedList,shuffled,find,1,0.03374979999989591
|
||||
LinkedList,shuffled,delete,1,0.013558000000102766
|
||||
LinkedList,shuffled,insert,2,2.7178337999998803
|
||||
LinkedList,shuffled,find,2,0.034134699999867735
|
||||
LinkedList,shuffled,delete,2,0.014517000000068947
|
||||
LinkedList,shuffled,insert,3,2.714019300000018
|
||||
LinkedList,shuffled,find,3,0.033463599999777216
|
||||
LinkedList,shuffled,delete,3,0.013073999999960506
|
||||
LinkedList,shuffled,insert,4,2.7287836000000425
|
||||
LinkedList,shuffled,find,4,0.03680309999981546
|
||||
LinkedList,shuffled,delete,4,0.015249600000061037
|
||||
LinkedList,shuffled,insert,5,2.722151500000109
|
||||
LinkedList,shuffled,find,5,0.03397850000010294
|
||||
LinkedList,shuffled,delete,5,0.015159399999902234
|
||||
LinkedList,sorted,insert,1,2.5469852999999603
|
||||
LinkedList,sorted,find,1,0.054332700000031764
|
||||
LinkedList,sorted,delete,1,0.013600199999928009
|
||||
LinkedList,sorted,insert,2,2.5274411999998847
|
||||
LinkedList,sorted,find,2,0.05538109999997687
|
||||
LinkedList,sorted,delete,2,0.014902900000379304
|
||||
LinkedList,sorted,insert,3,2.516689800000222
|
||||
LinkedList,sorted,find,3,0.05497689999992872
|
||||
LinkedList,sorted,delete,3,0.012883400000191614
|
||||
LinkedList,sorted,insert,4,2.528048000000126
|
||||
LinkedList,sorted,find,4,0.05493479999995543
|
||||
LinkedList,sorted,delete,4,0.012835600000016711
|
||||
LinkedList,sorted,insert,5,2.524865200000022
|
||||
LinkedList,sorted,find,5,0.05850929999996879
|
||||
LinkedList,sorted,delete,5,0.015247499999986758
|
||||
HashTable,shuffled,insert,1,0.014068699999825185
|
||||
HashTable,shuffled,find,1,0.00015149999990171636
|
||||
HashTable,shuffled,delete,1,7.469999991371878e-05
|
||||
HashTable,shuffled,insert,2,0.014089899999817135
|
||||
HashTable,shuffled,find,2,0.00014630000032411772
|
||||
HashTable,shuffled,delete,2,7.090000008247443e-05
|
||||
HashTable,shuffled,insert,3,0.013962699999865436
|
||||
HashTable,shuffled,find,3,0.00014389999978448031
|
||||
HashTable,shuffled,delete,3,6.919999987076153e-05
|
||||
HashTable,shuffled,insert,4,0.01387350000004517
|
||||
HashTable,shuffled,find,4,0.00014590000000680448
|
||||
HashTable,shuffled,delete,4,7.129999994504033e-05
|
||||
HashTable,shuffled,insert,5,0.014038799999980256
|
||||
HashTable,shuffled,find,5,0.00014629999986937037
|
||||
HashTable,shuffled,delete,5,7.400000004054164e-05
|
||||
HashTable,sorted,insert,1,0.01933809999991354
|
||||
HashTable,sorted,find,1,0.0001700000002529123
|
||||
HashTable,sorted,delete,1,8.489999981975416e-05
|
||||
HashTable,sorted,insert,2,0.014241200000014942
|
||||
HashTable,sorted,find,2,0.00016050000022005406
|
||||
HashTable,sorted,delete,2,7.110000024113106e-05
|
||||
HashTable,sorted,insert,3,0.013520700000299257
|
||||
HashTable,sorted,find,3,0.0001594999998815183
|
||||
HashTable,sorted,delete,3,6.890000031489762e-05
|
||||
HashTable,sorted,insert,4,0.014047699999991892
|
||||
HashTable,sorted,find,4,0.00015880000000834116
|
||||
HashTable,sorted,delete,4,6.900000016685226e-05
|
||||
HashTable,sorted,insert,5,0.013919299999997747
|
||||
HashTable,sorted,find,5,0.0001606000000720087
|
||||
HashTable,sorted,delete,5,7.239999968078337e-05
|
||||
BST,shuffled,insert,1,0.021964499999739928
|
||||
BST,shuffled,find,1,0.00016349999987141928
|
||||
BST,shuffled,delete,1,0.00017139999999926658
|
||||
BST,shuffled,insert,2,0.022091499999987718
|
||||
BST,shuffled,find,2,0.00016019999975469545
|
||||
BST,shuffled,delete,2,0.00015999999959603883
|
||||
BST,shuffled,insert,3,0.02204540000002453
|
||||
BST,shuffled,find,3,0.00016659999982948648
|
||||
BST,shuffled,delete,3,0.00015170000006037299
|
||||
BST,shuffled,insert,4,0.022226300000056654
|
||||
BST,shuffled,find,4,0.00016219999997701962
|
||||
BST,shuffled,delete,4,0.0001567000003888097
|
||||
BST,shuffled,insert,5,0.021780500000204484
|
||||
BST,shuffled,find,5,0.00015780000012455275
|
||||
BST,shuffled,delete,5,0.0001606000000720087
|
||||
BST,sorted,insert,1,6.614551799999845
|
||||
BST,sorted,find,1,0.0005606999998235551
|
||||
BST,sorted,delete,1,0.0634210999996867
|
||||
BST,sorted,insert,2,6.625495499999943
|
||||
BST,sorted,find,2,0.0005660000001626031
|
||||
BST,sorted,delete,2,0.06643010000016147
|
||||
BST,sorted,insert,3,6.6205589999999575
|
||||
BST,sorted,find,3,0.0005686999998033571
|
||||
BST,sorted,delete,3,0.06744570000000749
|
||||
BST,sorted,insert,4,6.639703100000133
|
||||
BST,sorted,find,4,0.0005636999999296677
|
||||
BST,sorted,delete,4,0.0661270999999033
|
||||
BST,sorted,insert,5,6.6624039000002995
|
||||
BST,sorted,find,5,0.0005601999996542872
|
||||
BST,sorted,delete,5,0.057285699999738426
|
||||
|
306
novikovsd/hashtab.py
Normal file
306
novikovsd/hashtab.py
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import time
|
||||
import random
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.setrecursionlimit(30000)
|
||||
|
||||
def ll_insert(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': head}
|
||||
return new_node
|
||||
|
||||
|
||||
def ll_find(head, name):
|
||||
curr = head
|
||||
while curr is not None:
|
||||
if curr['name'] == name:
|
||||
return curr['phone']
|
||||
curr = curr['next']
|
||||
return None
|
||||
|
||||
|
||||
def ll_delete(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 ll_list_all(head):
|
||||
entries = []
|
||||
curr = head
|
||||
while curr is not None:
|
||||
entries.append((curr['name'], curr['phone']))
|
||||
curr = curr['next']
|
||||
entries.sort(key=lambda x: x[0])
|
||||
return entries
|
||||
|
||||
def _hash(name, bucket_count):
|
||||
h = 0
|
||||
for ch in name:
|
||||
h = (h * 31 + ord(ch)) % bucket_count
|
||||
return h
|
||||
|
||||
|
||||
def ht_create(bucket_count=2000):
|
||||
return [None] * bucket_count
|
||||
|
||||
|
||||
def ht_insert(buckets, name, phone):
|
||||
idx = _hash(name, len(buckets))
|
||||
buckets[idx] = ll_insert(buckets[idx], name, phone)
|
||||
|
||||
|
||||
def ht_find(buckets, name):
|
||||
idx = _hash(name, len(buckets))
|
||||
return ll_find(buckets[idx], name)
|
||||
|
||||
|
||||
def ht_delete(buckets, name):
|
||||
idx = _hash(name, len(buckets))
|
||||
buckets[idx] = ll_delete(buckets[idx], name)
|
||||
|
||||
|
||||
def ht_list_all(buckets):
|
||||
entries = []
|
||||
for head in buckets:
|
||||
curr = head
|
||||
while curr is not None:
|
||||
entries.append((curr['name'], curr['phone']))
|
||||
curr = curr['next']
|
||||
entries.sort(key=lambda x: x[0])
|
||||
return entries
|
||||
|
||||
def bst_insert(root, name, phone):
|
||||
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||||
if root is None:
|
||||
return new_node
|
||||
|
||||
parent = None
|
||||
curr = root
|
||||
while curr is not None:
|
||||
parent = curr
|
||||
if name < curr['name']:
|
||||
curr = curr['left']
|
||||
elif name > curr['name']:
|
||||
curr = curr['right']
|
||||
else:
|
||||
curr['phone'] = phone
|
||||
return root
|
||||
|
||||
if name < parent['name']:
|
||||
parent['left'] = new_node
|
||||
else:
|
||||
parent['right'] = new_node
|
||||
return root
|
||||
|
||||
|
||||
def bst_find(root, name):
|
||||
while root is not None:
|
||||
if name == root['name']:
|
||||
return root['phone']
|
||||
elif name < root['name']:
|
||||
root = root['left']
|
||||
else:
|
||||
root = root['right']
|
||||
return None
|
||||
|
||||
|
||||
def _bst_min_node(node):
|
||||
while node and 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 = _bst_min_node(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):
|
||||
def inorder(node, res):
|
||||
if node is None:
|
||||
return
|
||||
inorder(node['left'], res)
|
||||
res.append((node['name'], node['phone']))
|
||||
inorder(node['right'], res)
|
||||
result = []
|
||||
inorder(root, result)
|
||||
return result
|
||||
|
||||
def generate_test_data(n=10000):
|
||||
records = [(f"User_{i:05d}", f"+7-999-{i:05d}") for i in range(n)]
|
||||
records_sorted = records[:]
|
||||
records_shuffled = records[:]
|
||||
random.shuffle(records_shuffled)
|
||||
return records_sorted, records_shuffled
|
||||
|
||||
def measure_insert(struct_name, records):
|
||||
start = time.perf_counter()
|
||||
if struct_name == "LinkedList":
|
||||
head = None
|
||||
for name, phone in records:
|
||||
head = ll_insert(head, name, phone)
|
||||
obj = head
|
||||
elif struct_name == "HashTable":
|
||||
buckets = ht_create(bucket_count=2000)
|
||||
for name, phone in records:
|
||||
ht_insert(buckets, name, phone)
|
||||
obj = buckets
|
||||
elif struct_name == "BST":
|
||||
root = None
|
||||
for name, phone in records:
|
||||
root = bst_insert(root, name, phone)
|
||||
obj = root
|
||||
else:
|
||||
raise ValueError(f"Unknown structure: {struct_name}")
|
||||
elapsed = time.perf_counter() - start
|
||||
return elapsed, obj
|
||||
|
||||
def measure_find(obj, struct_name, existing_names, nonexisting_names):
|
||||
start = time.perf_counter()
|
||||
for name in existing_names:
|
||||
if struct_name == "LinkedList":
|
||||
ll_find(obj, name)
|
||||
elif struct_name == "HashTable":
|
||||
ht_find(obj, name)
|
||||
else:
|
||||
bst_find(obj, name)
|
||||
for name in nonexisting_names:
|
||||
if struct_name == "LinkedList":
|
||||
ll_find(obj, name)
|
||||
elif struct_name == "HashTable":
|
||||
ht_find(obj, name)
|
||||
else:
|
||||
bst_find(obj, name)
|
||||
return time.perf_counter() - start
|
||||
|
||||
def measure_delete(obj, struct_name, names_to_delete):
|
||||
start = time.perf_counter()
|
||||
if struct_name == "LinkedList":
|
||||
for name in names_to_delete:
|
||||
obj = ll_delete(obj, name)
|
||||
elif struct_name == "HashTable":
|
||||
for name in names_to_delete:
|
||||
ht_delete(obj, name)
|
||||
else:
|
||||
for name in names_to_delete:
|
||||
obj = bst_delete(obj, name)
|
||||
elapsed = time.perf_counter() - start
|
||||
return elapsed, obj
|
||||
|
||||
def run_experiment(n=10000, repeats=5):
|
||||
records_sorted, records_shuffled = generate_test_data(n)
|
||||
existing_names = [name for name, _ in records_sorted[:100]]
|
||||
nonexisting_names = [f"None_{i}" for i in range(10)]
|
||||
all_names = [name for name, _ in records_sorted]
|
||||
|
||||
structures = ["LinkedList", "HashTable", "BST"]
|
||||
modes = [("shuffled", records_shuffled), ("sorted", records_sorted)]
|
||||
|
||||
results = []
|
||||
for struct_name in structures:
|
||||
for mode_name, records in modes:
|
||||
for rep in range(repeats):
|
||||
insert_time, obj = measure_insert(struct_name, records)
|
||||
results.append([struct_name, mode_name, "insert", rep+1, insert_time])
|
||||
|
||||
find_time = measure_find(obj, struct_name, existing_names, nonexisting_names)
|
||||
results.append([struct_name, mode_name, "find", rep+1, find_time])
|
||||
|
||||
random.seed(rep)
|
||||
to_delete = random.sample(all_names, 50)
|
||||
delete_time, obj = measure_delete(obj, struct_name, to_delete)
|
||||
results.append([struct_name, mode_name, "delete", rep+1, delete_time])
|
||||
|
||||
return results
|
||||
|
||||
def save_results_to_csv(results, filename="docs/data/results.csv"):
|
||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||
with open(filename, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(["Structure", "Mode", "Operation", "Repeat", "Time_sec"])
|
||||
writer.writerows(results)
|
||||
print(f"Результаты сохранены в {filename}")
|
||||
|
||||
|
||||
def aggregate_results(results):
|
||||
from collections import defaultdict
|
||||
agg = defaultdict(list)
|
||||
for row in results:
|
||||
struct, mode, op, rep, t = row
|
||||
agg[(struct, mode, op)].append(t)
|
||||
means = {k: sum(v)/len(v) for k, v in agg.items()}
|
||||
return means
|
||||
|
||||
|
||||
def plot_results(means, output_dir="docs"):
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("Matplotlib не установлен. Графики не построены.")
|
||||
return
|
||||
|
||||
operations = ["insert", "find", "delete"]
|
||||
structures = ["LinkedList", "HashTable", "BST"]
|
||||
modes = ["shuffled", "sorted"]
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
for idx, op in enumerate(operations):
|
||||
ax = axes[idx]
|
||||
x = np.arange(len(structures))
|
||||
width = 0.35
|
||||
shuffled_means = [means.get((struct, "shuffled", op), 0) for struct in structures]
|
||||
sorted_means = [means.get((struct, "sorted", op), 0) for struct in structures]
|
||||
ax.bar(x - width/2, shuffled_means, width, label='случайный порядок', color='skyblue')
|
||||
ax.bar(x + width/2, sorted_means, width, label='отсортированный порядок', color='salmon')
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(structures, rotation=15)
|
||||
ax.set_ylabel('Время (сек)')
|
||||
ax.set_title(f'{op.upper()}')
|
||||
ax.legend()
|
||||
ax.grid(axis='y', linestyle='--', alpha=0.7)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(output_dir, "comparison.png"), dpi=150)
|
||||
plt.show()
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = run_experiment(n=10000, repeats=5)
|
||||
save_results_to_csv(results)
|
||||
means = aggregate_results(results)
|
||||
print("\nСреднее время по операциям (сек):")
|
||||
for (struct, mode, op), t in sorted(means.items()):
|
||||
print(f"{struct:12} {mode:8} {op:6} : {t:.6f}")
|
||||
plot_results(means)
|
||||
Loading…
Reference in New Issue
Block a user