2026-rff_mp/svetlakovkyu/docs/report.md
2026-05-01 23:49:53 +03:00

14 KiB
Raw Blame History

Задание 1: Структуры данных

Выполнил: Светлаков Кирилл

Студент 426 группы

Цель работы

Реализовать три различные структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. Необходимо собственными руками написать код, чтобы понять внутреннее устройство связного списка, хеш-таблицы и двоичного дерева поиска, а также осознать их сильные и слабые стороны на практике.


1. Реализация структур данных

1.1 Связный список (Linked List)

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

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
    current = head
    if head['name'] == name:
        return head['next']
    while current['next'] is not None:
        if current['next']['name'] == name:
            current['next'] = current['next']['next']
            break
        current = current['next']
    return head
    
def ll_list_all(head):
    res = []
    current = head
    while current is not None:
        res.append((current['name'], current['phone']))
        current = current['next']
    res.sort()
    return res

1.2 Хеш-таблица (Hash Table)

def ht_insert(buckets, name, phone):
    index = hash(name) % len(buckets)
    current_head = buckets[index]
    new_head = ll_insert(current_head, name, phone) 
    buckets[index] = new_head

def ht_find(buckets, name):
    index = hash(name)%len(buckets)
    slot_head = buckets[index]
    res_ph = ll_find(slot_head, name)
    return res_ph

def ht_delete(buskets, name):
    index = hash(name)%len(buskets)
    buskets[index] = ll_delete(buskets[index], name)

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

1.3 Двоичное дерево поиска (BST)

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

def get_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:
            return root['right']
        if root['right'] is None:
            return root['left']
        successor = get_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, res = None):
    if res is None:
        res = []
    if root is not None:
        bst_list_all(root['left'], res)
        res.append({'name': root['name'], 'phone': root['phone']})
        bst_list_all(root['right'], res)
    #сортировка уже сделана
    return res

2. Эксперимент

Для экспериментального сравнения были сгенерированы 10 000 записей вида (User_XXXXX, номерелефона).

Каждый тест проводился в двух режимах входных данных:

  • Случайный (shuffled) - записи перемешаны в произвольном порядке
  • Отсортированный (sorted) - записи отсортированы по имени

Для каждой структуры и режима измерялось время выполнения трёх операций:

  • Вставка - 10 000 элементов
  • Поиск - 110 запросов (100 существующих + 10 несуществующих)
  • Удаление - 50 элементов

Каждый замер повторялся 5 раз, итоговое значение - среднее арифметическое.

В файл results.py записывались в следующем виде

Structure Mode Operation Time
Название структуры: LL, BST, HT Режим данных: shufled, sorted Операция и номер попытки или среднее Время выполнения в секундах
LL shufled Вставка (попытка 1) 2.730272
LL shufled Вставка (попытка 2) 2.675253
LL shufled Вставка (попытка 3) 2.628982
LL shufled Вставка (попытка 4) 2.673355
LL shufled Вставка (попытка 5) 2.636129
LL shufled Вставка СРЕДНЕЕ 2.668798
... ... ... ...

3. Результаты

3.1 Вставка

График 1

3.2 Поиск

График 2

3.3 Удаление

График 3


4. Анализ результатов

4.1 Влияние порядка данных на BST: деградация до O(n)

При вставке случайных данных BST ведёт себя как сбалансированное дерево: каждый новый ключ с равной вероятностью уходит влево или вправо, глубина дерева составляет ~log₂(10000) ≈ 13. Операция вставки 10 000 записей заняла 0.025 сек.

При вставке отсортированных данных каждый новый ключ оказывается больше предыдущего и всегда уходит вправо. Дерево вырождается в линейный список глубиной 10 000. Каждая следующая вставка проходит на один шаг дальше, итоговая сложность становится O(1 + 2 + ... + n) = O(n²). Именно поэтому вставка отсортированных данных заняла 13.16 сек - более чем в 500 раз медленнее случайных.

Это фундаментальный недостаток, реализованного BST.


4.2 Нечувствительность хеш-таблицы к порядку данных

Хеш-таблица вычисляет позицию элемента через hash(name) % len(buckets). Функция hash() в Python зависит только от значения ключа, но никак не от порядка вставки. Независимо от того, отсортированы данные или перемешаны, каждый ключ попадает в тот же бакет с той же скоростью.

Это подтверждается полученными результатами: время вставки для случайных данных - 0.0281 сек, для отсортированных - 0.0286 сек, что практически одинаково.

В графике для поиска и удаления, моожно заметить примено такой же результат - время работы программы для отсортированных и не для неотсортированных данных одинаково.


4.3 Медленный поиск в связном списке

Связный список не имеет структуры, позволяющей перейти к нужному элементу. При поиске приходится последовательно проходить узел за узлом от головы до нужного элемента - это линейный поиск O(n).

При N = 10 000 и 110 запросах среднее время поиска составило 0.03 сек, в ~150 раз медленнее поиска в BST и в ~75 раз медленнее поиска в HT на случайных данных.

Порядок данных на LL практически не влияет: в любом случае нужно проходить примерно половину списка для найденных и весь список для несуществующих записей.


4.4 Удаление в каждой структуре

Связный список: удаление требует сначала найти удаляемый элемент - O(n). Затем достаточно переключить одну ссылку у предшественника. Итог: O(n) за счёт поиска. При 50 удалениях время составило ~0.02 сек.

Хеш-таблица: вычисляем бакет за O(1), затем удаляем элемент из короткой цепочки. Итог: O(1) амортизированно. При 50 удалениях время составило 0.00003 сек для случайных и 0.00004 для отсортированых данных, что значительно быстрее связного списка, но медленее BST(для неотсортированных данных).

BST (случайные данные): находим узел за O(log n). Если у него два потомка - находим минимум правого поддерева, копируем его значение и рекурсивно удаляем его. Итог: O(log n). При 50 удалениях время составило 0.00012 сек.

BST (отсортированные данные): дерево вырождено в список, глубина O(n). Каждое удаление - O(n), 50 удалений - 0.060 сек, что медленнее даже связного списка.


5. Выводы

Из результатов эксперимента, можно сделать вывод, что нет универсальной структуры данных, и под конкретную задачу надо выбирать определенную.

Хеш-таблица - лучший выбор, если нужны быстрые вставка, поиск и удаление и порядок хранения данных не важен. Идеальна для кешей, словарей, телефонных справочников, где операции выполняются в O(1). Не подходит для задач, требующих обхода данных в отсортированном порядке.

BST - лучший выбор, когда важен порядок данных: обход дерева in-order даёт отсортированную последовательность за O(n), можно быстро найти минимум/максимум или диапазон ключей. Подходит для задач типа «найти все записи от A до B». Критически важно использовать только на случайных или специально перемешанных данных.

Связный список - подходит для задач, где данные постоянно добавляются и удаляются с известной позиции (начало/конец списка), а поиск по значению происходит редко. В телефонном справочнике с 10 000 записей является наихудшим вариантом из трёх.

Задача Лучшая структура
Частые вставки и удаления по ключу Хеш-таблица
Частый поиск по ключу Хеш-таблица
Обход данных в отсортированном порядке BST
Поиск по диапазону ключей BST
Встава/удаление в начало/конец Связный список
Данные приходят отсортированными, нужен быстрый поиск Хеш-таблица