2026-rff_mp/fomichevks/docs/отчет.txt
2026-05-19 21:14:37 +03:00

138 lines
9.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) |
| **Диапазонные запросы** (все имена AM) | 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).