Классификация структур данных: по каким признакам их делят

Классификация структур данных: по каким признакам их делят Полезное

Классификация структур данных — это разбиение способов организации данных в памяти на группы по общим признакам: по расположению элементов (линейные или нелинейные), по составу (примитивные или составные) и по изменчивости размера (статические или динамические). Один и тот же тип попадает сразу в несколько групп: массив одновременно линейный, составной и статический.

Ниже — три оси классификации с явным разграничением близких терминов, обзорная таблица семи базовых структур со сложностью операций и короткий алгоритм выбора структуры под задачу. Это обзорная статья: глубокий разбор отдельных структур — по ссылкам в конце.

Три независимые оси: не путать признаки

Структуру данных классифицируют не по одному признаку, а по нескольким осям сразу. Путаница возникает, когда «линейный» противопоставляют «динамическому» — это признаки из разных осей, они не исключают друг друга.

Мини-словарь осей:

  • расположение элементов — линейная структура (элементы идут цепочкой, у каждого один сосед слева и один справа) против нелинейной (у элемента может быть несколько соседей);
  • состав — примитивная структура (неделимая единица) против составной (собрана из других структур);
  • изменчивость — статическая (размер фиксирован заранее) против динамической (размер меняется во время работы программы).

Массив — линейный, составной, статический. Связный список — линейный, составной, динамический. Дерево — нелинейное, составное, динамическое. Одна структура получает по метке с каждой оси.

Линейные и нелинейные

Линейная структура хранит элементы в виде последовательности: есть первый элемент, последний, и у каждого промежуточного ровно один предыдущий и один следующий. К линейным относят массив, строку, стек, очередь, связный список.

Нелинейная структура допускает у элемента несколько связей. В дереве у узла один родитель, но несколько потомков; в графе связи между вершинами произвольны. К нелинейным относят деревья и графы.

Признак линейности — про логические связи, а не про физическое расположение в памяти. Массив линеен и лежит в памяти подряд, а связный список тоже линеен, но его узлы разбросаны по памяти и соединены указателями. То есть линейность не означает «лежит сплошным куском».

Примитивные и составные

Примитивная структура (базовый, простой тип) — неделимая с точки зрения программы единица: целое число, символ, логическое значение, ссылка (указатель). Для нее заранее известны размер в памяти и набор допустимых операций.

Составная структура (сложная, интегрированная) собирается из других структур — примитивных или, в свою очередь, составных. Массив, запись (структура с именованными полями), список, дерево — составные.

Граница «примитивный» зависит от языка и платформы, это упрощение. В одном языке строка — примитив, в другом (например, в C) строка — составная структура из массива символов. Размер целого числа тоже платформо-зависим, поэтому «примитив» — это не абсолют, а роль типа в конкретном языке.

Статические, полустатические и динамические

Ось изменчивости — про то, меняется ли число элементов и связей во время работы программы.

  • Статические — размер фиксирован и выделяется заранее (на этапе компиляции или при создании). Пример: массив фиксированной длины, запись. Плюс — быстрый доступ по адресу; минус — нельзя выйти за исходный размер.
  • Полустатические — длина переменная, но в заранее заданных пределах, с простыми операциями изменения (добавить/убрать с одного конца). Пример: стек и очередь. Это промежуточная категория из классической русскоязычной литературы по структурам данных; в англоязычных источниках такой отдельной группы обычно нет — это разница терминологий, а не сути.
  • Динамические — размер ограничен только доступной памятью, элементы создаются и удаляются в любой момент, связи между ними задаются указателями. Пример: связный список, дерево, граф.

За гибкость динамических структур платят: указатели занимают дополнительную память, а доступ к элементу требует прохода по цепочке, а не вычисления адреса.

Обзор семи базовых структур

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

Структура Назначение Доступ по индексу Поиск значения Вставка Удаление
Массив (динамич.) хранение по индексу, быстрый доступ O(1) O(n) O(1) в конце*, O(n) внутри O(n)
Связный список частые вставки/удаления по краям O(n) O(n) O(1) в известной точке O(1) в известной точке
Стек (LIFO) откат, вложенность, парсинг O(1) push O(1) pop
Очередь (FIFO) буфер задач, обход в ширину O(1) enqueue O(1) dequeue
Хеш-таблица поиск по ключу O(1) в среднем, O(n) в худшем O(1) в среднем O(1) в среднем
Дерево поиска упорядоченное хранение, диапазоны O(log n) если сбалансировано O(log n) O(log n)
Граф связи «многие ко многим» зависит от обхода O(1) добавить ребро (список смежности) зависит от представления

Оговорки к таблице (это упрощенные типичные значения, не гарантии):

  • «O(1) в конце*» для массива — амортизированная оценка: чаще всего вставка мгновенна, но при переполнении массив копируется целиком.
  • Хеш-таблица дает O(1) только в среднем; при массовых коллизиях операции вырождаются в O(n).
  • Дерево поиска дает O(log n) только в сбалансированном виде; несбалансированное дерево вырождается в цепочку с O(n).
  • Для графа сложность зависит от представления (матрица или список смежности) и от алгоритма обхода.

Минимальные примеры на Python

Массив в Python — это тип list: доступ по индексу за O(1), добавление в конец амортизированно за O(1).

arr = [10, 20, 30]
print(arr[1])       # 20
arr.append(40)
print(arr)          # [10, 20, 30, 40]

Стек (LIFO) удобно строить на list, очередь (FIFO) — на deque, где удаление слева идет за O(1):

from collections import deque

stack = []
stack.append("a")
stack.append("b")
print(stack.pop())      # b  (последним пришел - первым ушел)

queue = deque()
queue.append("a")
queue.append("b")
print(queue.popleft())  # a  (первым пришел - первым ушел)

Хеш-таблица в Python — это dict: поиск по ключу в среднем за O(1):

prices = {"hleb": 45, "moloko": 80}
print(prices["moloko"])  # 80

Узел связного списка — пример указательной структуры: каждый узел хранит значение и ссылку на следующий:

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

head = Node("A")
head.next = Node("B")
print(head.value, head.next.value)  # A B

Как выбрать структуру под задачу

Выбор идет не от «какая структура красивее», а от того, какая операция у вас самая частая. Короткий разбор по задаче:

  1. Нужен быстрый доступ по номеру позиции — массив.
  2. Часто вставляете и удаляете по краям, а доступ по индексу не важен — связный список, стек или очередь.
  3. Нужен принцип «последним пришел — первым ушел» (откат действий, разбор скобок) — стек.
  4. Нужен принцип «первым пришел — первым ушел» (очередь задач, обход в ширину) — очередь.
  5. Ищете по ключу и порядок не важен — хеш-таблица.
  6. Нужен порядок и поиск по диапазону («все значения от 10 до 20») — дерево поиска.
  7. Моделируете связи «многие ко многим» (маршруты, соцсвязи) — граф.

Если сомневаетесь, начните с массива или хеш-таблицы: они закрывают большинство прикладных задач, а к спискам, деревьям и графам переходят, когда упирается конкретная операция.

Выводы

  • Классификация идет по трем независимым осям: расположение (линейные/нелинейные), состав (примитивные/составные), изменчивость (статические/динамические); одна структура получает метку с каждой оси.
  • Линейность — про логические связи, а не про сплошное расположение в памяти: связный список линеен, но разбросан по памяти.
  • Динамические структуры гибче, но платят памятью на указатели и временем на проход по цепочке.
  • Сложность операций — ориентир для выбора: массив силен доступом по индексу, хеш-таблица — поиском по ключу, дерево — упорядоченным поиском по диапазону.
  • Приведенные O-оценки типичные и имеют условия: O(1) хеш-таблицы и O(log n) дерева держатся только без массовых коллизий и при балансировке.

Где применяется / связь с практикой

Выбор структуры данных — повседневная работа при проектировании: от него зависит скорость поиска, объем памяти и читаемость кода. Умение сопоставить задачу и структуру, а затем оценить сложность операций, входит в базу для собеседований и для проектирования систем.

Освойте тему на практике

Системно разобрать структуры данных вместе с алгоритмами над ними помогает курс Алгоритмы и структуры данных: там разбирают не только сами структуры, но и оценку сложности и выбор под задачу. Прежде чем идти на полный курс, можно бесплатно посмотреть формат на открытых уроках и оценить, насколько тема заходит.

Смежные темы: Дерево как структура данных, Табличная структура данных, Типы данных в C.

FAQ

Чем структура данных отличается от типа данных? Тип данных описывает множество допустимых значений и операций (например, целое число). Структура данных — это способ организации набора таких значений и связей между ними (например, массив целых чисел). Тип — про одну ячейку, структура — про их организацию.

Абстрактный тип данных (АТД) — это то же, что структура данных? Нет. АТД (например, «стек») задает поведение и набор операций, не фиксируя реализацию. Структура данных — конкретная реализация этого поведения; один и тот же стек можно построить на массиве или на связном списке.

Полустатические структуры — обязательная категория? Нет, это термин из части русскоязычной литературы для стеков, очередей и строк с ограниченной переменной длиной. Во многих англоязычных источниках такой отдельной группы нет, а эти структуры относят к линейным динамическим. Это разница классификаций, а не разных объектов.

OTUS Журнал