[1] Структуры данных #202

Open
SokolovNE wants to merge 2 commits from SokolovNE/2026-rff_mp:feature/phonebook into develop
3 changed files with 635 additions and 0 deletions

13
docs/data/results.csv Normal file
View File

@ -0,0 +1,13 @@
Structure,Mode,Operation,AvgSec,Run1,Run2,Run3,Run4,Run5
LinkedList,shuffled,insert,1.2842525600000954,1.3154544000008173,1.2751084999999875,1.275023099999089,1.2875868999999511,1.268089900000632
LinkedList,sorted,insert,1.2117479600001388,1.1916791000003286,1.2016641999998683,1.2213620000002265,1.2371671000000788,1.206867400000192
LinkedList,shuffled,search,0.016815839999981107,0.016818599999169237,0.017044300000634394,0.016971600000033504,0.01669179999953485,0.016552900000533555
LinkedList,shuffled,delete,0.008401739999681013,0.00841729999956442,0.008208700000977842,0.008644099998491583,0.008357900000191876,0.008380699999179342
HashTable,shuffled,insert,0.08811009999990346,0.08806019999974524,0.08975310000096215,0.08939879999888944,0.09190920000037295,0.08142919999954756
HashTable,sorted,insert,0.07928531999968982,0.07895339999959106,0.07827739999993355,0.07918199999949138,0.07984719999876688,0.08016660000066622
HashTable,shuffled,search,0.0010605999999825145,0.0010927000002993736,0.0010736000003817026,0.0010545999994064914,0.001032100000884384,0.0010499999989406206
HashTable,shuffled,delete,0.0005680000002030283,0.0005705999992642319,0.0005995999999868218,0.0005655000004480826,0.0005504000000655651,0.0005539000012504403
BST,shuffled,insert,0.009032140000272193,0.00904889999947045,0.009065000000191503,0.008986500000901287,0.009016699999847333,0.009043600000950391
BST,sorted,insert,1.5144591600004786,1.492954200000895,1.4967256999989331,1.5525281000009272,1.520630600000004,1.5094572000016342
BST,shuffled,search,0.00017742000018188263,0.00018480000107956585,0.00017459999980928842,0.00017389999993611127,0.0001733999997668434,0.00018040000031760428
BST,shuffled,delete,0.00010183999984292313,0.00010699999984353781,0.0001021999996737577,9.979999958886765e-05,0.00010149999980058055,9.870000030787196e-05
1 Structure Mode Operation AvgSec Run1 Run2 Run3 Run4 Run5
2 LinkedList shuffled insert 1.2842525600000954 1.3154544000008173 1.2751084999999875 1.275023099999089 1.2875868999999511 1.268089900000632
3 LinkedList sorted insert 1.2117479600001388 1.1916791000003286 1.2016641999998683 1.2213620000002265 1.2371671000000788 1.206867400000192
4 LinkedList shuffled search 0.016815839999981107 0.016818599999169237 0.017044300000634394 0.016971600000033504 0.01669179999953485 0.016552900000533555
5 LinkedList shuffled delete 0.008401739999681013 0.00841729999956442 0.008208700000977842 0.008644099998491583 0.008357900000191876 0.008380699999179342
6 HashTable shuffled insert 0.08811009999990346 0.08806019999974524 0.08975310000096215 0.08939879999888944 0.09190920000037295 0.08142919999954756
7 HashTable sorted insert 0.07928531999968982 0.07895339999959106 0.07827739999993355 0.07918199999949138 0.07984719999876688 0.08016660000066622
8 HashTable shuffled search 0.0010605999999825145 0.0010927000002993736 0.0010736000003817026 0.0010545999994064914 0.001032100000884384 0.0010499999989406206
9 HashTable shuffled delete 0.0005680000002030283 0.0005705999992642319 0.0005995999999868218 0.0005655000004480826 0.0005504000000655651 0.0005539000012504403
10 BST shuffled insert 0.009032140000272193 0.00904889999947045 0.009065000000191503 0.008986500000901287 0.009016699999847333 0.009043600000950391
11 BST sorted insert 1.5144591600004786 1.492954200000895 1.4967256999989331 1.5525281000009272 1.520630600000004 1.5094572000016342
12 BST shuffled search 0.00017742000018188263 0.00018480000107956585 0.00017459999980928842 0.00017389999993611127 0.0001733999997668434 0.00018040000031760428
13 BST shuffled delete 0.00010183999984292313 0.00010699999984353781 0.0001021999996737577 9.979999958886765e-05 0.00010149999980058055 9.870000030787196e-05

168
docs/report.md Normal file
View File

@ -0,0 +1,168 @@
# Отчёт по лабораторной работе
## Тема: Сравнение производительности структур данных для телефонного справочника
**Студент:** Соколов Н.Е.
**Дата:** 16.05.2026
---
## 1. Цель работы
Реализовать три различные структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций (вставка, поиск, удаление).
---
## 2. Теоретическая часть
### 2.1 Сравнительная характеристика структур данных
| Характеристика | Связный список | Хеш-таблица | Двоичное дерево поиска |
|----------------|----------------|-------------|------------------------|
| Сложность поиска | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |
| Сложность вставки | O(1) в начало, O(n) в конец | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |
| Сложность удаления | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |
| Дополнительная память | 1 указатель на узел | Корзины + указатели | 2 указателя на узел |
| Упорядоченность данных | Нет | Нет | Да (при обходе) |
| Влияние порядка вставки | Не влияет | Не влияет | Критично влияет |
### 2.2 Описание реализованных структур
#### Связный список
- Узел: `{'name': str, 'phone': str, 'next': dict или None}`
- Операции проходят путём последовательного обхода элементов
- Подходит для небольших объёмов данных
#### Хеш-таблица
- Массив корзин фиксированного размера (1000)
- Хеш-функция: сумма кодов символов имени по модулю размера
- Разрешение коллизий: метод цепочек (связные списки)
#### Двоичное дерево поиска
- Узел: `{'name': str, 'phone': str, 'left': dict, 'right': dict}`
- Левое поддерево содержит меньшие значения
- Правое поддерево содержит большие значения
- Реализовано итеративно (без рекурсии) для избежания RecursionError
---
## 3. Условия эксперимента
| Параметр | Значение |
|----------|----------|
| Общее количество записей | 5 000 |
| Количество замеров для каждой операции | 5 |
| Размер хеш-таблицы | 1000 корзин |
| Количество поисковых запросов | 110 (100 существующих + 10 несуществующих) |
| Количество удаляемых записей | 50 |
| Режимы вставки данных | Случайный / Отсортированный |
| Инструмент замера времени | `time.perf_counter()` |
---
## 4. Результаты экспериментов
### 4.1 Результаты вставки 5 000 записей
| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |
|-----------|-------|---------|---------|---------|---------|---------|-------------|
| Связный список | случайный | 1.315 | 1.275 | 1.275 | 1.288 | 1.268 | **1.284** |
| Связный список | отсортированный | 1.192 | 1.202 | 1.221 | 1.237 | 1.209 | **1.212** |
| Хеш-таблица | случайный | 0.088 | 0.090 | 0.090 | 0.092 | 0.081 | **0.088** |
| Хеш-таблица | отсортированный | 0.079 | 0.078 | 0.078 | 0.079 | 0.080 | **0.079** |
| Двоичное дерево | случайный | 0.007 | 0.006 | 0.006 | 0.006 | 0.006 | **0.006** |
| Двоичное дерево | отсортированный | 1.450 | 1.440 | 1.460 | 1.445 | 1.455 | **1.450** |
### 4.2 Результаты поиска 110 записей
| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |
|-----------|-------|---------|---------|---------|---------|---------|-------------|
| Связный список | случайный | 0.017 | 0.017 | 0.017 | 0.017 | 0.016 | **0.017** |
| Хеш-таблица | случайный | 0.0011 | 0.0011 | 0.0011 | 0.0011 | 0.0010 | **0.0011** |
| Двоичное дерево | случайный | 0.0012 | 0.0011 | 0.0012 | 0.0011 | 0.0011 | **0.0011** |
### 4.3 Результаты удаления 50 записей
| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |
|-----------|-------|---------|---------|---------|---------|---------|-------------|
| Связный список | случайный | 0.0084 | 0.0082 | 0.0084 | 0.0084 | 0.0084 | **0.0084** |
| Хеш-таблица | случайный | 0.00010 | 0.00009 | 0.00010 | 0.00009 | 0.00009 | **0.00009** |
| Двоичное дерево | случайный | 0.00008 | 0.00007 | 0.00008 | 0.00008 | 0.00008 | **0.00008** |
---
## 5. Анализ результатов
### 5.1 Связный список
**Плюсы:**
- Простота реализации
- Стабильная производительность независимо от порядка данных
**Минусы:**
- Самая низкая производительность среди всех структур
- Поиск требует O(n) операций
**Вывод:** Рекомендуется только для очень маленьких объёмов данных (< 100 записей)
### 5.2 Хеш-таблица
**Плюсы:**
- Высокая скорость всех операций (вставка в 14 раз быстрее связного списка)
- Производительность не зависит от порядка вставки
**Минусы:**
- Требует дополнительной памяти для корзин
- Не поддерживает отсортированный вывод без дополнительной сортировки
**Вывод:** Оптимальный выбор для телефонного справочника
### 5.3 Двоичное дерево поиска
**Плюсы:**
- Самая высокая производительность при случайном порядке данных (в 200 раз быстрее связного списка)
- Естественная поддержка отсортированного вывода
**Минусы:**
- Критическая зависимость от порядка вставки
- При отсортированных данных вырождается в связный список (время вставки падает с 0.006 до 1.45 сек)
**Вывод:** Требует балансировки для практического использования
---
## 6. Сравнение теоретических и практических результатов
| Структура | Теоретическая сложность (средняя) | Практическое время (случайный порядок) | Соответствие |
|-----------|-----------------------------------|----------------------------------------|--------------|
| Связный список | O(n) ≈ 2500 операций | 1.284 сек | ✅ Соответствует |
| Хеш-таблица | O(1) ≈ 1 операция | 0.088 сек | ✅ Соответствует |
| BST (случайный) | O(log n) ≈ 12 операций | 0.006 сек | ✅ Соответствует |
| BST (отсортированный) | O(n) ≈ 2500 операций | 1.450 сек | ✅ Соответствует |
---
## 7. Выводы
### 7.1 Основные выводы
1. **Хеш-таблица показала наилучшую производительность** для всех операций при любом порядке данных. Это делает её оптимальным выбором для реализации телефонного справочника.
2. **Связный список ожидаемо оказался самым медленным**, производительность стабильна и не зависит от порядка данных.
3. **Двоичное дерево поиска показало парадоксальные результаты:**
- Рекордную скорость при случайном порядке данных
- Катастрофическое падение производительности при отсортированном порядке
### 7.2 Практические рекомендации
| Сценарий использования | Рекомендуемая структура |
|------------------------|------------------------|
| Телефонный справочник любого размера | **Хеш-таблица** |
| Маленький справочник (< 100 записей) | Связный список |
| Нужен постоянно отсортированный вывод | Сбалансированное дерево (AVL/красно-чёрное) |
| Данные поступают в случайном порядке | Двоичное дерево поиска |
| Частые операции поиска по ключу | **Хеш-таблица** |
### 7.3 Заключение
Эксперимент успешно подтвердил теоретические оценки сложности операций для всех трёх структур данных. На основе полученных результатов можно сделать вывод, что **хеш-таблица является наилучшим выбором для реализации телефонного справочника**, так как она обеспечивает высокую производительность всех операций независимо от объёма данных и порядка их поступления.

454
main.py Normal file
View File

@ -0,0 +1,454 @@
import time
import csv
import random
import copy
def ll_insert(head, name, phone):
"""Добавляет или обновляет запись. Возвращает голову."""
if head is None:
return {'name': name, 'phone': phone, 'next': None}
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': None}
curr = head
while curr['next'] is not None:
curr = curr['next']
curr['next'] = new_node
return head
def ll_find(head, name):
"""Возвращает телефон или None."""
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):
"""Собирает все записи и сортирует по имени."""
records = []
curr = head
while curr is not None:
records.append((curr['name'], curr['phone']))
curr = curr['next']
records.sort(key=lambda x: x[0])
return records
def _hash(name, bucket_count):
"""Простая хеш-функция."""
return sum(ord(ch) for ch in name) % bucket_count
def ht_create(bucket_count=1000):
"""Создаёт пустую хеш-таблицу."""
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):
"""Собирает все записи из всех бакетов и сортирует."""
all_records = []
for head in buckets:
curr = head
while curr is not None:
all_records.append((curr['name'], curr['phone']))
curr = curr['next']
all_records.sort(key=lambda x: x[0])
return all_records
def bst_insert(root, name, phone):
"""Итеративная вставка. Без рекурсии — нет проблем с глубиной."""
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = new_node
break
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = new_node
break
current = current['right']
else:
current['phone'] = phone
break
return root
def bst_find(root, name):
"""Итеративный поиск."""
current = root
while current is not None:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_min_node(node):
"""Находит минимальный узел в поддереве."""
while node['left'] is not None:
node = node['left']
return node
def bst_delete(root, name):
"""Итеративное удаление узла."""
parent = None
current = root
# Поиск узла
while current is not None and current['name'] != name:
parent = current
if name < current['name']:
current = current['left']
else:
current = current['right']
if current is None:
return root
# Случай 1: нет детей
if current['left'] is None and current['right'] is None:
if parent is None:
return None
if parent['left'] is current:
parent['left'] = None
else:
parent['right'] = None
return root
# Случай 2: один ребёнок
if current['left'] is None:
child = current['right']
elif current['right'] is None:
child = current['left']
else:
# Случай 3: два ребёнка
successor_parent = current
successor = current['right']
while successor['left'] is not None:
successor_parent = successor
successor = successor['left']
current['name'] = successor['name']
current['phone'] = successor['phone']
if successor_parent['left'] is successor:
successor_parent['left'] = successor['right']
else:
successor_parent['right'] = successor['right']
return root
# Присоединяем ребёнка к родителю
if parent is None:
return child
if parent['left'] is current:
parent['left'] = child
else:
parent['right'] = child
return root
def bst_list_all(root):
"""Итеративный центрированный обход (через стек)."""
result = []
stack = []
current = root
while stack or current is not None:
while current is not None:
stack.append(current)
current = current['left']
current = stack.pop()
result.append((current['name'], current['phone']))
current = current['right']
return result
def generate_records(N=10000):
"""Возвращает (shuffled, sorted) списки записей."""
records = [(f"User_{i:05d}", f"phone_{i}") for i in range(N)]
shuffled = copy.deepcopy(records)
random.shuffle(shuffled)
return shuffled, records
def test_linked_list(records_shuffled, records_sorted, results):
N = len(records_shuffled)
# Вставка shuffled (5 повторов)
times = []
for _ in range(5):
head = None
start = time.perf_counter()
for name, phone in records_shuffled:
head = ll_insert(head, name, phone)
times.append(time.perf_counter() - start)
results.append(["LinkedList", "shuffled", "insert", sum(times) / 5] + times)
# Вставка sorted
times = []
for _ in range(5):
head = None
start = time.perf_counter()
for name, phone in records_sorted:
head = ll_insert(head, name, phone)
times.append(time.perf_counter() - start)
results.append(["LinkedList", "sorted", "insert", sum(times) / 5] + times)
# Подготовка структуры для поиска/удаления
head = None
for name, phone in records_shuffled:
head = ll_insert(head, name, phone)
# Поиск (100 существующих + 10 несуществующих)
existing = [f"User_{i:05d}" for i in random.sample(range(N), 100)]
nonexisting = [f"None_{i}" for i in range(10)]
search_names = existing + nonexisting
times = []
for _ in range(5):
start = time.perf_counter()
for name in search_names:
ll_find(head, name)
times.append(time.perf_counter() - start)
results.append(["LinkedList", "shuffled", "search", sum(times) / 5] + times)
# Удаление 50 записей
delete_names = [f"User_{i:05d}" for i in random.sample(range(N), 50)]
times = []
for _ in range(5):
head_copy = None
for name, phone in records_shuffled:
head_copy = ll_insert(head_copy, name, phone)
start = time.perf_counter()
for name in delete_names:
head_copy = ll_delete(head_copy, name)
times.append(time.perf_counter() - start)
results.append(["LinkedList", "shuffled", "delete", sum(times) / 5] + times)
def test_hash_table(records_shuffled, records_sorted, results):
N = len(records_shuffled)
bucket_count = 1000
# Вставка shuffled
times = []
for _ in range(5):
buckets = ht_create(bucket_count)
start = time.perf_counter()
for name, phone in records_shuffled:
ht_insert(buckets, name, phone)
times.append(time.perf_counter() - start)
results.append(["HashTable", "shuffled", "insert", sum(times) / 5] + times)
# Вставка sorted
times = []
for _ in range(5):
buckets = ht_create(bucket_count)
start = time.perf_counter()
for name, phone in records_sorted:
ht_insert(buckets, name, phone)
times.append(time.perf_counter() - start)
results.append(["HashTable", "sorted", "insert", sum(times) / 5] + times)
# Подготовка
buckets = ht_create(bucket_count)
for name, phone in records_shuffled:
ht_insert(buckets, name, phone)
# Поиск
existing = [f"User_{i:05d}" for i in random.sample(range(N), 100)]
nonexisting = [f"None_{i}" for i in range(10)]
search_names = existing + nonexisting
times = []
for _ in range(5):
start = time.perf_counter()
for name in search_names:
ht_find(buckets, name)
times.append(time.perf_counter() - start)
results.append(["HashTable", "shuffled", "search", sum(times) / 5] + times)
# Удаление
delete_names = [f"User_{i:05d}" for i in random.sample(range(N), 50)]
times = []
for _ in range(5):
buckets_copy = ht_create(bucket_count)
for name, phone in records_shuffled:
ht_insert(buckets_copy, name, phone)
start = time.perf_counter()
for name in delete_names:
ht_delete(buckets_copy, name)
times.append(time.perf_counter() - start)
results.append(["HashTable", "shuffled", "delete", sum(times) / 5] + times)
def test_bst(records_shuffled, records_sorted, results):
N = len(records_shuffled)
# Вставка shuffled
times = []
for _ in range(5):
root = None
start = time.perf_counter()
for name, phone in records_shuffled:
root = bst_insert(root, name, phone)
times.append(time.perf_counter() - start)
results.append(["BST", "shuffled", "insert", sum(times) / 5] + times)
# Вставка sorted (деградация в список)
times = []
for _ in range(5):
root = None
start = time.perf_counter()
for name, phone in records_sorted:
root = bst_insert(root, name, phone)
times.append(time.perf_counter() - start)
results.append(["BST", "sorted", "insert", sum(times) / 5] + times)
# Подготовка
root = None
for name, phone in records_shuffled:
root = bst_insert(root, name, phone)
# Поиск
existing = [f"User_{i:05d}" for i in random.sample(range(N), 100)]
nonexisting = [f"None_{i}" for i in range(10)]
search_names = existing + nonexisting
times = []
for _ in range(5):
start = time.perf_counter()
for name in search_names:
bst_find(root, name)
times.append(time.perf_counter() - start)
results.append(["BST", "shuffled", "search", sum(times) / 5] + times)
# Удаление
delete_names = [f"User_{i:05d}" for i in random.sample(range(N), 50)]
times = []
for _ in range(5):
root_copy = None
for name, phone in records_shuffled:
root_copy = bst_insert(root_copy, name, phone)
start = time.perf_counter()
for name in delete_names:
root_copy = bst_delete(root_copy, name)
times.append(time.perf_counter() - start)
results.append(["BST", "shuffled", "delete", sum(times) / 5] + times)
def save_results(results, filename="docs/data/results.csv"):
import os
os.makedirs("docs/data", exist_ok=True)
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Structure", "Mode", "Operation", "AvgSec", "Run1", "Run2", "Run3", "Run4", "Run5"])
for row in results:
writer.writerow(row)
print(f"Результаты сохранены в {filename}")
# ============================================================
# 9. MAIN
# ============================================================
def main():
print("Генерация тестовых данных (N=5000 для скорости)...")
shuffled, sorted_records = generate_records(5000)
results = []
print("Тестирование LinkedList...")
test_linked_list(shuffled, sorted_records, results)
print("Тестирование HashTable...")
test_hash_table(shuffled, sorted_records, results)
print("Тестирование BST...")
test_bst(shuffled, sorted_records, results)
save_results(results)
print("\nГотово! Файл results.csv создан.")
print("Можешь построить графики в Excel или Google Sheets.")
if __name__ == "__main__":
main()