diff --git a/BolonkinNM/Task 1/.idea/.gitignore b/BolonkinNM/Task 1/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/BolonkinNM/Task 1/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/BolonkinNM/Task 1/.idea/ds_project_archive.iml b/BolonkinNM/Task 1/.idea/ds_project_archive.iml
new file mode 100644
index 0000000..8e5446a
--- /dev/null
+++ b/BolonkinNM/Task 1/.idea/ds_project_archive.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BolonkinNM/Task 1/.idea/inspectionProfiles/profiles_settings.xml b/BolonkinNM/Task 1/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/BolonkinNM/Task 1/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BolonkinNM/Task 1/.idea/misc.xml b/BolonkinNM/Task 1/.idea/misc.xml
new file mode 100644
index 0000000..b56db6e
--- /dev/null
+++ b/BolonkinNM/Task 1/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/BolonkinNM/Task 1/.idea/modules.xml b/BolonkinNM/Task 1/.idea/modules.xml
new file mode 100644
index 0000000..699d578
--- /dev/null
+++ b/BolonkinNM/Task 1/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BolonkinNM/Task 1/README.md b/BolonkinNM/Task 1/README.md
new file mode 100644
index 0000000..7e15dad
--- /dev/null
+++ b/BolonkinNM/Task 1/README.md
@@ -0,0 +1,18 @@
+# Задание 1 — структуры данных
+
+Процедурная реализация:
+- linked_list.py
+- hash_table.py
+- bst.py
+
+Эксперименты и отчёты:
+- experiments.py
+- plot_results.py
+- results.csv
+- docs/report.md
+- docs/data/*.png
+
+Запуск:
+```bash
+python main.py
+```
diff --git a/BolonkinNM/Task 1/Task 1.py b/BolonkinNM/Task 1/Task 1.py
new file mode 100644
index 0000000..b0337c1
--- /dev/null
+++ b/BolonkinNM/Task 1/Task 1.py
@@ -0,0 +1,82 @@
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+col_names = ['T(C)', 'Td(C)', 'HR%', 'ff(kmh)', 'Gust(kmh)', 'P_0(HPa)', 'P_sea(HPa)']
+
+data = pd.read_csv(
+ "data_meteo.txt",
+ sep=r'\s+',
+ skiprows=1,
+ usecols=[2, 3, 4, 5, 6, 7, 8],
+ names=col_names,
+ engine='python'
+)
+
+print("загружено записей (строк):", len(data))
+
+data = data.apply(pd.to_numeric, errors='coerce')
+n_before = len(data)
+data = data.dropna(subset=col_names)
+n_after = len(data)
+
+print(f"после приведения к числам и dropna: {n_after} строк (удалено {n_before - n_after})")
+
+def correlation(vec1, vec2, center=False):
+ v1 = np.asarray(vec1, dtype=float)
+ v2 = np.asarray(vec2, dtype=float)
+
+ if center:
+ v1 = v1 - np.mean(v1)
+ v2 = v2 - np.mean(v2)
+
+ dot_product = np.sum(v1 * v2)
+ norm1 = np.sqrt(np.sum(v1 ** 2))
+ norm2 = np.sqrt(np.sum(v2 ** 2))
+
+ if norm1 == 0 or norm2 == 0:
+ return np.nan
+
+ return dot_product / (norm1 * norm2)
+
+n = len(col_names)
+
+raw_matrix = np.zeros((n, n))
+center_matrix = np.zeros((n, n))
+
+for i in range(n):
+ for j in range(n):
+ r_raw = correlation(data.iloc[:, i], data.iloc[:, j], center=False)
+ r_center = correlation(data.iloc[:, i], data.iloc[:, j], center=True)
+
+ raw_matrix[i, j] = np.round(r_raw, 3) if not np.isnan(r_raw) else np.nan
+ center_matrix[i, j] = np.round(r_center, 3) if not np.isnan(r_center) else np.nan
+
+df_raw = pd.DataFrame(raw_matrix, index=col_names, columns=col_names)
+df_center = pd.DataFrame(center_matrix, index=col_names, columns=col_names)
+
+print("\nкорреляция (raw):")
+print(df_raw.to_string())
+
+print("\nкорреляция (centered):")
+print(df_center.to_string())
+
+print("\nразница (centered - raw):")
+print((df_center - df_raw).round(3).to_string())
+
+plt.figure(figsize=(14, 8))
+
+for col in col_names:
+ centered_values = data[col] - data[col].mean()
+ plt.plot(centered_values.values, label=col, marker='.', linewidth=1, markersize=4)
+
+plt.axhline(0, linestyle='--')
+plt.title("центрированные метеоданные")
+plt.xlabel("номер измерения")
+plt.ylabel("отклонение от среднего")
+plt.legend()
+plt.grid()
+plt.tight_layout()
+
+plt.savefig("meteo_analysis.png", dpi=150)
+plt.show()
\ No newline at end of file
diff --git a/BolonkinNM/Task 1/bst.py b/BolonkinNM/Task 1/bst.py
new file mode 100644
index 0000000..221fd67
--- /dev/null
+++ b/BolonkinNM/Task 1/bst.py
@@ -0,0 +1,118 @@
+from typing import Any, Dict, List, Optional
+
+
+Node = Dict[str, Any]
+
+
+def _make_node(name: str, phone: str) -> Node:
+ return {"name": name, "phone": phone, "left": None, "right": None}
+
+
+def bst_insert(root: Optional[Node], name: str, phone: str) -> Node:
+ new_node = _make_node(name, phone)
+
+ if root is None:
+ return new_node
+
+ current = root
+ parent = None
+
+ while current is not None:
+ parent = current
+ if name < current["name"]:
+ current = current["left"]
+ elif name > current["name"]:
+ current = current["right"]
+ else:
+ current["phone"] = phone
+ return root
+
+ if name < parent["name"]:
+ parent["left"] = new_node
+ else:
+ parent["right"] = new_node
+
+ return root
+
+
+def bst_find(root: Optional[Node], name: str) -> Optional[str]:
+ current = root
+ while current is not None:
+ if name < current["name"]:
+ current = current["left"]
+ elif name > current["name"]:
+ current = current["right"]
+ else:
+ return current["phone"]
+ return None
+
+
+def _find_min_node(node: Node) -> Node:
+ current = node
+ while current["left"] is not None:
+ current = current["left"]
+ return current
+
+
+def bst_delete(root: Optional[Node], name: str) -> Optional[Node]:
+ if root is None:
+ return None
+
+ 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
+
+ if current["left"] is None or current["right"] is None:
+ child = current["left"] if current["left"] is not None else current["right"]
+
+ if parent is None:
+ return child
+
+ if parent["left"] is current:
+ parent["left"] = child
+ else:
+ parent["right"] = child
+ return root
+
+ succ_parent = current
+ successor = current["right"]
+ while successor["left"] is not None:
+ succ_parent = successor
+ successor = successor["left"]
+
+ current["name"] = successor["name"]
+ current["phone"] = successor["phone"]
+
+ successor_child = successor["right"]
+ if succ_parent["left"] is successor:
+ succ_parent["left"] = successor_child
+ else:
+ succ_parent["right"] = successor_child
+
+ return root
+
+
+def bst_list_all(root: Optional[Node]) -> List[Dict[str, str]]:
+ result: List[Dict[str, str]] = []
+ stack: List[Node] = []
+ current = root
+
+ while current is not None or stack:
+ while current is not None:
+ stack.append(current)
+ current = current["left"]
+
+ current = stack.pop()
+ result.append({"name": current["name"], "phone": current["phone"]})
+ current = current["right"]
+
+ return result
diff --git a/BolonkinNM/Task 1/docs/data/delete.png b/BolonkinNM/Task 1/docs/data/delete.png
new file mode 100644
index 0000000..b5307a7
Binary files /dev/null and b/BolonkinNM/Task 1/docs/data/delete.png differ
diff --git a/BolonkinNM/Task 1/docs/data/find.png b/BolonkinNM/Task 1/docs/data/find.png
new file mode 100644
index 0000000..d0d4e81
Binary files /dev/null and b/BolonkinNM/Task 1/docs/data/find.png differ
diff --git a/BolonkinNM/Task 1/docs/data/insert.png b/BolonkinNM/Task 1/docs/data/insert.png
new file mode 100644
index 0000000..b2832b9
Binary files /dev/null and b/BolonkinNM/Task 1/docs/data/insert.png differ
diff --git a/BolonkinNM/Task 1/docs/data/results.csv b/BolonkinNM/Task 1/docs/data/results.csv
new file mode 100644
index 0000000..4ccfb69
--- /dev/null
+++ b/BolonkinNM/Task 1/docs/data/results.csv
@@ -0,0 +1,109 @@
+Структура,Режим,Операция,Замер,Время (сек)
+LinkedList,случайный,insert,1,4.2622492010
+LinkedList,случайный,find,1,0.0314994130
+LinkedList,случайный,delete,1,0.0149069000
+LinkedList,случайный,insert,2,4.0154580330
+LinkedList,случайный,find,2,0.0393284500
+LinkedList,случайный,delete,2,0.0210732100
+LinkedList,случайный,insert,3,4.0436019780
+LinkedList,случайный,find,3,0.0344933660
+LinkedList,случайный,delete,3,0.0152639850
+LinkedList,случайный,insert,4,3.7182993220
+LinkedList,случайный,find,4,0.0327698850
+LinkedList,случайный,delete,4,0.0149959540
+LinkedList,случайный,insert,5,3.7082228200
+LinkedList,случайный,find,5,0.0303762490
+LinkedList,случайный,delete,5,0.0141406560
+LinkedList,случайный,insert,среднее,3.9495662708
+LinkedList,случайный,find,среднее,0.0336934726
+LinkedList,случайный,delete,среднее,0.0160761410
+HashTable,случайный,insert,1,0.2059865770
+HashTable,случайный,find,1,0.0014966100
+HashTable,случайный,delete,1,0.0006891700
+HashTable,случайный,insert,2,0.2024331460
+HashTable,случайный,find,2,0.0015934880
+HashTable,случайный,delete,2,0.0007212620
+HashTable,случайный,insert,3,0.2126128040
+HashTable,случайный,find,3,0.0016566220
+HashTable,случайный,delete,3,0.0008358420
+HashTable,случайный,insert,4,0.2157934910
+HashTable,случайный,find,4,0.0015542810
+HashTable,случайный,delete,4,0.0007269120
+HashTable,случайный,insert,5,0.2079924580
+HashTable,случайный,find,5,0.0013696990
+HashTable,случайный,delete,5,0.0006616050
+HashTable,случайный,insert,среднее,0.2089636952
+HashTable,случайный,find,среднее,0.0015341400
+HashTable,случайный,delete,среднее,0.0007269582
+BST,случайный,insert,1,0.0166981280
+BST,случайный,find,1,0.0001569360
+BST,случайный,delete,1,0.0000917280
+BST,случайный,insert,2,0.0184119040
+BST,случайный,find,2,0.0001517110
+BST,случайный,delete,2,0.0001163770
+BST,случайный,insert,3,0.0174662270
+BST,случайный,find,3,0.0001582930
+BST,случайный,delete,3,0.0000892660
+BST,случайный,insert,4,0.0191369100
+BST,случайный,find,4,0.0002087170
+BST,случайный,delete,4,0.0001067050
+BST,случайный,insert,5,0.0184276900
+BST,случайный,find,5,0.0002767720
+BST,случайный,delete,5,0.0001067660
+BST,случайный,insert,среднее,0.0180281718
+BST,случайный,find,среднее,0.0001904858
+BST,случайный,delete,среднее,0.0001021684
+LinkedList,отсортированный,insert,1,2.9875078340
+LinkedList,отсортированный,find,1,0.0237300610
+LinkedList,отсортированный,delete,1,0.0111698260
+LinkedList,отсортированный,insert,2,3.0573987940
+LinkedList,отсортированный,find,2,0.0243270360
+LinkedList,отсортированный,delete,2,0.0115366030
+LinkedList,отсортированный,insert,3,2.9641987260
+LinkedList,отсортированный,find,3,0.0236313330
+LinkedList,отсортированный,delete,3,0.0112848510
+LinkedList,отсортированный,insert,4,3.0345914950
+LinkedList,отсортированный,find,4,0.0240271220
+LinkedList,отсортированный,delete,4,0.0112117310
+LinkedList,отсортированный,insert,5,2.9481954700
+LinkedList,отсортированный,find,5,0.0239006100
+LinkedList,отсортированный,delete,5,0.0110857710
+LinkedList,отсортированный,insert,среднее,2.9983784638
+LinkedList,отсортированный,find,среднее,0.0239232324
+LinkedList,отсортированный,delete,среднее,0.0112577564
+HashTable,отсортированный,insert,1,0.1997087560
+HashTable,отсортированный,find,1,0.0017550400
+HashTable,отсортированный,delete,1,0.0008407980
+HashTable,отсортированный,insert,2,0.1968675190
+HashTable,отсортированный,find,2,0.0019886760
+HashTable,отсортированный,delete,2,0.0008920910
+HashTable,отсортированный,insert,3,0.1907563580
+HashTable,отсортированный,find,3,0.0018447440
+HashTable,отсортированный,delete,3,0.0008684640
+HashTable,отсортированный,insert,4,0.2625327630
+HashTable,отсортированный,find,4,0.0016053140
+HashTable,отсортированный,delete,4,0.0008098670
+HashTable,отсортированный,insert,5,0.1936840590
+HashTable,отсортированный,find,5,0.0019015160
+HashTable,отсортированный,delete,5,0.0009053780
+HashTable,отсортированный,insert,среднее,0.2087098910
+HashTable,отсортированный,find,среднее,0.0018190580
+HashTable,отсортированный,delete,среднее,0.0008633196
+BST,отсортированный,insert,1,4.2195800190
+BST,отсортированный,find,1,0.0389314570
+BST,отсортированный,delete,1,0.0190308920
+BST,отсортированный,insert,2,4.1356184250
+BST,отсортированный,find,2,0.0383339310
+BST,отсортированный,delete,2,0.0194247740
+BST,отсортированный,insert,3,4.1204731890
+BST,отсортированный,find,3,0.0388593320
+BST,отсортированный,delete,3,0.0215428460
+BST,отсортированный,insert,4,4.2120902370
+BST,отсортированный,find,4,0.0378190250
+BST,отсортированный,delete,4,0.0188528460
+BST,отсортированный,insert,5,4.1304951260
+BST,отсортированный,find,5,0.0359927840
+BST,отсортированный,delete,5,0.0179617110
+BST,отсортированный,insert,среднее,4.1636513992
+BST,отсортированный,find,среднее,0.0379873058
+BST,отсортированный,delete,среднее,0.0193626138
diff --git a/BolonkinNM/Task 1/docs/report.md b/BolonkinNM/Task 1/docs/report.md
new file mode 100644
index 0000000..0a757c7
--- /dev/null
+++ b/BolonkinNM/Task 1/docs/report.md
@@ -0,0 +1,101 @@
+# Отчёт по заданию 1 — структуры данных
+
+## Цель работы
+
+Реализовать три структуры данных с нуля в процедурном стиле:
+
+- связный список;
+- хеш-таблицу;
+- двоичное дерево поиска.
+
+Также были выполнены измерения времени для операций `insert`, `find`, `delete` и построены графики по результатам эксперимента.
+
+## Реализованные структуры
+
+### Связный список
+
+Узел хранится как словарь:
+
+```python
+{"name": "Имя", "phone": "123", "next": None}
+```
+
+### Хеш-таблица
+
+Хранится как список бакетов фиксированной длины, где каждый бакет — голова связного списка или `None`.
+
+### Двоичное дерево поиска
+
+Узел хранится как словарь:
+
+```python
+{"name": "Имя", "phone": "123", "left": None, "right": None}
+```
+
+Для BST использованы итеративные операции, чтобы корректно работать и на отсортированных данных.
+
+## Методика эксперимента
+
+- Количество записей: `N = 10000`
+- Режимы данных:
+ - случайный порядок;
+ - отсортированный порядок.
+- Каждое измерение повторялось **5 раз**.
+- В CSV сохранены:
+ - все отдельные замеры;
+ - среднее время для каждой операции, структуры и режима.
+
+Операции:
+
+- вставка всех записей;
+- поиск 100 существующих и 10 отсутствующих имён;
+- удаление 50 случайных имён.
+
+## Графики
+
+
+
+
+
+
+
+## Средние результаты
+
+| Режим | Операция | LinkedList | HashTable | BST | Лучший результат |
+|---|---:|---:|---:|---:|---|
+| случайный | insert | 3.949566 | 0.208964 | 0.018028 | BST |
+| случайный | find | 0.033693 | 0.001534 | 0.000190 | BST |
+| случайный | delete | 0.016076 | 0.000727 | 0.000102 | BST |
+| отсортированный | insert | 2.998378 | 0.208710 | 4.163651 | HashTable |
+| отсортированный | find | 0.023923 | 0.001819 | 0.037987 | HashTable |
+| отсортированный | delete | 0.011258 | 0.000863 | 0.019363 | HashTable |
+
+## Анализ результатов
+
+### Влияние порядка входных данных на BST
+
+На случайных данных BST работает значительно быстрее, чем на отсортированных. Это связано с тем, что при случайной вставке дерево остаётся ближе к сбалансированному состоянию.
+
+На отсортированных данных дерево вырождается в цепочку, поэтому вставка становится медленной, а поиск и удаление тоже деградируют по времени.
+
+### Почему хеш-таблица почти не чувствительна к порядку
+
+Хеш-таблица распределяет элементы по бакетам через хеш-функцию, поэтому сам порядок входа почти не влияет на скорость. Влияние может появляться только из-за коллизий, но в целом поведение остаётся близким к постоянному времени.
+
+### Почему связный список всегда медленен при поиске
+
+Поиск в связном списке выполняется последовательным просмотром элементов. Поэтому при большом количестве записей приходится проходить много узлов, и операция остаётся линейной по времени.
+
+### Как удаление работает в каждой структуре
+
+- В связном списке нужно сначала найти нужный узел, затем переназначить ссылку.
+- В хеш-таблице сначала выбирается бакет, затем удаление выполняется внутри короткой цепочки.
+- В BST удаление зависит от числа потомков: если потомок один или ноль, операция простая; если два — нужно найти преемника.
+
+## Вывод
+
+Для частых вставок и особенно частого поиска в реальной задаче чаще всего лучше подходит **хеш-таблица**.
+
+Если важно получать данные в отсортированном виде, удобнее использовать **BST**.
+
+**Связный список** подходит для маленьких объёмов данных или очень простых сценариев, но при большом числе записей он проигрывает по скорости поиска.
diff --git a/BolonkinNM/Task 1/experiments.py b/BolonkinNM/Task 1/experiments.py
new file mode 100644
index 0000000..f5face1
--- /dev/null
+++ b/BolonkinNM/Task 1/experiments.py
@@ -0,0 +1,172 @@
+from __future__ import annotations
+
+import csv
+import random
+import time
+from pathlib import Path
+from typing import Dict, List, Tuple
+
+from linked_list import ll_insert, ll_find, ll_delete
+from hash_table import ht_insert, ht_find, ht_delete
+from bst import bst_insert, bst_find, bst_delete
+from utils import generate_records, prepare_records_variants
+
+
+Record = Tuple[str, str]
+
+
+def make_missing_names(count: int = 10) -> List[str]:
+ return [f"None_{i}" for i in range(count)]
+
+
+def pick_existing_names(records: List[Record], count: int, seed: int = 42) -> List[str]:
+ rng = random.Random(seed)
+ unique_names = list(dict.fromkeys(name for name, _ in records))
+ if len(unique_names) < count:
+ raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
+ return rng.sample(unique_names, count)
+
+
+def pick_delete_names(records: List[Record], count: int = 50, seed: int = 43) -> List[str]:
+ rng = random.Random(seed)
+ unique_names = list(dict.fromkeys(name for name, _ in records))
+ if len(unique_names) < count:
+ raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
+ return rng.sample(unique_names, count)
+
+
+def build_structure(structure_name: str, records: List[Record], buckets_count: int = 2048):
+ if structure_name == "linked_list":
+ structure = None
+ for name, phone in records:
+ structure = ll_insert(structure, name, phone)
+ return structure
+
+ if structure_name == "hash_table":
+ buckets = [None] * buckets_count
+ for name, phone in records:
+ buckets = ht_insert(buckets, name, phone)
+ return buckets
+
+ if structure_name == "bst":
+ root = None
+ for name, phone in records:
+ root = bst_insert(root, name, phone)
+ return root
+
+ raise ValueError(f"Unknown structure: {structure_name}")
+
+
+def do_find(structure_name: str, structure: object, existing_names: List[str], missing_names: List[str]) -> None:
+ if structure_name == "linked_list":
+ for name in existing_names:
+ ll_find(structure, name)
+ for name in missing_names:
+ ll_find(structure, name)
+ return
+
+ if structure_name == "hash_table":
+ for name in existing_names:
+ ht_find(structure, name)
+ for name in missing_names:
+ ht_find(structure, name)
+ return
+
+ if structure_name == "bst":
+ for name in existing_names:
+ bst_find(structure, name)
+ for name in missing_names:
+ bst_find(structure, name)
+ return
+
+ raise ValueError(f"Unknown structure: {structure_name}")
+
+
+def do_delete(structure_name: str, structure: object, delete_names: List[str]):
+ if structure_name == "linked_list":
+ for name in delete_names:
+ structure = ll_delete(structure, name)
+ return structure
+
+ if structure_name == "hash_table":
+ for name in delete_names:
+ structure = ht_delete(structure, name)
+ return structure
+
+ if structure_name == "bst":
+ for name in delete_names:
+ structure = bst_delete(structure, name)
+ return structure
+
+ raise ValueError(f"Unknown structure: {structure_name}")
+
+
+def measure_once(structure_name: str, records: List[Record], buckets_count: int = 2048) -> Dict[str, float]:
+ existing_names = pick_existing_names(records, 100, seed=42)
+ missing_names = make_missing_names(10)
+ delete_names = pick_delete_names(records, 50, seed=43)
+
+ start = time.perf_counter()
+ structure = build_structure(structure_name, records, buckets_count=buckets_count)
+ insert_time = time.perf_counter() - start
+
+ start = time.perf_counter()
+ do_find(structure_name, structure, existing_names, missing_names)
+ find_time = time.perf_counter() - start
+
+ start = time.perf_counter()
+ structure = do_delete(structure_name, structure, delete_names)
+ delete_time = time.perf_counter() - start
+
+ return {"insert": insert_time, "find": find_time, "delete": delete_time}
+
+
+def run_experiments(n: int = 10000, buckets_count: int = 2048, repeats: int = 5):
+ records = generate_records(n, repeat_names=False)
+ records_shuffled, records_sorted = prepare_records_variants(records)
+
+ datasets = [
+ ("случайный", records_shuffled),
+ ("отсортированный", records_sorted),
+ ]
+ structures = [
+ ("LinkedList", "linked_list"),
+ ("HashTable", "hash_table"),
+ ("BST", "bst"),
+ ]
+ operations = ("insert", "find", "delete")
+
+ rows = [["Структура", "Режим", "Операция", "Замер", "Время (сек)"]]
+
+ for mode_name, dataset_records in datasets:
+ for human_name, structure_name in structures:
+ times_by_op = {op: [] for op in operations}
+
+ for attempt in range(1, repeats + 1):
+ result = measure_once(structure_name, dataset_records, buckets_count=buckets_count)
+ for op_name in operations:
+ elapsed = result[op_name]
+ times_by_op[op_name].append(elapsed)
+ rows.append([human_name, mode_name, op_name, attempt, f"{elapsed:.10f}"])
+
+ for op_name in operations:
+ avg_time = sum(times_by_op[op_name]) / len(times_by_op[op_name])
+ rows.append([human_name, mode_name, op_name, "среднее", f"{avg_time:.10f}"])
+
+ return rows
+
+
+def save_results_csv(rows, filename: str = "results.csv"):
+ with open(filename, "w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerows(rows)
+
+
+def main():
+ rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
+ save_results_csv(rows, "results.csv")
+ print("Saved results.csv")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/BolonkinNM/Task 1/hash_table.py b/BolonkinNM/Task 1/hash_table.py
new file mode 100644
index 0000000..9aa720d
--- /dev/null
+++ b/BolonkinNM/Task 1/hash_table.py
@@ -0,0 +1,44 @@
+
+
+from typing import Any, Dict, List, Optional
+
+from linked_list import ll_insert, ll_find, ll_delete, ll_list_all
+
+
+Bucket = Optional[Dict[str, Any]]
+
+
+def _hash_name(name: str, buckets_count: int) -> int:
+ if buckets_count <= 0:
+ return 0
+ return sum(ord(ch) for ch in name) % buckets_count
+
+
+def ht_insert(buckets: List[Bucket], name: str, phone: str) -> List[Bucket]:
+ if not buckets:
+ return buckets
+ index = _hash_name(name, len(buckets))
+ buckets[index] = ll_insert(buckets[index], name, phone)
+ return buckets
+
+
+def ht_find(buckets: List[Bucket], name: str) -> Optional[str]:
+ if not buckets:
+ return None
+ index = _hash_name(name, len(buckets))
+ return ll_find(buckets[index], name)
+
+
+def ht_delete(buckets: List[Bucket], name: str) -> List[Bucket]:
+ if not buckets:
+ return buckets
+ index = _hash_name(name, len(buckets))
+ buckets[index] = ll_delete(buckets[index], name)
+ return buckets
+
+
+def ht_list_all(buckets: List[Bucket]) -> List[Dict[str, str]]:
+ records: List[Dict[str, str]] = []
+ for head in buckets:
+ records.extend(ll_list_all(head))
+ return sorted(records, key=lambda x: x["name"])
diff --git a/BolonkinNM/Task 1/linked_list.py b/BolonkinNM/Task 1/linked_list.py
new file mode 100644
index 0000000..0260036
--- /dev/null
+++ b/BolonkinNM/Task 1/linked_list.py
@@ -0,0 +1,73 @@
+
+
+from typing import Any, Dict, List, Optional
+
+
+Node = Dict[str, Any]
+
+
+def _make_node(name: str, phone: str) -> Node:
+ return {"name": name, "phone": phone, "next": None}
+
+
+def sort_records(records: List[Dict[str, str]]) -> List[Dict[str, str]]:
+
+ return sorted(records, key=lambda x: x["name"])
+
+
+def ll_insert(head: Optional[Node], name: str, phone: str) -> Node:
+
+ new_node = _make_node(name, phone)
+
+ if head is None:
+ return new_node
+
+ current = head
+ while current is not None:
+ if current["name"] == name:
+ current["phone"] = phone
+ return head
+ if current["next"] is None:
+ current["next"] = new_node
+ return head
+ current = current["next"]
+
+ return head
+
+
+def ll_find(head: Optional[Node], name: str) -> Optional[str]:
+ current = head
+ while current is not None:
+ if current["name"] == name:
+ return current["phone"]
+ current = current["next"]
+ return None
+
+
+def ll_delete(head: Optional[Node], name: str) -> Optional[Node]:
+ if head is None:
+ return None
+
+ if head["name"] == name:
+ return head["next"]
+
+ prev = head
+ current = head["next"]
+
+ while current is not None:
+ if current["name"] == name:
+ prev["next"] = current["next"]
+ return head
+ prev = current
+ current = current["next"]
+
+ return head
+
+
+def ll_list_all(head: Optional[Node]) -> List[Dict[str, str]]:
+ records: List[Dict[str, str]] = []
+ current = head
+ while current is not None:
+ records.append({"name": current["name"], "phone": current["phone"]})
+ current = current["next"]
+ return sort_records(records)
diff --git a/BolonkinNM/Task 1/main.py b/BolonkinNM/Task 1/main.py
new file mode 100644
index 0000000..70de618
--- /dev/null
+++ b/BolonkinNM/Task 1/main.py
@@ -0,0 +1,21 @@
+
+
+from __future__ import annotations
+
+import csv
+from pathlib import Path
+
+from experiments import run_experiments, save_results_csv
+from plot_results import build_graphs, load_average_results
+
+
+def main():
+ rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
+ save_results_csv(rows, "results.csv")
+ averaged = load_average_results("results.csv")
+ build_graphs(averaged, output_dir="docs/data")
+ print("Done.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/BolonkinNM/Task 1/plot_results.py b/BolonkinNM/Task 1/plot_results.py
new file mode 100644
index 0000000..f4f3b6c
--- /dev/null
+++ b/BolonkinNM/Task 1/plot_results.py
@@ -0,0 +1,60 @@
+
+
+from __future__ import annotations
+
+import csv
+from collections import defaultdict
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+
+
+def load_average_results(csv_file: str):
+ results = []
+ with open(csv_file, "r", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ if row["Замер"] != "среднее":
+ continue
+ results.append({
+ "structure": row["Структура"],
+ "mode": row["Режим"],
+ "operation": row["Операция"],
+ "time": float(row["Время (сек)"]),
+ })
+ return results
+
+
+def build_graphs(results, output_dir: str = "docs/data"):
+ output = Path(output_dir)
+ output.mkdir(parents=True, exist_ok=True)
+
+ grouped = defaultdict(list)
+ for row in results:
+ grouped[row["operation"]].append(row)
+
+ for operation in ("insert", "find", "delete"):
+ rows = grouped[operation]
+ labels = [f"{r['structure']}\n{r['mode']}" for r in rows]
+ values = [r["time"] for r in rows]
+
+ plt.figure(figsize=(11, 6))
+ plt.bar(labels, values)
+ plt.title(f"{operation.capitalize()} comparison")
+ plt.xlabel("Structure / data order")
+ plt.ylabel("Time, seconds")
+ plt.xticks(rotation=20)
+ plt.tight_layout()
+ filename = output / f"{operation}.png"
+ plt.savefig(filename, dpi=160)
+ plt.close()
+ print(f"Saved {filename}")
+
+
+def main():
+ results = load_average_results("results.csv")
+ build_graphs(results)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/BolonkinNM/Task 1/requirements.txt b/BolonkinNM/Task 1/requirements.txt
new file mode 100644
index 0000000..a9006fd
--- /dev/null
+++ b/BolonkinNM/Task 1/requirements.txt
@@ -0,0 +1 @@
+matplotlib>=3.8
diff --git a/BolonkinNM/Task 1/results.csv b/BolonkinNM/Task 1/results.csv
new file mode 100644
index 0000000..cc67b71
--- /dev/null
+++ b/BolonkinNM/Task 1/results.csv
@@ -0,0 +1,109 @@
+Структура,Режим,Операция,Замер,Время (сек)
+LinkedList,случайный,insert,1,2.4210275000
+LinkedList,случайный,find,1,0.0214394000
+LinkedList,случайный,delete,1,0.0108667000
+LinkedList,случайный,insert,2,2.4208055000
+LinkedList,случайный,find,2,0.0216110000
+LinkedList,случайный,delete,2,0.0106216000
+LinkedList,случайный,insert,3,2.4210881000
+LinkedList,случайный,find,3,0.0216503000
+LinkedList,случайный,delete,3,0.0106497000
+LinkedList,случайный,insert,4,2.4530798000
+LinkedList,случайный,find,4,0.0222764000
+LinkedList,случайный,delete,4,0.0108350000
+LinkedList,случайный,insert,5,2.4567773000
+LinkedList,случайный,find,5,0.0219400000
+LinkedList,случайный,delete,5,0.0108697000
+LinkedList,случайный,insert,среднее,2.4345556400
+LinkedList,случайный,find,среднее,0.0217834200
+LinkedList,случайный,delete,среднее,0.0107685400
+HashTable,случайный,insert,1,0.1621210000
+HashTable,случайный,find,1,0.0011201000
+HashTable,случайный,delete,1,0.0005854000
+HashTable,случайный,insert,2,0.1732676000
+HashTable,случайный,find,2,0.0011247000
+HashTable,случайный,delete,2,0.0005818000
+HashTable,случайный,insert,3,0.1638609000
+HashTable,случайный,find,3,0.0011355000
+HashTable,случайный,delete,3,0.0005814000
+HashTable,случайный,insert,4,0.1642886000
+HashTable,случайный,find,4,0.0011268000
+HashTable,случайный,delete,4,0.0005785000
+HashTable,случайный,insert,5,0.1640916000
+HashTable,случайный,find,5,0.0011287000
+HashTable,случайный,delete,5,0.0005787000
+HashTable,случайный,insert,среднее,0.1655259400
+HashTable,случайный,find,среднее,0.0011271600
+HashTable,случайный,delete,среднее,0.0005811600
+BST,случайный,insert,1,0.0153754000
+BST,случайный,find,1,0.0001491000
+BST,случайный,delete,1,0.0000786000
+BST,случайный,insert,2,0.0155821000
+BST,случайный,find,2,0.0001453000
+BST,случайный,delete,2,0.0000724000
+BST,случайный,insert,3,0.0151360000
+BST,случайный,find,3,0.0001437000
+BST,случайный,delete,3,0.0000741000
+BST,случайный,insert,4,0.0153703000
+BST,случайный,find,4,0.0001425000
+BST,случайный,delete,4,0.0000715000
+BST,случайный,insert,5,0.0153753000
+BST,случайный,find,5,0.0001455000
+BST,случайный,delete,5,0.0000723000
+BST,случайный,insert,среднее,0.0153678200
+BST,случайный,find,среднее,0.0001452200
+BST,случайный,delete,среднее,0.0000737800
+LinkedList,отсортированный,insert,1,2.5884851000
+LinkedList,отсортированный,find,1,0.0227221000
+LinkedList,отсортированный,delete,1,0.0111309000
+LinkedList,отсортированный,insert,2,2.5095731000
+LinkedList,отсортированный,find,2,0.0217208000
+LinkedList,отсортированный,delete,2,0.0107773000
+LinkedList,отсортированный,insert,3,2.5642096000
+LinkedList,отсортированный,find,3,0.0228242000
+LinkedList,отсортированный,delete,3,0.0115945000
+LinkedList,отсортированный,insert,4,2.7163021000
+LinkedList,отсортированный,find,4,0.0431456000
+LinkedList,отсортированный,delete,4,0.0136020000
+LinkedList,отсортированный,insert,5,2.6891794000
+LinkedList,отсортированный,find,5,0.0217679000
+LinkedList,отсортированный,delete,5,0.0106384000
+LinkedList,отсортированный,insert,среднее,2.6135498600
+LinkedList,отсортированный,find,среднее,0.0264361200
+LinkedList,отсортированный,delete,среднее,0.0115486200
+HashTable,отсортированный,insert,1,0.1524640000
+HashTable,отсортированный,find,1,0.0014973000
+HashTable,отсортированный,delete,1,0.0006991000
+HashTable,отсортированный,insert,2,0.1537592000
+HashTable,отсортированный,find,2,0.0012225000
+HashTable,отсортированный,delete,2,0.0006561000
+HashTable,отсортированный,insert,3,0.1555816000
+HashTable,отсортированный,find,3,0.0012080000
+HashTable,отсортированный,delete,3,0.0006472000
+HashTable,отсортированный,insert,4,0.1546417000
+HashTable,отсортированный,find,4,0.0015017000
+HashTable,отсортированный,delete,4,0.0007512000
+HashTable,отсортированный,insert,5,0.1531659000
+HashTable,отсортированный,find,5,0.0012219000
+HashTable,отсортированный,delete,5,0.0006493000
+HashTable,отсортированный,insert,среднее,0.1539224800
+HashTable,отсортированный,find,среднее,0.0013302800
+HashTable,отсортированный,delete,среднее,0.0006805800
+BST,отсортированный,insert,1,4.5025059000
+BST,отсортированный,find,1,0.0387267000
+BST,отсортированный,delete,1,0.0162161000
+BST,отсортированный,insert,2,4.6704081000
+BST,отсортированный,find,2,0.0435012000
+BST,отсортированный,delete,2,0.0203211000
+BST,отсортированный,insert,3,6.2192950000
+BST,отсортированный,find,3,0.0578654000
+BST,отсортированный,delete,3,0.0327529000
+BST,отсортированный,insert,4,4.7844525000
+BST,отсортированный,find,4,0.0380228000
+BST,отсортированный,delete,4,0.0159740000
+BST,отсортированный,insert,5,4.4861403000
+BST,отсортированный,find,5,0.0382484000
+BST,отсортированный,delete,5,0.0159402000
+BST,отсортированный,insert,среднее,4.9325603600
+BST,отсортированный,find,среднее,0.0432729000
+BST,отсортированный,delete,среднее,0.0202408600
diff --git a/BolonkinNM/Task 1/utils.py b/BolonkinNM/Task 1/utils.py
new file mode 100644
index 0000000..0fcb993
--- /dev/null
+++ b/BolonkinNM/Task 1/utils.py
@@ -0,0 +1,35 @@
+import random
+from typing import List, Tuple
+
+
+Record = Tuple[str, str]
+
+
+def generate_records(n: int, repeat_names: bool = False, seed: int = 42) -> List[Record]:
+ rng = random.Random(seed)
+ records: List[Record] = []
+
+ if repeat_names:
+ name_pool = [
+ "User_Alex", "User_Bob", "User_Cat", "User_Dan", "User_Eva",
+ "User_Fox", "User_Geo", "User_Hen", "User_Ira", "User_Leo",
+ ]
+ for _ in range(n):
+ name = rng.choice(name_pool)
+ phone = f"{rng.randint(1000000000, 9999999999)}"
+ records.append((name, phone))
+ else:
+ for i in range(n):
+ name = f"User_{i:05d}"
+ phone = f"{1000000000 + i}"
+ records.append((name, phone))
+
+ return records
+
+
+def prepare_records_variants(records: List[Record], seed: int = 42):
+ rng = random.Random(seed)
+ records_shuffled = list(records)
+ rng.shuffle(records_shuffled)
+ records_sorted = sorted(records, key=lambda x: x[0])
+ return records_shuffled, records_sorted
diff --git a/BolonkinNM/Task 1/график.png b/BolonkinNM/Task 1/график.png
new file mode 100644
index 0000000..448942a
Binary files /dev/null and b/BolonkinNM/Task 1/график.png differ
diff --git a/BolonkinNM/Task 1/отчет.docx b/BolonkinNM/Task 1/отчет.docx
new file mode 100644
index 0000000..3a40fa2
Binary files /dev/null and b/BolonkinNM/Task 1/отчет.docx differ