diff --git a/svetlakovkyu/docs/report.md b/svetlakovkyu/docs/report.md new file mode 100644 index 0000000..a24e6e9 --- /dev/null +++ b/svetlakovkyu/docs/report.md @@ -0,0 +1,267 @@ +# Задание 1: Структуры данных + + +>Выполнил: Светлаков Кирилл +> +>Студент 426 группы + + + +## Цель работы + +Реализовать три различные структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. Необходимо собственными руками написать код, чтобы понять внутреннее устройство связного списка, хеш-таблицы и двоичного дерева поиска, а также осознать их сильные и слабые стороны на практике. + +--- + +## 1. Реализация структур данных + +### 1.1 Связный список (Linked List) + +```python +def ll_insert(head, name, phone): + current = head + while current is not None: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + return {'name': name, 'phone': phone, 'next': head} + +def ll_find(head, name): + current = head + while current is not None: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + +def ll_delete(head, name): + if head is None: + return None + current = head + if head['name'] == name: + return head['next'] + while current['next'] is not None: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + break + current = current['next'] + return head + +def ll_list_all(head): + res = [] + current = head + while current is not None: + res.append((current['name'], current['phone'])) + current = current['next'] + res.sort() + return res +``` + +--- + +### 1.2 Хеш-таблица (Hash Table) + +```python +def ht_insert(buckets, name, phone): + index = hash(name) % len(buckets) + current_head = buckets[index] + new_head = ll_insert(current_head, name, phone) + buckets[index] = new_head + +def ht_find(buckets, name): + index = hash(name)%len(buckets) + slot_head = buckets[index] + res_ph = ll_find(slot_head, name) + return res_ph + +def ht_delete(buskets, name): + index = hash(name)%len(buskets) + buskets[index] = ll_delete(buskets[index], name) + +def ht_list_all(buckets): + all_rec = [] + for head in buckets: + current = head + while current is not None: + all_rec.append((current['name'], current['phone'])) + current = current['next'] + all_rec.sort() + return all_rec +``` + +--- + +### 1.3 Двоичное дерево поиска (BST) +```python +def bst_insert(root, name, phone): + if root is None: + return {'name': name, 'phone': phone, 'left': None, 'right': None} + if name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + elif name > root['name']: + root['right'] = bst_insert(root['right'], name, phone) + else: + root['phone'] = phone + return root + +def bst_find(root, name): + if root is None: + return None + if name == root['name']: + return root['phone'] + + if name < root['name']: + return bst_find(root['left'], name) + else: + return bst_find(root['right'], name) + +def get_min(node): + current = node + while current['left'] is not None: + current = current['left'] + return current + +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'] + successor = get_min(root['right']) + root['name'] = successor['name'] + root['phone'] = successor['phone'] + root['right'] = bst_delete(root['right'], successor['name']) + return root + +def bst_list_all(root, res = None): + if res is None: + res = [] + if root is not None: + bst_list_all(root['left'], res) + res.append({'name': root['name'], 'phone': root['phone']}) + bst_list_all(root['right'], res) + #сортировка уже сделана + return res +``` + +--- + +## 2. Эксперимент + +Для экспериментального сравнения были сгенерированы 10 000 записей вида `(User_XXXXX, номер_телефона)`. + +Каждый тест проводился в двух режимах входных данных: +- Случайный (shuffled) - записи перемешаны в произвольном порядке +- Отсортированный (sorted) - записи отсортированы по имени + +Для каждой структуры и режима измерялось время выполнения трёх операций: +- Вставка - 10 000 элементов +- Поиск - 110 запросов (100 существующих + 10 несуществующих) +- Удаление - 50 элементов + +Каждый замер повторялся 5 раз, итоговое значение - среднее арифметическое. + +В файл `results.py` записывались в следующем виде +| Structure | Mode | Operation | Time | +|---|---|---|---| +| Название структуры: LL, BST, HT | Режим данных: shufled, sorted | Операция и номер попытки или среднее | Время выполнения в секундах | +| LL | shufled | Вставка (попытка 1) | 2.730272 | +| LL | shufled | Вставка (попытка 2) | 2.675253 | +| LL | shufled | Вставка (попытка 3) | 2.628982 | +| LL | shufled | Вставка (попытка 4) | 2.673355 | +| LL | shufled | Вставка (попытка 5) | 2.636129 | +| LL | shufled | Вставка СРЕДНЕЕ | 2.668798 | +| ... | ... | ... | ... | + +--- + + +## 3. Результаты + +### 3.1 Вставка + +![График 1](insert_plot.png) + + +### 3.2 Поиск + +![График 2](search_plot.png) + + +### 3.3 Удаление + +![График 3](delete_plot.png) + + +--- + +## 4. Анализ результатов + +### 4.1 Влияние порядка данных на BST: деградация до O(n) + +При вставке случайных данных BST ведёт себя как сбалансированное дерево: каждый новый ключ с равной вероятностью уходит влево или вправо, глубина дерева составляет ~log₂(10000) ≈ 13. Операция вставки 10 000 записей заняла 0.025 сек. + +При вставке отсортированных данных каждый новый ключ оказывается больше предыдущего и всегда уходит вправо. Дерево вырождается в линейный список глубиной 10 000. Каждая следующая вставка проходит на один шаг дальше, итоговая сложность становится O(1 + 2 + ... + n) = O(n²). Именно поэтому вставка отсортированных данных заняла 13.16 сек - более чем в 500 раз медленнее случайных. + +Это фундаментальный недостаток, реализованного BST. + +--- + +### 4.2 Нечувствительность хеш-таблицы к порядку данных + +Хеш-таблица вычисляет позицию элемента через `hash(name) % len(buckets)`. Функция `hash()` в Python зависит только от значения ключа, но никак не от порядка вставки. Независимо от того, отсортированы данные или перемешаны, каждый ключ попадает в тот же бакет с той же скоростью. + +Это подтверждается полученными результатами: время вставки для случайных данных - 0.0281 сек, для отсортированных - 0.0286 сек, что практически одинаково. + +В графике для поиска и удаления, моожно заметить примено такой же результат - время работы программы для отсортированных и не для неотсортированных данных одинаково. + +--- + +### 4.3 Медленный поиск в связном списке + +Связный список не имеет структуры, позволяющей перейти к нужному элементу. При поиске приходится последовательно проходить узел за узлом от головы до нужного элемента - это линейный поиск O(n). + +При N = 10 000 и 110 запросах среднее время поиска составило 0.03 сек, в ~150 раз медленнее поиска в BST и в ~75 раз медленнее поиска в HT на случайных данных. + +Порядок данных на LL практически не влияет: в любом случае нужно проходить примерно половину списка для найденных и весь список для несуществующих записей. + +--- + +### 4.4 Удаление в каждой структуре + +Связный список: удаление требует сначала найти удаляемый элемент - O(n). Затем достаточно переключить одну ссылку у предшественника. Итог: O(n) за счёт поиска. При 50 удалениях время составило ~0.02 сек. + +Хеш-таблица: вычисляем бакет за O(1), затем удаляем элемент из короткой цепочки. Итог: O(1) амортизированно. При 50 удалениях время составило 0.00003 сек для случайных и 0.00004 для отсортированых данных, что значительно быстрее связного списка, но медленее BST(для неотсортированных данных). + +BST (случайные данные): находим узел за O(log n). Если у него два потомка - находим минимум правого поддерева, копируем его значение и рекурсивно удаляем его. Итог: O(log n). При 50 удалениях время составило 0.00012 сек. + +BST (отсортированные данные): дерево вырождено в список, глубина O(n). Каждое удаление - O(n), 50 удалений - 0.060 сек, что медленнее даже связного списка. + +--- + +## 5. Выводы + +Из результатов эксперимента, можно сделать вывод, что нет универсальной структуры данных, и под конкретную задачу надо выбирать определенную. + + +Хеш-таблица - лучший выбор, если нужны быстрые вставка, поиск и удаление и порядок хранения данных не важен. Идеальна для кешей, словарей, телефонных справочников, где операции выполняются в O(1). Не подходит для задач, требующих обхода данных в отсортированном порядке. + +BST - лучший выбор, когда важен порядок данных: обход дерева in-order даёт отсортированную последовательность за O(n), можно быстро найти минимум/максимум или диапазон ключей. Подходит для задач типа «найти все записи от A до B». Критически важно использовать только на случайных или специально перемешанных данных. + +Связный список - подходит для задач, где данные постоянно добавляются и удаляются с известной позиции (начало/конец списка), а поиск по значению происходит редко. В телефонном справочнике с 10 000 записей является наихудшим вариантом из трёх. + +| Задача | Лучшая структура | +|---|---| +| Частые вставки и удаления по ключу | Хеш-таблица | +| Частый поиск по ключу | Хеш-таблица | +| Обход данных в отсортированном порядке | BST | +| Поиск по диапазону ключей | BST | +| Встава/удаление в начало/конец | Связный список | +| Данные приходят отсортированными, нужен быстрый поиск | Хеш-таблица |