diff --git a/novikovsd/answers.txt b/novikovsd/answers.txt new file mode 100644 index 0000000..2929d70 --- /dev/null +++ b/novikovsd/answers.txt @@ -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 деградирует на упорядоченных данных, поэтому в реальной жизни используют сбалансированные деревья. + +Вывод: Если нужен только доступ по ключу – хеш‑таблица. Если нужен отсортированный вывод или диапазонные запросы – сбалансированное дерево. Связный список для этой задачи неприменим. \ No newline at end of file diff --git a/novikovsd/docs/comparison.png b/novikovsd/docs/comparison.png new file mode 100644 index 0000000..f5391f3 Binary files /dev/null and b/novikovsd/docs/comparison.png differ diff --git a/novikovsd/docs/data/results.csv b/novikovsd/docs/data/results.csv new file mode 100644 index 0000000..8f3fbef --- /dev/null +++ b/novikovsd/docs/data/results.csv @@ -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 diff --git a/novikovsd/hashtab.py b/novikovsd/hashtab.py new file mode 100644 index 0000000..d889419 --- /dev/null +++ b/novikovsd/hashtab.py @@ -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) \ No newline at end of file