2026-rff_mp/sorokinfi/427.md

14 KiB
Raw Blame History

import csv import random import sys import time from collections import defaultdict import pandas as pd import matplotlib.pyplot as plt

увеличиваем лимит рекурсии

sys.setrecursionlimit(25000)

1. связный список, узел: {'name': 'Имя', 'phone': '123', 'next': None}

проходит до конца и добавляет в конец

def ll_insert(head, name, phone): new_node = {'name': name, 'phone': phone, 'next': None} if head is None: return new_node current = head while current['next'] is not None: current = current['next'] current['next'] = new_node return head

ищет узел, возвращает телефон или None

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 if head['name'] == name: return head['next']

current = head
while current['next'] is not None:
    if current['next']['name'] == name:
        current['next'] = current['next']['next']
        return head
    current = current['next']
return head

собирает все записи в список и сортирует

def ll_list_all(head): records = [] current = head while current is not None: records.append((current['name'], current['phone'])) current = current['next'] records.sort(key=lambda x: x[0]) return records

2. хеш-таблица

хеш-функция для вычисления бекета

def ht_hash(name, size): return hash(name) % size

вычисляет индекс, вызывает ll_insert для соответствующего бакета

def ht_insert(buckets, name, phone): size = len(buckets) idx = ht_hash(name, size) buckets[idx] = ll_insert(buckets[idx], name, phone)

поиск по хеш-таблице

def ht_find(buckets, name): size = len(buckets) idx = ht_hash(name, size) return ll_find(buckets[idx], name)

удаление из хеш-таблицы

def ht_delete(buckets, name): size = len(buckets) idx = ht_hash(name, size) buckets[idx] = ll_delete(buckets[idx], name)

собирает все записи из всех бакетов и сортирует

def ht_list_all(buckets): all_records = [] for head in buckets: current = head while current is not None: all_records.append((current['name'], current['phone'])) current = current['next'] all_records.sort(key=lambda x: x[0]) return all_records

3. двоичное дерево поиска

узел — словарь: {'name': 'Имя', 'phone': '123', 'left': None, 'right': None}

рекурсивно или итеративно вставляет, возвращает новый корень (если корень меняется)

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'] elif name < root['name']: return bst_find(root['left'], name) else: return bst_find(root['right'], name)

удаление, возвращает новый корень

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 = root['right']
    while successor['left'] is not None:
        successor = successor['left']
        
    root['name'] = successor['name']
    root['phone'] = successor['phone']
    root['right'] = bst_delete(root['right'], successor['name'])
    
return root

центрированный обход (рекурсивно собирает записи в отсортированном порядке)

def bst_list_all(root): records = [] def _inorder(node): if node is not None: _inorder(node['left']) records.append((node['name'], node['phone'])) _inorder(node['right']) _inorder(root) return records

4. ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ

def run_experiments(): N = 3000 HASH_SIZE = 1007

print(f"генерация тестовых данных для N = {N}...")
records_sorted = [(f"User_{i:05d}", f"+7999123{i:04d}") for i in range(N)]
records_shuffled = records_sorted.copy()
random.seed(42)
random.shuffle(records_shuffled)

# подготовка выборок 
existing_sample = [r[0] for r in random.sample(records_sorted, min(100, N))]
non_existing_sample = [f"None_{i}" for i in range(10)]
search_names = existing_sample + non_existing_sample
 
delete_names = [r[0] for r in random.sample(records_sorted, min(50, N))]
csv_rows = [["структура", "режим", "операция", "повторение", "время (сек)"]]
modes = [("случайный", records_shuffled), ("отсортированный", records_sorted)]

print("запуск экспериментов (5 повторений для каждого режима)")

ТЕСТ: СВЯЗНЫЙ СПИСОК

for mode_name, data in modes:
    for rep in range(1, 6):
        head = None
        t_start = time.perf_counter()
        for name, phone in data:
            head = ll_insert(head, name, phone)
        t_end = time.perf_counter()
        csv_rows.append(["LinkedList", mode_name, "вставка", rep, t_end - t_start])
        
        t_start = time.perf_counter()
        for name in search_names:
            ll_find(head, name)
        t_end = time.perf_counter()
        csv_rows.append(["LinkedList", mode_name, "поиск", rep, t_end - t_start])
        
        t_start = time.perf_counter()
        for name in delete_names:
            head = ll_delete(head, name)
        t_end = time.perf_counter()
        csv_rows.append(["LinkedList", mode_name, "удаление", rep, t_end - t_start])

ТЕСТ: ХЕШ-ТАБЛИЦА

for mode_name, data in modes:
    for rep in range(1, 6):
        buckets = [None] * HASH_SIZE
        t_start = time.perf_counter()
        for name, phone in data:
            ht_insert(buckets, name, phone)
        t_end = time.perf_counter()
        csv_rows.append(["HashTable", mode_name, "вставка", rep, t_end - t_start])
        
        t_start = time.perf_counter()
        for name in search_names:
            ht_find(buckets, name)
        t_end = time.perf_counter()
        csv_rows.append(["HashTable", mode_name, "поиск", rep, t_end - t_start])
        
        t_start = time.perf_counter()
        for name in delete_names:
            ht_delete(buckets, name)
        t_end = time.perf_counter()
        csv_rows.append(["HashTable", mode_name, "удаление", rep, t_end - t_start])

ТЕСТ: ДЕРЕВО ПОИСКА (BST)

for mode_name, data in modes:
    for rep in range(1, 6):
        root = None
        t_start = time.perf_counter()
        for name, phone in data:
            root = bst_insert(root, name, phone)
        t_end = time.perf_counter()
        csv_rows.append(["BST", mode_name, "вставка", rep, t_end - t_start])
        
        t_start = time.perf_counter()
        for name in search_names:
            bst_find(root, name)
        t_end = time.perf_counter()
        csv_rows.append(["BST", mode_name, "поиск", rep, t_end - t_start])
        
        t_start = time.perf_counter()
        for name in delete_names:
            root = bst_delete(root, name)
        t_end = time.perf_counter()
        csv_rows.append(["BST", mode_name, "удаление", rep, t_end - t_start])

сохранение в csv

with open("results.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(csv_rows)
print("\nвсе замеры сохранены в файл 'results.csv'.")

show_summary(csv_rows)

функция для подсчета и вывода среднего времени

def show_summary(rows): summary = defaultdict(list) for row in rows[1:]: struct, mode, op, rep, elapsed = row summary[(struct, mode, op)].append(elapsed)

print("\nСВОДНЫЕ РЕЗУЛЬТАТЫ (СРЕДНЕЕ ВРЕМЯ ИЗ 5 ЗАПУСКОВ)")
print(f"{'структура':<12} | {'режим данных':<15} | {'операция':<10} | {'время (сек)':<12}")
print("-" * 59)
for (struct, mode, op), times in sorted(summary.items()):
    avg_time = sum(times) / len(times)
    print(f"{struct:<12} | {mode:<15} | {op:<10} | {avg_time:.6f}")

5. АНАЛИЗ РЕЗУЛЬТАТОВ

def plot_results(csv_filename="results.csv"): print("построение графика") try: df = pd.read_csv(csv_filename)

    df_insert = df[df["операция"] == "вставка"]
    
    pivot_df = df_insert.pivot_table(
        index="структура", 
        columns="режим", 
        values="время (сек)", 
        aggfunc="mean"
    )
    
    pivot_df.plot(kind="bar", figsize=(10, 6), color=['#1f77b4', '#ff7f0e'])
    
    plt.title("сравнение времени вставки (N=3000)")
    plt.ylabel("среднее время выполнения (сек)")
    plt.xlabel("структура данных")
    plt.xticks(rotation=0)
    plt.grid(axis='y', linestyle='--', alpha=0.7)
    plt.savefig("benchmark_chart.png")
    print("график сохранен как benchmark_chart.png")
    plt.show()
    
except FileNotFoundError:
    print(f"файл {csv_filename} не найден. сначала запустите тесты.")

def print_report(): report = """

                  5. АНАЛИЗ РЕЗУЛЬТАТОВ ЭКСПЕРИМЕНТОВ
  1. Влияние порядка входных данных на скорость вставки в BST:

    • На случайных данных BST строится сбалансированным. Высота дерева составляет примерно O(log N), поэтому вставка происходит почти мгновенно.
    • На отсортированных данных происходит ДЕГРАДАЦИЯ дерева. Каждый элемент больше предыдущего и вставляется строго вправо. Дерево вырождается в связный список. Сложность возрастает до O(N), что отчетливо видно по гигантскому пику на графике.
  2. Чувствительность Хеш-таблицы к порядку:

    • Хеш-таблица НЕ ЧУВСТВИТЕЛЬНА к порядку данных. Математическая хеш-функция превращает любое имя в хаотичный индекс и равномерно распределяет записи по бакетам. В обоих режимах операции выполняются за константное время O(1).
  3. Почему связный список всегда медленен при поиске:

    • У связного списка нет индексов для прямого доступа. Поиск всегда линейный O(N) — алгоритм вынужден последовательно перебирать элементы от головы к хвосту.
  4. Как удаление работает в каждой структуре:

    • Связный список: O(N) затрачивается на линейный поиск узла, само удаление — O(1).
    • Хеш-таблица: O(1) нахождение бакета по хешу, удаление из цепочки коллизий мгновенно.
    • BST: В среднем O(log N), в худшем O(N). Требует поиска узла и перестройки связей (замена удаляемого узла на его потомка или минимальный элемент правого поддерева).

ВЫВОД:

  • ДЛЯ ЧАСТЫХ ВСТАВОК И ПОИСКА: Идеально подходит Хеш-таблица благодаря скорости O(1).
  • ДЛЯ ПОЛУЧЕНИЯ ДАННЫХ В ПОРЯДКЕ (АЛФАВИТНОМ): Стоит выбирать Двоичное дерево поиска (BST), так как обход дерева (In-order traversal) сразу выдает отсортированные данные. """ print(report)

if name == "main": run_experiments() plot_results() print_report()