Структуры данных
Структуры данных: массив, связный список, стек, очередь, дек, хеш-таблица, множество, дерево, куча, граф. Операции, сложность O() и примеры на Python.
Структуры данных — это способы организации и хранения информации, от которых напрямую зависит скорость работы программы. Эта шпаргалка собирает ключевые структуры (массив, связный список, стек, очередь, дек, хеш-таблицу, множество, дерево, кучу и граф), их операции, асимптотическую сложность и готовые реализации на Python из стандартной библиотеки.
Как читать сложность (Big O)
O(1) — константа, не зависит от размера. O(log n) — логарифм, очень медленно растёт. O(n) — линейно. O(n log n) — типичная сортировка. O(n²) — квадрат, медленно. Ниже под каждой структурой указана сложность основных операций в худшем (а где важно — в усреднённом) случае.
Массив и динамический список (array, dynamic array)
Массив хранит элементы подряд в памяти, доступ по индексу — за O(1). В Python встроенный тип list — это динамический массив: он сам расширяется при добавлении. Минус массива — вставка и удаление в середину требуют сдвига элементов.
Когда применять
- Нужен быстрый доступ по индексу и проход по порядку.
- Добавление в основном в конец.
- Не нужны частые вставки/удаления в начало или середину.
Сложность операций
| Операция | Сложность | Комментарий |
|---|---|---|
Доступ по индексу a[i] | O(1) | прямая адресация |
Добавить в конец append | O(1)* | амортизированно |
Вставить в начало insert(0, x) | O(n) | сдвиг всех элементов |
| Удалить из середины | O(n) | сдвиг хвоста |
Поиск значения x in a | O(n) | перебор |
a = [10, 20, 30]
a.append(40) # [10, 20, 30, 40] — O(1)*
print(a[2]) # 30 — O(1)
a.insert(0, 5) # [5, 10, 20, 30, 40] — O(n)
a.pop() # удалить с конца — O(1)
a.pop(0) # удалить с начала — O(n)
print(20 in a) # поиск — O(n)
Связный список (singly / doubly linked list)
Связный список хранит элементы как узлы, каждый из которых ссылается на следующий (односвязный) или ещё и на предыдущий (двусвязный). Вставка и удаление узла — O(1), если ссылка на узел уже есть, но доступ по индексу медленный — O(n), нужно идти по цепочке.
Когда применять
- Частые вставки/удаления в начало или середину при известной позиции.
- Не нужен быстрый доступ по индексу.
- В Python чаще берут
collections.dequeвместо ручного списка.
Сложность операций
| Операция | Сложность | Комментарий |
|---|---|---|
| Доступ по индексу | O(n) | проход по узлам |
| Вставка в начало | O(1) | перецепить ссылку |
| Удаление узла (есть ссылка) | O(1) | в двусвязном |
| Поиск значения | O(n) | перебор |
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
# Односвязный список 1 -> 2 -> 3
head = Node(1, Node(2, Node(3)))
# Вставка в начало — O(1)
head = Node(0, head)
# Обход — O(n)
node = head
while node:
print(node.value, end=" ") # 0 1 2 3
node = node.next
Стек (stack, LIFO)
Стек работает по принципу LIFO (last in, first out) — последний пришёл, первый вышел. Как стопка тарелок: кладём и берём только сверху. Обе операции — O(1).
Когда применять
- Откат действий (undo), история переходов.
- Проверка скобок, обход в глубину (DFS), вычисление выражений.
- Рекурсия по сути использует стек вызовов.
Сложность операций
| Операция | Сложность |
|---|---|
Положить push (append) | O(1) |
Снять pop | O(1) |
Посмотреть верх peek | O(1) |
# Стек на обычном list
stack = []
stack.append("a") # push
stack.append("b")
top = stack[-1] # peek -> "b"
x = stack.pop() # pop -> "b"
print(stack) # ["a"]
Очередь (queue, FIFO)
Очередь работает по принципу FIFO (first in, first out) — первый пришёл, первый вышел. Как очередь в магазине. В Python для очереди берут collections.deque: добавление в конец и удаление из начала — O(1). Использовать обычный list.pop(0) нельзя — это O(n).
Когда применять
- Обработка задач в порядке поступления.
- Обход в ширину (BFS) по графу.
- Буферы, пайплайны, планировщики.
Сложность операций
| Операция | deque | list |
|---|---|---|
| Добавить в конец | O(1) | O(1)* |
| Взять из начала | O(1) | O(n) |
from collections import deque
queue = deque()
queue.append("task1") # в конец
queue.append("task2")
first = queue.popleft() # из начала -> "task1" — O(1)
print(queue) # deque(["task2"])
Дек (deque, двусторонняя очередь)
Дек (double-ended queue) — это очередь, в которую можно добавлять и из которой можно удалять с обоих концов за O(1). В Python это collections.deque — универсальный инструмент и для стека, и для очереди.
Когда применять
- Нужны быстрые операции с обоих концов.
- Скользящее окно (sliding window), история ограниченной длины (
maxlen).
Сложность операций
| Операция | Сложность |
|---|---|
append / appendleft | O(1) |
pop / popleft | O(1) |
| Доступ по индексу в середине | O(n) |
from collections import deque
d = deque([1, 2, 3])
d.appendleft(0) # [0, 1, 2, 3]
d.append(4) # [0, 1, 2, 3, 4]
d.popleft() # 0
d.pop() # 4
# дек ограниченной длины
window = deque(maxlen=3)
for x in [1, 2, 3, 4]:
window.append(x)
print(window) # deque([2, 3, 4], maxlen=3)
Хеш-таблица (hash table / dict)
Хеш-таблица хранит пары ключ → значение и обеспечивает доступ по ключу в среднем за O(1). В Python это встроенный тип dict. Ключи должны быть хешируемыми (неизменяемыми): строки, числа, кортежи.
Когда применять
- Нужен быстрый поиск/вставка/удаление по ключу.
- Подсчёт частот, кеширование, индексы, группировка.
Сложность операций
| Операция | Среднее | Худшее |
|---|---|---|
Вставка d[k] = v | O(1) | O(n) |
Поиск d[k] | O(1) | O(n) |
Удаление del d[k] | O(1) | O(n) |
Проверка k in d | O(1) | O(n) |
Худший случай O(n) возникает крайне редко — при массовых коллизиях хешей.
from collections import Counter
d = {"apple": 3, "banana": 5}
d["cherry"] = 7 # вставка — O(1)
print(d["banana"]) # 5 — поиск O(1)
print("apple" in d) # True
del d["apple"] # удаление — O(1)
# Подсчёт частот через Counter
text = "abracadabra"
freq = Counter(text)
print(freq.most_common(2)) # [("a", 5), ("b", 2)]
Множество (set)
Множество хранит уникальные элементы без порядка и поддерживает проверку принадлежности за O(1). Под капотом — та же хеш-таблица. В Python это тип set (и неизменяемый frozenset).
Когда применять
- Убрать дубликаты.
- Быстро проверить «есть ли элемент».
- Операции над множествами: объединение, пересечение, разность.
Сложность операций
| Операция | Сложность |
|---|---|
Добавить add | O(1) |
Проверка x in s | O(1) |
Удалить discard | O(1) |
| Объединение / пересечение | O(n) |
a = {1, 2, 3}
b = {3, 4, 5}
a.add(4) # {1, 2, 3, 4}
print(2 in a) # True — O(1)
print(a | b) # объединение {1, 2, 3, 4, 5}
print(a & b) # пересечение {3, 4}
print(a - b) # разность {1, 2}
# Убрать дубликаты из списка
nums = [1, 1, 2, 3, 3, 3]
print(set(nums)) # {1, 2, 3}
Дерево и двоичное дерево поиска (binary tree, BST)
Дерево — иерархическая структура из узлов, у каждого есть потомки. В двоичном дереве у узла максимум двое детей. Двоичное дерево поиска (BST) хранит элементы упорядоченно: слева меньшие, справа большие — поиск идёт за O(log n) для сбалансированного дерева.
Когда применять
- Нужны упорядоченные данные с быстрым поиском, вставкой и обходом по порядку.
- Иерархии: файловая система, DOM, дерево решений.
- Сбалансированные деревья (AVL, красно-чёрные) лежат в основе многих БД-индексов.
Сложность операций (BST)
| Операция | Сбаланс. | Худшее |
|---|---|---|
| Поиск | O(log n) | O(n) |
| Вставка | O(log n) | O(n) |
| Удаление | O(log n) | O(n) |
Худший случай O(n) — когда дерево выродилось в «список» (узлы добавляли по возрастанию).
class TreeNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
def insert(root, value):
if root is None:
return TreeNode(value)
if value < root.value:
root.left = insert(root.left, value)
else:
root.right = insert(root.right, value)
return root
def inorder(root):
if root:
inorder(root.left)
print(root.value, end=" ") # по возрастанию
inorder(root.right)
root = None
for v in [5, 3, 8, 1, 4]:
root = insert(root, v)
inorder(root) # 1 3 4 5 8
Куча и очередь с приоритетом (heap, priority queue)
Куча (heap) — это двоичное дерево, где родитель всегда не больше детей (min-heap) или не меньше (max-heap). Это даёт мгновенный доступ к минимуму/максимуму за O(1) и извлечение за O(log n). На куче строят очередь с приоритетом. В Python — модуль heapq (min-heap на основе list).
Когда применять
- Нужно постоянно доставать минимальный/максимальный элемент.
- Алгоритм Дейкстры, A*, задачи «топ-K».
- Планировщики задач по приоритету.
Сложность операций
| Операция | Сложность |
|---|---|
Посмотреть минимум heap[0] | O(1) |
Вставить heappush | O(log n) |
Извлечь минимум heappop | O(log n) |
Построить кучу heapify | O(n) |
import heapq
heap = [5, 3, 8, 1]
heapq.heapify(heap) # превратить в кучу — O(n)
heapq.heappush(heap, 2) # вставка — O(log n)
print(heap[0]) # минимум -> 1 — O(1)
print(heapq.heappop(heap)) # извлечь минимум -> 1
# Очередь с приоритетом: (приоритет, задача)
pq = []
heapq.heappush(pq, (2, "отправить письмо"))
heapq.heappush(pq, (1, "срочный звонок"))
print(heapq.heappop(pq)) # (1, "срочный звонок")
# max-heap трюк: храним отрицательные значения
nums = [5, 3, 8, 1]
print(heapq.nlargest(2, nums)) # [8, 5]
Граф (graph): матрица и список смежности
Граф — это набор вершин (узлов) и рёбер (связей между ними). Графы бывают ориентированные/неориентированные, взвешенные/невзвешенные. Два главных способа хранения: матрица смежности и список смежности.
Когда применять
- Любые сети связей: дороги, соцсети, зависимости, маршруты.
- Поиск пути (BFS/DFS, Дейкстра), проверка связности, поиск циклов.
Два представления
| Критерий | Матрица смежности | Список смежности |
|---|---|---|
| Память | O(V²) | O(V + E) |
| Проверка ребра (u, v) | O(1) | O(степени u) |
| Перебор соседей | O(V) | O(степени u) |
| Лучше для | плотных графов | разреженных графов |
V — число вершин, E — число рёбер.
from collections import deque, defaultdict
# Список смежности (разреженный граф)
graph = defaultdict(list)
edges = [(0, 1), (0, 2), (1, 3), (2, 3)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # неориентированный
# Матрица смежности (4 вершины)
n = 4
matrix = [[0] * n for _ in range(n)]
for u, v in edges:
matrix[u][v] = 1
matrix[v][u] = 1
# Обход в ширину (BFS) — O(V + E)
def bfs(start):
visited = {start}
q = deque([start])
order = []
while q:
node = q.popleft()
order.append(node)
for nxt in graph[node]:
if nxt not in visited:
visited.add(nxt)
q.append(nxt)
return order
print(bfs(0)) # порядок обхода от вершины 0
Сравнительная таблица структур
| Структура | Доступ | Поиск | Вставка | Удаление | Python |
|---|---|---|---|---|---|
| Массив / список | O(1) | O(n) | O(n) | O(n) | list |
| Связный список | O(n) | O(n) | O(1) | O(1) | — |
| Стек | O(n) | O(n) | O(1) | O(1) | list |
| Очередь | O(n) | O(n) | O(1) | O(1) | deque |
| Дек | O(n) | O(n) | O(1) | O(1) | deque |
| Хеш-таблица | — | O(1)* | O(1)* | O(1)* | dict |
| Множество | — | O(1)* | O(1)* | O(1)* | set |
| BST (сбаланс.) | O(log n) | O(log n) | O(log n) | O(log n) | — |
| Куча | O(1) min | O(n) | O(log n) | O(log n) | heapq |
* — усреднённая сложность. Прочерк означает, что операция для структуры не характерна.
Как выбрать структуру
- Доступ по индексу и порядок → массив /
list. - Быстрый поиск по ключу → хеш-таблица /
dict. - Уникальность и членство → множество /
set. - LIFO → стек (
list); FIFO → очередь (deque). - Всегда нужен минимум/максимум → куча /
heapq. - Упорядоченные данные с быстрым поиском → сбалансированное дерево (BST).
- Сети связей и маршруты → граф (список смежности для разреженных, матрица — для плотных).
Смотри также шпаргалки по сложности алгоритмов и сортировкам, чтобы закрепить асимптотику на практике.