1. Цель работы Реализовать три базовые структуры данных без использования объектно-ориентированных механизмов, применить их для хранения записей телефонного справочника, экспериментально измерить производительность операций вставки, поиска и удаления, а также проанализировать влияние порядка входных данных на время выполнения. Связный список (LinkedListPhoneBook) Узел: {'name': str, 'phone': str, 'next': dict | None} Операции: ll_insert: линейный проход до конца, обновление при совпадении имени, вставка нового узла в хвост. Возвращает голову списка. ll_find: последовательный перебор до первого совпадения. ll_delete: поиск предшественника удаляемого узла, переназначение ссылки next. ll_list_all: сбор записей в список, явная сортировка по имени. Хеш-таблица с цепочками (HashTable) Структура: список из BUCKET_COUNT = 1024 элементов, каждый элемент — голова связного списка. Хеширование: idx = hash(name) % BUCKET_COUNT Операции: делегируют соответствующим ll_* функциям для конкретного бакета. Узел: {'name': str, 'phone': str, 'left': dict | None, 'right': dict | None} Операции: bst_insert: рекурсивное сравнение имён, создание листа при достижении None. bst_find: рекурсивный спуск влево/вправо в зависимости от результата сравнения. bst_delete: три случая: 0 потомков, 1 потомок, 2 потомка. При двух потомках используется inorder-преемник (минимальный элемент правого поддерева). bst_list_all: центрированный (in-order) обход, гарантирующий отсортированный вывод без дополнительной сортировки. Влияние порядка входных данных на скорость вставки в BST Двоичное дерево поиска (BST) поддерживает инвариант: left.name < root.name < right.name. При вставке новых узлов алгоритм рекурсивно спускается по дереву, выбирая левую или правую ветвь в зависимости от результата сравнения. Случай 1: Случайный порядок данных Ключи распределяются по дереву хаотично Левые и правые поддеревья заполняются примерно равномерно Высота дерева: h ≈ log₂(N) ≈ 14 для N=10 000 Сложность вставки одного элемента: O(log N) Общая сложность вставки всех N элементов: O(N log N) Случай 2: Отсортированный порядок данных Каждый следующий ключ больше всех предыдущих Алгоритм всегда выбирает правую ветвь Дерево вырождается в линейную цепочку Почему хеш-таблица почти не чувствительна к порядку? Функция hash() в Python: Детерминирована: один и тот же ключ → один и тот же хеш Равномерно распределяет значения по пространству хешей Не зависит от порядка вызова: hash("User_00001") всегда одинаков Распределение по бакетам При N=10 000 записей и 1024 бакетах: Ожидаемая загрузка: α = N / BUCKET_COUNT ≈ 9.77 элементов на бакет Даже если все ключи отсортированы, их хеши «размазываются» по всему диапазону Внутри каждого бакета хранится короткий связный список (~10 элементов) Почему связный список всегда медленен при поиске? Связный список хранит элементы последовательно, без индексации Для поиска элемента с именем X: Начать с головы списка Сравнить curr['name'] == X Если не совпало → перейти к curr['next'] Повторять до нахождения или конца списка Связный список не подходит для задач с частым поиском. Его удел очереди, стеки, или вспомогательная роль внутри других структур. Как удаление работает в каждой структуре? Связный список def ll_delete(head, name): if head['name'] == name: return head['next'] curr = head while curr['next']: if curr['next']['name'] == name: curr['next'] = curr['next']['next'] return head curr = curr['next'] return head Поиск узла (или его предшественника) — O(N) Переназначение ссылки next — O(1) Сборка мусора (автоматически в Python) Хеш-таблица def ht_delete(buckets, name): idx = hash(name) % BUCKET_COUNT buckets[idx] = ll_delete(buckets[idx], name) Вычисление индекса бакета — O(1) Поиск и удаление в связном списке бакета — O(L), где L ≈ 10 Итого: O(1) в среднем Двоичное дерево поиска def bst_delete(root, name): # 1. Поиск узла if name < root['name']: root['left'] = bst_delete(root['left'], name) elif name > root['name']: root['right'] = bst_delete(root['right'], name) else: # 2. Три случая удаления if root['left'] is None: return root['right'] # 0 или 1 потомок elif root['right'] is None: return root['left'] else: # 2 потомка: найти inorder-преемника successor = _bst_find_min(root['right']) root['name'] = successor['name'] root['phone'] = successor['phone'] root['right'] = bst_delete(root['right'], successor['name']) return root Поиск удаляемого узла — O(h) Обработка случая: 0 потомков: просто удалить узел 1 потомок: «поднять» потомка на место удаляемого 2 потомка: найти минимум в правом поддереве (inorder-преемник), скопировать его данные, рекурсивно удалить преемника Возврат обновлённого корня поддерева Когда какую структуру использовать? | Сценарий | Рекомендация | |---|---| | **Частый поиск** по имени | HashTable или BST (случайные данные) | | **Данные приходят отсортированными** | HashTable (BST деградирует!) | | **Нужен отсортированный список** | BST (in-order обход — бесплатный) | | **Частые вставки/удаления + поиск** | HashTable | | **Минимальная память, простота** | LinkedList (для малых N) | | **Диапазонные запросы** (все имена A–M) | BST | ### Сложности операций | Структура | Insert | Find | Delete | List (sorted) | |---|---|---|---|---| | LinkedList | O(n) | O(n) | O(n) | O(n log n) | | HashTable | O(1) avg | O(1) avg | O(1) avg | O(n log n) | | BST (сбалансированный) | O(log n) | O(log n) | O(log n) | O(n) | | BST (вырожденный) | O(n) | O(n) | O(n) | O(n) | HashTable — лучший выбор для телефонного справочника при частых вставках и поисках. BST лучше HashTable только если нужен отсортированный вывод без дополнительной сортировки — но при условии случайного порядка вставки или использования самобалансирующегося дерева (AVL, Red-Black).