Структура данных — это способ организовать набор значений в памяти вместе с операциями над ними: добавить, найти, удалить, обойти. Ключевое различие, которое дальше держим сквозь статью: абстрактный тип данных (АТД) описывает, какие операции доступны и как они себя ведут (контракт), а реализация описывает, как это устроено внутри и сколько стоит каждая операция. Один и тот же АТД можно построить на разных реализациях с разной сложностью.
Содержание
- АТД против реализации: развести сразу
- Массив и связный список: две базовые реализации
- Стек и очередь: АТД поверх базовых структур
- Хеш-таблица: доступ по ключу
- Дерево: иерархия и порядок
- Граф: произвольные связи
- Сводная таблица: сложность операций
- Как выбрать структуру: короткий алгоритм
- Выводы
- Где применяется / связь с практикой
- FAQ
Примеры на Python и псевдокоде — иллюстрация идеи, а не особенность языка. Разберем базовые структуры (массив, связный список), абстрактные типы поверх них (стек, очередь), а также хеш-таблицу, дерево и граф, и дадим алгоритм выбора по операциям и сложности.
АТД против реализации: развести сразу
Чтобы примеры дальше не казались противоречивыми, зафиксируем мини-словарь.
- АТД (контракт) — набор операций и их обещанное поведение. Пример: стек обещает
push,pop,topи правило LIFO (последним вошел — первым вышел). Про память и скорость контракт молчит. - Реализация — конкретное устройство в памяти. Тот же стек можно построить на массиве или на связном списке. Обе реализации выполняют контракт, но по-разному расходуют память и по-разному ведут себя при росте.
- Оценка сложности O — как растет число шагов операции при росте размера n. O(1) — не зависит от n; O(log n) — растет медленно; O(n) — линейно; O(n^2) — квадратично.
Практический вывод: выбирают сначала АТД (какие операции нужны), потом реализацию (какая дает нужную сложность). «Нужен стек» — это про контракт, «нужен массив» — уже про реализацию.
Массив и связный список: две базовые реализации
Почти все линейные структуры внутри опираются на одну из двух моделей хранения.
Массив (в динамическом варианте — список Python, ArrayList, vector) держит элементы в непрерывном участке памяти подряд. За счет этого доступ по индексу — O(1): адрес элемента вычисляется арифметикой от начала. Плата — вставка или удаление в середине сдвигает хвост, это O(n).
Связный список хранит элементы как отдельные узлы, каждый узел ссылается на следующий. Непрерывности нет, поэтому доступ по индексу — O(n) (надо идти по ссылкам). Зато вставка и удаление в уже найденной точке — O(1): достаточно переставить ссылки.
Минимальный узел односвязного списка на псевдокоде:
class Node:
def __init__(self, value):
self.value = value
self.next = None # ссылка на следующий узел, None - конец
# a -> b -> None
a = Node("a")
b = Node("b")
a.next = b
print(a.value, a.next.value) # a b
Чтобы получить второй элемент, мы прошли по a.next, а не обратились по индексу: для n элементов это до n переходов, отсюда O(n) на доступ.
Стек и очередь: АТД поверх базовых структур
Стек — АТД с дисциплиной LIFO: доступен только верхний элемент. Операции push (положить наверх), pop (снять верхний), top (посмотреть верхний) — все O(1). Типичные применения: отмена действий (undo), разбор скобок, обход в глубину, стек вызовов функций.
stack = []
stack.append(1) # push
stack.append(2)
stack.append(3)
print(stack.pop()) # 3 - последний вошел, первым вышел
print(stack.pop()) # 2
print(stack) # [1]
Очередь — АТД с дисциплиной FIFO: элементы выходят в порядке прихода. Операции enqueue (добавить в конец) и dequeue (взять из начала). На обычном массиве взятие из начала было бы O(n) из-за сдвига, поэтому в Python берут deque (дек на блоках), где оба конца — O(1).
from collections import deque
q = deque()
q.append("a") # enqueue
q.append("b")
q.append("c")
print(q.popleft()) # a - первый вошел, первым вышел
print(q.popleft()) # b
print(list(q)) # ['c']
Обратите внимание: стек и очередь — это контракты, а list и deque — реализации. Один и тот же стек можно собрать и на связном списке. Выбор реализации меняет сложность, но не поведение LIFO/FIFO.
Хеш-таблица: доступ по ключу
Хеш-таблица (в Python — dict) хранит пары ключ-значение и дает поиск, вставку и удаление по ключу в среднем за O(1). Идея: хеш-функция переводит ключ в индекс во внутреннем массиве, и мы попадаем в нужную ячейку почти сразу.
prices = {}
prices["milk"] = 80
prices["bread"] = 45
print(prices["milk"]) # 80 - доступ по ключу
print("eggs" in prices) # False - проверка наличия
Важная граница: O(1) — это средняя оценка. При неудачных коллизиях (много ключей в одну ячейку) операция вырождается к O(n), и гарантии худшего случая у хеш-таблицы нет — в отличие от сбалансированного дерева. Второе следствие: сортировки по ключу хеш-таблица не дает (в CPython dict сохраняет порядок вставки как деталь реализации, но опираться на упорядоченность нельзя).
Дерево: иерархия и порядок
Дерево — нелинейная структура из узлов, где у каждого узла есть потомки, а путь от корня к любому узлу единственный. Частный случай — двоичное дерево поиска (BST): для каждого узла левое поддерево меньше его, правое больше. Это дает поиск за O(log n), если дерево сбалансировано.
class TreeNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def insert(root, key):
if root is None:
return TreeNode(key)
if key < root.key:
root.left = insert(root.left, key)
else:
root.right = insert(root.right, key)
return root
root = None
for k in [8, 3, 10, 1, 6]:
root = insert(root, k)
print(root.key, root.left.key, root.right.key) # 8 3 10
Границу назовем явно: O(log n) верна только для сбалансированного дерева. Если вставлять отсортированные ключи в наивный BST, он выродится в цепочку и даст O(n). Поэтому на практике берут самобалансирующиеся варианты (AVL, красно-черное дерево). Деревья лежат в основе индексов баз данных (там чаще B-дерево под диск) и сортированных множеств.
Граф: произвольные связи
Граф — самая общая структура: узлы (вершины) и связи (ребра) без ограничения на форму. Ребра бывают направленными и ненаправленными, с весами и без. Граф моделирует сети: дороги, друзей, зависимости задач, ссылки между страницами.
Чаще всего граф хранят списком смежности — словарь, где ключ это вершина, а значение список ее соседей:
graph = {
"A": ["B", "C"],
"B": ["D"],
"C": ["D"],
"D": [],
}
print(graph["A"]) # ['B', 'C'] - соседи вершины A
Список смежности экономен для разреженных графов: память O(V + E). Альтернатива — матрица смежности O(V^2), удобна, когда ребер много. Обходят граф в ширину (BFS, на очереди) или в глубину (DFS, на стеке) — вот где базовые АТД возвращаются как инструмент.
Сводная таблица: сложность операций
Оценки — для типичных реализаций; среднее и худший случай различаются там, где это отмечено.
| Структура (реализация) | Доступ по индексу | Поиск значения | Вставка | Удаление | Память |
|---|---|---|---|---|---|
| Динамический массив | O(1) | O(n) | O(1) в конец, O(n) в середину | O(n) | O(n) |
| Связный список | O(n) | O(n) | O(1) в известной точке | O(1) в известной точке | O(n) |
| Стек / очередь | — | — | O(1) | O(1) | O(n) |
| Хеш-таблица | — | O(1) среднее, O(n) худшее | O(1) среднее | O(1) среднее | O(n) |
| Сбалансированное BST | — | O(log n) | O(log n) | O(log n) | O(n) |
| Граф (список смежности) | — | O(V + E) обход | O(1) добавить ребро | O(степень) | O(V + E) |
Как выбрать структуру: короткий алгоритм
Идти стоит от операций, которые в задаче выполняются чаще всего.
- Нужен доступ по позиции и предсказуемая память, вставки в основном в конец — динамический массив.
- Часто вставляете и удаляете в середине, а доступ по индексу не нужен — связный список.
- Обрабатываете в порядке LIFO (последнее — первым) — стек; в порядке FIFO (очередь задач, обход в ширину) — очередь.
- Главное — быстрый поиск по ключу, порядок не важен — хеш-таблица.
- Нужен быстрый поиск И упорядоченный обход или диапазоны — сбалансированное дерево.
- Моделируете связи многие-ко-многим, ищете пути — граф.
Правило-ориентир: назовите две-три самые частые операции и по таблице выберите структуру, у которой именно они дешевые. Проигрыш на редкой операции обычно допустим.
Выводы
- АТД — это контракт (какие операции и их поведение), реализация — устройство в памяти и стоимость операций; выбирают сначала контракт, потом реализацию.
- Массив дает O(1) доступ по индексу, но O(n) на вставку в середину; связный список наоборот — O(1) вставка в точке, O(n) доступ.
- Стек (LIFO) и очередь (FIFO) — абстрактные типы, их можно строить на массиве или списке с O(1) на основные операции.
- Хеш-таблица дает O(1) в среднем, но без гарантии худшего случая и без порядка; сбалансированное дерево дает O(log n) с гарантией и с упорядоченным обходом.
- Выбор структуры ведут от самых частых операций задачи и их сложности по сводной таблице, а не от привычки.
Где применяется / связь с практикой
Структуры данных решают, будет ли программа работать за секунды или за минуты на тех же данных. Понимание разницы между АТД и реализацией и умение читать сложность O влияют на выбор архитектуры, на собеседования и на отладку узких мест. Дальше эти структуры соединяются с алгоритмами: обход графа, сортировки, поиск, динамическое программирование.
Освойте тему на практике
Если хочется собрать это в систему и научиться выбирать структуру и алгоритм под задачу осознанно, посмотрите курс Алгоритмы и структуры данных. Формат и глубину удобно оценить на открытых уроках Otus — там разбирают конкретные задачи вживую.
FAQ
Чем структура данных отличается от типа данных?
Скалярный тип (целое, символ, логический) описывает одно значение и операции над ним. Структура данных организует набор значений и связи между ними. В этой статье речь именно о композитных структурах, а не о примитивных типах языка.
Массив или список — что быстрее?
Зависит от операции. Массив быстрее на доступе по индексу (O(1) против O(n)), связный список — на вставке и удалении в уже найденной точке (O(1) против O(n)). Универсально «быстрее» нет, сравнивать нужно по частым операциям задачи.
Почему у хеш-таблицы пишут O(1), но иногда O(n)?
O(1) — средняя оценка при равномерном распределении по ячейкам. При массовых коллизиях (много ключей в одну ячейку) операция вырождается к O(n). Гарантии худшего случая, в отличие от сбалансированного дерева, у хеш-таблицы нет.



