import time
import csv
import random
import sys
from typing import List, Tuple, Optional, Any, Dict

#лимит рекурсии 
sys.setrecursionlimit(20000)
def ll_insert(head: Optional[Dict], name: str, phone: str) -> Dict:
    """Вставка в конец связного списка"""
    new_node = {'name': name, 'phone': phone, 'next': None}
    
    if head is None:
        return new_node
    
    current = head
    while current['next'] is not None:
        # Обновляем, если уже есть
        if current['name'] == name:
            current['phone'] = phone
            return head
        current = current['next']
    
    if current['name'] == name:
        current['phone'] = phone
    else:
        current['next'] = new_node
    
    return head


def ll_find(head: Optional[Dict], 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[Dict], name: str) -> Optional[Dict]:
    """Удаление из связного списка"""
    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: Optional[Dict]) -> List[Tuple[str, str]]:
    """Сбор всех записей из связного списка с сортировкой"""
    records = []
    current = head
    while current is not None:
        records.append((current['name'], current['phone']))
        current = current['next']
def hash_function(name: str, size: int) -> int:
    """Простая хеш-функция"""
    return sum(ord(c) for c in name) % size


def ht_create(size: int = 1000) -> List[Optional[Dict]]:
    """Создание хеш-таблицы"""
    return [None] * size


def ht_insert(buckets: List[Optional[Dict]], name: str, phone: str) -> None:
    """Вставка в хеш-таблицу"""
    index = hash_function(name, len(buckets))
    buckets[index] = ll_insert(buckets[index], name, phone)


def ht_find(buckets: List[Optional[Dict]], name: str) -> Optional[str]:
    """Поиск в хеш-таблице"""
    index = hash_function(name, len(buckets))
    return ll_find(buckets[index], name)


def ht_delete(buckets: List[Optional[Dict]], name: str) -> None:
    """Удаление из хеш-таблицы"""
    index = hash_function(name, len(buckets))
    buckets[index] = ll_delete(buckets[index], name)


def ht_list_all(buckets: List[Optional[Dict]]) -> List[Tuple[str, str]]:
    """Сбор всех записей из хеш-таблицы с сортировкой"""
    records = []
    for head in buckets:
        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
def bst_insert(root: Optional[Dict], name: str, phone: str) -> Dict:
    """Вставка в BST (итеративная)"""
    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
            else:
                current = current['left']
        elif name > current['name']:
            if current['right'] is None:
                current['right'] = new_node
                break
            else:
                current = current['right']
        else:
            # Обновляем существующую запись
            current['phone'] = phone
            break
    
    return root


def bst_find(root: Optional[Dict], name: str) -> Optional[str]:
    """Поиск в BST (итеративный)"""
    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: Dict) -> Dict:
    """Поиск узла с минимальным значением"""
    current = node
    while current['left'] is not None:
        current = current['left']
    return current


def bst_delete(root: Optional[Dict], name: str) -> Optional[Dict]:
    """Удаление из BST (итеративная версия)"""
    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
    
    # Случай 1: узел не имеет детей
    if current['left'] is None and current['right'] is None:
        if parent is None:
            return None
        if parent['left'] == 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']
        
        # Копируем данные из successor в current
        current['name'] = successor['name']
        current['phone'] = successor['phone']
        
        # Удаляем successor
        if successor_parent['left'] == successor:
            successor_parent['left'] = successor['right']
        else:
            successor_parent['right'] = successor['right']
        
        return root
    
    # Присоединяем ребёнка к родителю
    if parent is None:
        return child
    if parent['left'] == current:
        parent['left'] = child
    else:
        parent['right'] = child
    
    return root


def bst_inorder(root: Optional[Dict], records: List[Tuple[str, str]]) -> None:
    """Центрированный обход BST (рекурсивный)"""
    if root is not None:
        bst_inorder(root['left'], records)
        records.append((root['name'], root['phone']))
        bst_inorder(root['right'], records)


def bst_list_all(root: Optional[Dict]) -> List[Tuple[str, str]]:
    """Сбор всех записей из BST (уже отсортированы)"""
    records = []
    bst_inorder(root, records)
    return records


    
    records.sort(key=lambda x: x[0])
    return records

def generate_records(n: int) -> List[Tuple[str, str]]:
    """Генерация записей"""
    records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)]
    return records
def measure_insertion(structure_type: str, data: List[Tuple[str, str]], 
                      ht_size: int = 1000) -> float:
    """Замер времени вставки"""
    start = time.perf_counter()
    
    if structure_type == "LinkedList":
        head = None
        for name, phone in data:
            head = ll_insert(head, name, phone)
    
    elif structure_type == "HashTable":
        buckets = ht_create(ht_size)
        for name, phone in data:
            ht_insert(buckets, name, phone)
    
    elif structure_type == "BST":
        root = None
        for name, phone in data:
            root = bst_insert(root, name, phone)
    
    end = time.perf_counter()
    return end - start


def measure_find(structure_type: str, data: List[Tuple[str, str]], 
                 existing_names: List[str], missing_names: List[str],
                 ht_size: int = 1000) -> Tuple[float, Any]:
    """Замер времени поиска (возвращает время и структуру для удаления)"""
    # Сначала создаём структуру
    if structure_type == "LinkedList":
        head = None
        for name, phone in data:
            head = ll_insert(head, name, phone)
        
        start = time.perf_counter()
        for name in existing_names + missing_names:
            ll_find(head, name)
        end = time.perf_counter()
        return end - start, head
    
    elif structure_type == "HashTable":
        buckets = ht_create(ht_size)
        for name, phone in data:
            ht_insert(buckets, name, phone)
        
        start = time.perf_counter()
        for name in existing_names + missing_names:
            ht_find(buckets, name)
        end = time.perf_counter()
        return end - start, buckets
    
    elif structure_type == "BST":
        root = None
        for name, phone in data:
            root = bst_insert(root, name, phone)
        
        start = time.perf_counter()
        for name in existing_names + missing_names:
            bst_find(root, name)
        end = time.perf_counter()
        return end - start, root


def measure_delete(structure_type: str, structure: Any, 
                   names_to_delete: List[str]) -> float:
    """Замер времени удаления"""
    start = time.perf_counter()
    
    if structure_type == "LinkedList":
        head = structure
        for name in names_to_delete:
            head = ll_delete(head, name)
    
    elif structure_type == "HashTable":
        buckets = structure
        for name in names_to_delete:
            ht_delete(buckets, name)
    
    elif structure_type == "BST":
        root = structure
        for name in names_to_delete:
            root = bst_delete(root, name)
    
    end = time.perf_counter()
    return end - start
def run_experiment(n_records: int = 10000, n_find: int = 110, 
                   n_delete: int = 50, n_runs: int = 5) -> List[List]:
    """Запуск всех замеров"""
    
    # Генерация данных
    all_records = generate_records(n_records)
    records_shuffled = all_records.copy()
    random.shuffle(records_shuffled)
    records_sorted = sorted(all_records, key=lambda x: x[0])
    
    # Имена для поиска
    all_names = [name for name, _ in all_records]
    existing_names = random.sample(all_names, n_find - 10)
    missing_names = [f"None_{i}" for i in range(10)]
    
    # Имена для удаления
    names_to_delete = random.sample(all_names, n_delete)
    
    structures = ["LinkedList", "HashTable", "BST"]
    modes = {"shuffled": records_shuffled, "sorted": records_sorted}
    
    # Заголовок CSV
    results = [["Структура", "Режим", "Операция", 
                "Замер1", "Замер2", "Замер3", "Замер4", "Замер5", "Среднее"]]
    
    for structure in structures:
        for mode_name, mode_data in modes.items():
            print(f"\nТестирование: {structure}, режим: {mode_name}")
            
            # Вставка
            insertion_times = []
            for run in range(n_runs):
                print(f"  Вставка, run {run+1}/{n_runs}...")
                t = measure_insertion(structure, mode_data)
                insertion_times.append(t)
            
            avg_insertion = sum(insertion_times) / n_runs
            results.append([structure, mode_name, "вставка"] + 
                          [f"{t:.6f}" for t in insertion_times] + 
                          [f"{avg_insertion:.6f}"])
            print(f"    Замеры: {[f'{t:.6f}' for t in insertion_times]}")
            print(f"    Среднее: {avg_insertion:.6f} сек")
            
            # Поиск
            find_times = []
            for run in range(n_runs):
                print(f"  Поиск, run {run+1}/{n_runs}...")
                t, _ = measure_find(structure, mode_data, 
                                      existing_names, missing_names)
                find_times.append(t)
            
            avg_find = sum(find_times) / n_runs
            results.append([structure, mode_name, "поиск"] + 
                          [f"{t:.6f}" for t in find_times] + 
                          [f"{avg_find:.6f}"])
            print(f"    Замеры: {[f'{t:.6f}' for t in find_times]}")
            print(f"    Среднее: {avg_find:.6f} сек")
            
            # Удаление
            delete_times = []
            for run in range(n_runs):
                print(f"  Удаление, run {run+1}/{n_runs}...")
                # Создаём свежую структуру для каждого замера удаления
                if structure == "LinkedList":
                    head = None
                    for name, phone in mode_data:
                        head = ll_insert(head, name, phone)
                    t = measure_delete(structure, head, names_to_delete)
                elif structure == "HashTable":
                    buckets = ht_create()
                    for name, phone in mode_data:
                        ht_insert(buckets, name, phone)
                    t = measure_delete(structure, buckets, names_to_delete)
                elif structure == "BST":
                    root = None
                    for name, phone in mode_data:
                        root = bst_insert(root, name, phone)
                    t = measure_delete(structure, root, names_to_delete)
                
                delete_times.append(t)
            
            avg_delete = sum(delete_times) / n_runs
            results.append([structure, mode_name, "удаление"] + 
                          [f"{t:.6f}" for t in delete_times] + 
                          [f"{avg_delete:.6f}"])
            print(f"    Замеры: {[f'{t:.6f}' for t in delete_times]}")
            print(f"    Среднее: {avg_delete:.6f} сек")
    
    return results

def save_results(results: List[List], filename: str = "results.csv"):
    """Сохранение результатов в CSV"""
    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerows(results)
    print(f"\nРезультаты сохранены в {filename}")
def plot_results(results_file: str = "results.csv"):
    """Построение графика сравнения производительности"""
    import matplotlib.pyplot as plt
    import numpy as np
    
    # Чтение результатов из CSV
    data = {}
    with open(results_file, 'r', encoding='utf-8') as f:
        reader = csv.reader(f)
        header = next(reader)  # пропускаем заголовок
        
        for row in reader:
            structure = row[0]
            mode = row[1]
            operation = row[2]
            # Берём последнюю колонку (Среднее)
            avg_time = float(row[-1])
            
            if structure not in data:
                data[structure] = {}
            if mode not in data[structure]:
                data[structure][mode] = {}
            
            data[structure][mode][operation] = avg_time
    
    # Настройка стиля
    plt.style.use('seaborn-v0_8-darkgrid')
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    colors = ['#FF6B6B', '#4ECDC4']
    
    structures = ["LinkedList", "HashTable", "BST"]
    modes = ["shuffled", "sorted"]
    operations = ["вставка", "поиск", "удаление"]
    op_titles = ["ВСТАВКИ", "ПОИСКА (110 запросов)", "УДАЛЕНИЯ (50 записей)"]
    
    for idx, (op, op_title) in enumerate(zip(operations, op_titles)):
        ax = axes[idx]
        x = np.arange(len(structures))
        width = 0.35
        
        shuffled_vals = [data[s]['shuffled'][op] for s in structures]
        sorted_vals = [data[s]['sorted'][op] for s in structures]
        
        bars1 = ax.bar(x - width/2, shuffled_vals, width, 
                       label='Случайный порядок', color=colors[0])
        bars2 = ax.bar(x + width/2, sorted_vals, width, 
                       label='Отсортированный порядок', color=colors[1])
        
        ax.set_xlabel('Структура данных')
        ax.set_ylabel('Время (секунды)')
        ax.set_title(f'Сравнение времени {op_title}')
        ax.set_xticks(x)
        ax.set_xticklabels(['Связный\nсписок', 'Хеш-\nтаблица', 'Двоичное\nдерево'])
        ax.legend()
        
        # Добавляем значения на столбцы
        for bars in [bars1, bars2]:
            for bar in bars:
                height = bar.get_height()
                fmt = '{:.4f}'.format(height) if op == 'вставка' else '{:.6f}'.format(height)
                ax.text(bar.get_x() + bar.get_width()/2., height,
                        fmt, ha='center', va='bottom', fontsize=8)
    
    plt.tight_layout()
    plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()
    
    # Вывод сводной таблицы в консоль
    print("\n" + "=" * 90)
    print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ (среднее время в секундах)")
    print("=" * 90)
    print(f"{'Структура':<15} {'Режим':<12} {'Вставка':<14} {'Поиск':<14} {'Удаление':<14}")
    print("-" * 90)
    
    for structure in structures:
        for mode in modes:
            print(f"{structure:<15} {mode:<12} "
                  f"{data[structure][mode]['вставка']:<14.6f} "
                  f"{data[structure][mode]['поиск']:<14.6f} "
                  f"{data[structure][mode]['удаление']:<14.6f}")
    
    print("=" * 90)
def main():
    print("=" * 60)
    print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ")
    print("Связный список | Хеш-таблица | Двоичное дерево поиска")
    print("=" * 60)
    
    # Запуск эксперимента (5 прогонов)
    results = run_experiment(n_records=10000, n_runs=5)
    
    # Сохранение результатов
    save_results(results)
    
    # Построение графика
    try:
        import matplotlib.pyplot as plt
        print("\nПостроение графика...")
        plot_results("results.csv")
    
    except ImportError:
        print("\nВНИМАНИЕ: Библиотека matplotlib не установлена.")
        print("Для построения графика выполните: pip install matplotlib")
        print("Результаты сохранены в CSV файл, вы можете построить график в Excel.")
    
    print("\n" + "=" * 60)
    print("ЭКСПЕРИМЕНТ ЗАВЕРШЁН")
    print("=" * 60)


if __name__ == "__main__":
    main()
