import random
import time
import csv
import os
import traceback
import sys

sys.setrecursionlimit(30000)

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:
        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, name):
    
    """
    ищет узел по имени.
    возвращает телефон или None
    """
    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:
        new_head = head['next']
        head['next'] = None
        return new_head
    
    # ищем узел для удаления
    current = head
    while current['next'] is not None:
        if current['next']['name'] == name:
          target = current['next']
          current['next'] = target['next']
          target['next'] = None  # разрываем связь у удаляемого узла (иными словами, обнуление ссылки)
          return head
        current = current['next']
    
    return head

def ll_collect(head, result_list):
    """собирает все данные из связного списка в result_list"""
    current = head
    while current is not None:
        result_list.append((current['name'], current['phone']))
        current = current['next']


def ll_list_all(head):
    
    """
    собирает все записи в список и сортирует
    """
    result = []
    current = head
    
    while current is not None:
        result.append((current['name'], current['phone']))
        current = current['next']
    
    # ручная сортировка пузырьком
    n = len(result)
    for i in range(n):
        for j in range(0, n - i - 1):
            if result[j][0] > result[j + 1][0]:
                result[j], result[j + 1] = result[j + 1], result[j]
    
    return result


def hash_table(size):
    """создание хеш-таблицы"""
    return [None] * size


def hash_func(name, buckets_count):
    """
    использует умножение на простое число для лучшего распределения
    """
    h = 0
    multiplier = 1
    for char in name:
        h = (h + ord(char) * multiplier) % buckets_count
        multiplier = (multiplier * 31) % buckets_count
    return h


def ht_insert(buckets, name, phone):
    """добавить или обновить запись"""
    if buckets is None:
        return
    
    index = hash_func(name, len(buckets))
    buckets[index] = ll_insert(buckets[index], name, phone)


def ht_find(buckets, name):
    """найти телефон по имени"""
    idx = hash_func(name, len(buckets))
    return ll_find(buckets[idx], name)


def ht_delete(buckets, name):
    """
    удалить запись
    """
    idx = hash_func(name, len(buckets))
    buckets[idx] = ll_delete(buckets[idx], name)


def bubble_sort(records):
    """пузырьковая сортировка"""
    n = len(records)
    for i in range(n - 1):
        swapped = False
        for j in range(n - 1 - i):
            if records[j][0] > records[j + 1][0]:
                records[j], records[j + 1] = records[j + 1], records[j]
                swapped = True
        if not swapped:
            break
    return records


def ht_list_all(buckets):
    """
    собрание всех записей и сортировка
    """
    # Собираем все записи
    full_data = []
    for head in buckets:
        ll_collect(head, full_data)
    
    # Сортируем пузырьком
    bubble_sort(full_data)
    return full_data

#Hash_table1 = hash_table(3)

#ht_insert(Hash_table1, 'Alena', '010')
#ht_insert(Hash_table1, 'Helena', '111')
#ht_insert(Hash_table1, 'Gena', '222')


#print(ht_list_all(Hash_table1))

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):
    """
    рекурсивный поиск телефона по имени
    возвращает phone или None
    """
    # не нашли
    if root is None:
        return None
    
    # нашли
    if root['name'] == name:
        return root['phone']
    
    # ищем в левом поддереве
    elif name < root['name']:
        return bst_find(root['left'], name)
    
    # ищем в правом поддереве
    else:
        return bst_find(root['right'], name)


def bst_find_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 and root['right'] is None:
            return None
        
        
        elif root['left'] is None:
            return root['right']
        elif root['right'] is None:
            return root['left']
        
        
        # находим минимальный элемент в правом поддереве
        successor = bst_find_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, result=None):
    """
    центрированный (in-order) обход дерева
    рекурсивно собирает записи в отсортированном порядке
    """
    if result is None:
        result = []
    
    if root is not None:
        # сначала обходим левое поддерево (все меньшие ключи)
        bst_list_all(root['left'], result)
        
        # затем текущий узел
        result.append((root['name'], root['phone']))
        
        # затем правое поддерево (все большие ключи)
        bst_list_all(root['right'], result)
    
    return result

def generate_records(count=10000):
    """
    генерация тестовых данных
    70% уникальных имён, 30% повторяющихся (для коллизий)
    """
    records = []
    base_names = ["Алексей", "Борис", "Владимир", "Дмитрий", "Елена", 
                  "Иван", "Мария", "Николай", "Ольга", "Павел"]
    
    for i in range(count):
        if random.random() < 0.7:
            name = f"User_{i:05d}"
        else:
            name = random.choice(base_names) + f"_{random.randint(1, 100)}"
        
        phone = f"+7-{random.randint(100,999)}-{random.randint(100,999)}-{random.randint(1000,9999)}"
        records.append((name, phone))
    
    shuffled = records.copy()
    random.shuffle(shuffled)
    sorted_records = sorted(records, key=lambda x: x[0])
    
    return shuffled, sorted_records


def measure_insertion(structure_name, records):
    """
    замер времени вставки
    возвращает список замеров и заполненную структуру
    """
    times = []
    filled_structure = None
    
    for run in range(5):
        if structure_name == "linked_list":
            structure = None
        elif structure_name == "hash_table":
            structure = hash_table(2003)
        elif structure_name == "bst":
            structure = None
        
        start = time.perf_counter()
        
        for name, phone in records:
            if structure_name == "linked_list":
                structure = ll_insert(structure, name, phone)
            elif structure_name == "hash_table":
                ht_insert(structure, name, phone)
            elif structure_name == "bst":
                structure = bst_insert(structure, name, phone)
        
        end = time.perf_counter()
        times.append(end - start)
        
        if run == 4:  # Сохраняем после последнего замера
            filled_structure = structure
    
    return times, filled_structure


def measure_search(structure_name, structure, search_names):
    """
    замер времени поиска
    возвращает список замеров
    """
    times = []
    
    for run in range(5):
        start = time.perf_counter()
        
        for name in search_names:
            if structure_name == "linked_list":
                ll_find(structure, name)
            elif structure_name == "hash_table":
                ht_find(structure, name)
            elif structure_name == "bst":
                bst_find(structure, name)
        
        end = time.perf_counter()
        times.append(end - start)
    
    return times


def measure_deletion(structure_name, original_structure, delete_names):
    """
    замер времени удаления
    возвращает список замеров
    """
    times = []
    
    for run in range(5):
        # создаём копию структуры
        if structure_name == "linked_list":
            all_records = ll_list_all(original_structure)
            test_structure = None
            for name, phone in all_records:
                test_structure = ll_insert(test_structure, name, phone)
        
        elif structure_name == "hash_table":
            all_records = ht_list_all(original_structure)
            test_structure = hash_table(2003)
            for name, phone in all_records:
                ht_insert(test_structure, name, phone)
        
        elif structure_name == "bst":
            all_records = bst_list_all(original_structure)
            test_structure = None
            for name, phone in all_records:
                test_structure = bst_insert(test_structure, name, phone)
        
        start = time.perf_counter()
        
        for name in delete_names:
            if structure_name == "linked_list":
                test_structure = ll_delete(test_structure, name)
            elif structure_name == "hash_table":
                ht_delete(test_structure, name)
            elif structure_name == "bst":
                test_structure = bst_delete(test_structure, name)
        
        end = time.perf_counter()
        times.append(end - start)
    
    return times

print(f"Текущая рабочая директория: {os.getcwd()}")
print(f"Путь к файлу: {os.path.abspath(__file__)}")

def run_experiment():
    """
    запуск всех экспериментов и сохранение результатов
    """
    
    current_dir = os.path.dirname(__file__)
    docs_dir = os.path.dirname(current_dir)
    csv_file = os.path.join(docs_dir, "experiment_results.csv")
   
    
    
    os.makedirs(docs_dir, exist_ok=True)
    
    print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ")
    print("Телефонный справочник - 10000 записей")
    print(f"\nРезультаты будут сохранены в: {csv_file}")
    
    # генерация данных
    print("\n1. Генерация тестовых данных...")
    shuffled_records, sorted_records = generate_records(10000)
    print(f"Сгенерировано 10000 записей")
    print(f"Уникальных имён: {len(set([r[0] for r in shuffled_records]))}")
    
    # подготовка имён для поиска и удаления
    random.seed(42)
    existing_names = [shuffled_records[i][0] for i in random.sample(range(10000), 100)]
    nonexisting_names = [f"NotExist_{i}" for i in range(10)]
    search_names = existing_names + nonexisting_names
    delete_names = [shuffled_records[i][0] for i in random.sample(range(10000), 50)]
    
    results = [["Структура", "Режим", "Операция", 
                "Замер1(с)", "Замер2(с)", "Замер3(с)", "Замер4(с)", "Замер5(с)", 
                "Среднее(с)"]]
    
    # тестирование для каждого режима
    for mode_name, records in [("случайный", shuffled_records), 
                                ("отсортированный", sorted_records)]:
        
        print(f"\n2. Тестирование режима: {mode_name}")
        
        
        for struct_name in ["linked_list", "hash_table", "bst"]:
            print(f"\n   {struct_name.upper()}:")
            
            # вставка
            print("Вставка 10000 записей...")
            insert_times, filled_struct = measure_insertion(struct_name, records)
            avg_insert = sum(insert_times) / 5
            print(f"Время: {avg_insert:.4f} сек (среднее)")
            
            # поиск
            print("Поиск 110 записей (100 существующих + 10 которых нет)...")
            search_times = measure_search(struct_name, filled_struct, search_names)
            avg_search = sum(search_times) / 5
            print(f"Время: {avg_search:.4f} сек (среднее)")
            
            # удаление
            print("Удаление 50 случайных записей...")
            delete_times = measure_deletion(struct_name, filled_struct, delete_names)
            avg_delete = sum(delete_times) / 5
            print(f"Время: {avg_delete:.4f} сек (среднее)")
            
            # сохраняем результаты
            results.append([struct_name, mode_name, "вставка"] + insert_times + [avg_insert])
            results.append([struct_name, mode_name, "поиск"] + search_times + [avg_search])
            results.append([struct_name, mode_name, "удаление"] + delete_times + [avg_delete])
    
    # сохранение CSV
    print("\n3. Сохранение результатов...")
    try:
        with open(csv_file, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.writer(f, delimiter=';')
            writer.writerows(results)
        print(f"Результаты сохранены в: {csv_file}")
    except Exception as e:
        print(f"Ошибка сохранения: {e}")
    
    # вывод табл.
    print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ")
    print(f"{'Структура':<15} {'Режим':<12} {'Операция':<10} {'Среднее время (сек)':<20}")
    
    for row in results[1:]:
        struct, mode, op, t1, t2, t3, t4, t5, avg = row
        print(f"{struct:<15} {mode:<12} {op:<10} {avg:<20.6f}")
    
    return results

def create_report_table(results):
    """Создание сводной таблицы"""

    print("СВОДНАЯ ТАБЛИЦА (среднее время в секундах)")

    print(f"{'Структура':<12} {'Режим':<12} {'Вставка':<12} {'Поиск':<12} {'Удаление':<12}")

    summary = {}
    for row in results[1:]:
        struct, mode, op, _, _, _, _, _, avg = row
        key = (struct, mode)
        if key not in summary:
            summary[key] = {}
        summary[key][op] = avg
    
    names = {'linked_list': 'LinkedList', 'hash_table': 'HashTable', 'bst': 'BST'}
    for (struct, mode), ops in summary.items():
        print(f"{names[struct]:<12} {mode:<12} {ops.get('вставка', 0):<12.6f} {ops.get('поиск', 0):<12.6f} {ops.get('удаление', 0):<12.6f}")


def print_analysis():
    """вывод краткого анализа"""
  
    print("АНАЛИЗ РЕЗУЛЬТАТОВ")
 
    print("""
1. Влияние порядка данных на BST:
   - На случайных данных: быстро O(log n)
   - На отсортированных: деградация до O(n) (дерево вырождается в список)

2. Хеш-таблица не чувствительна к порядку:
   - Хеш-функция случайно распределяет данные по bucket'ам
   - Порядок вставки не влияет на время операций

3. Связный список всегда медленен при поиске:
   - Поиск требует последовательного прохода O(n)
   - Нет индексов или сортировки для ускорения

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

5. Рекомендация для реальных задач:
   - Хеш-таблица: частый поиск, словари, кэши
   - BST (сбалансированный): нужны отсортированные данные
   - Связный список: маленькие объёмы, очереди/стеки
   - Для телефонного справочника ЛУЧШЕ: ХЕШ-ТАБЛИЦА
""")
    
def create_graphs(results):
    """Построение столбчатых диаграмм"""
    import matplotlib.pyplot as plt
    import numpy as np
    
    data = {}
    for row in results[1:]:
        struct = row[0]
        mode = row[1]
        op = row[2]
        avg = row[8]
        
        if struct not in data:
            data[struct] = {}
        if mode not in data[struct]:
            data[struct][mode] = {}
        data[struct][mode][op] = avg
    
    # Настройки
    struct_names = {'linked_list': 'LinkedList', 'hash_table': 'HashTable', 'bst': 'BST'}
    colors = {'linked_list': '#3498db', 'hash_table': '#2ecc71', 'bst': '#e74c3c'}
    modes = ['случайный', 'отсортированный']
    operations = ['вставка', 'поиск', 'удаление']
    op_titles = ['Вставка (10000 записей)', 'Поиск (110 запросов)', 'Удаление (50 записей)']
    
    fig, axes = plt.subplots(1, 3, figsize=(14, 5))
    fig.suptitle('Сравнение производительности структур данных', fontsize=14, fontweight='bold')
    
    for idx, (op, title) in enumerate(zip(operations, op_titles)):
        ax = axes[idx]
        x = np.arange(len(modes))
        width = 0.25
        multiplier = 0
        
        for struct in ['linked_list', 'hash_table', 'bst']:
            values = [data[struct][mode][op] for mode in modes]
            bars = ax.bar(x + multiplier * width, values, width, 
                         label=struct_names[struct], color=colors[struct])
            
            for bar, val in zip(bars, values):
                if val < 0.001:
                    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
                           f'{val:.6f}', ha='center', va='bottom', fontsize=7)
                elif val < 0.01:
                    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
                           f'{val:.5f}', ha='center', va='bottom', fontsize=7)
                else:
                    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
                           f'{val:.3f}', ha='center', va='bottom', fontsize=8)
            multiplier += 1
        
        ax.set_title(title)
        ax.set_ylabel('Время (сек)')
        ax.set_yscale('log')
        ax.set_ylim(1e-5, 10)
        ax.set_xticks(x + width)
        ax.set_xticklabels(['Случайный', 'Отсортированный'])
        ax.legend()
        ax.grid(True, alpha=0.3, axis='y')
    
    plt.tight_layout()
    
    current_dir = os.path.dirname(__file__)
    docs_dir = os.path.dirname(current_dir)
    path = os.path.join(docs_dir, 'graphs.png')
    plt.savefig(path, dpi=150)
    plt.close()
    print(f"\nГрафики сохранены: {path}")
    return path 

if __name__ == "__main__":
    results = run_experiment()
    create_report_table(results)
    create_graphs(results)
    print_analysis()
  
    print("ЭКСПЕРИМЕНТ ВЫПОЛНЕН ПОЛНОСТЬЮ!")
