Что такое структура данных

Представьте приложение, в котором хранятся миллионы записей.

Одной программе нужно постоянно находить запись по имени. Другой – обрабатывать задачи строго в порядке поступления. Третьей – хранить папки и файлы внутри других папок.

Во всех случаях программа работает с данными. Но просто сохранить данные недостаточно: нужно ещё решить, как их организовать, чтобы нужные операции было удобно выполнять.

Способ организации данных и работы с ними называется структурой данных.

Одни и те же данные можно организовать по-разному

Возьмём несколько чисел:

4, 7, 2, 9, 5

Мы можем расположить их последовательно:

[4, 7, 2, 9, 5]

Так числа лежат в массиве. Но те же значения можно представить иначе – связать каждое со следующим:

4 7 2 9 5
Связный список

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

Можно организовать эти числа и в виде дерева:

2 4 5 7 9
Дерево

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

Структура данных задаёт способ работы с данными

Вернёмся к последовательности:

[4, 7, 2, 9, 5]

Если нам нужен элемент с индексом 3, мы можем обратиться к нему напрямую:

numbers[3]

и получить:

9

Для массива такая операция естественна: каждый элемент имеет свою позицию.

Теперь рассмотрим связный список. У его элементов тоже есть порядок, но прямого доступа по индексу здесь уже нет. Чтобы добраться до 9, нужно начать с первого элемента и последовательно пройти по связям:

Получается, что одинаковые данные могут вести себя совершенно по-разному в зависимости от того, как они организованы.

Разные структуры удобны для разных операций

Массив удобен, когда нужно быстро обратиться к элементу по индексу: numbers[3]. Но теперь рассмотрим другую операцию – добавление нового элемента в начало.

Есть массив [4, 7, 9, 12], и нужно добавить 2 перед первым элементом. Просто поставить 2 перед 4 не получится: сначала нужно освободить место. Для этого все существующие элементы приходится сдвинуть на одну позицию вправо:

Если в массиве n элементов, при вставке в начало может потребоваться сдвинуть все n элементов.

Теперь посмотрим на связный список 4 → 7 → 9 → 12. У списка есть ссылка на первый элемент – head. Чтобы добавить 2 в начало, проходить по остальным элементам не нужно:

В массиве число сдвигов зависит от количества элементов. В связном списке всё проще: создаём новый узел, связываем его с текущим первым узлом и делаем новым head.

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

Не существует лучшей структуры данных

Было бы удобно найти одну структуру данных и использовать её всегда. Но такой структуры нет:

Поэтому вопрос обычно звучит не «какая структура данных самая быстрая?», а «какие операции важны именно в этой задаче?»

Если программа постоянно обращается к элементам по их позициям, удобно иметь структуру, где позиция элемента известна заранее. Если последнее добавленное действие нужно обработать первым, понадобится совсем другое поведение. Если мы хотим быстро находить пользователя по имени, структура снова будет другой.

Выбор зависит от задачи.

Структура данных – это не просто картинка

Когда мы видим массив, дерево или связный список, легко подумать, что разница только в том, как они нарисованы. Но структура данных определяет гораздо больше. Она задаёт:

  • какие данные хранятся;
  • как элементы связаны между собой;
  • какие операции можно выполнять;
  • как эти операции выполняются.

Например, у стека есть операции:

stackпсевдокод
stack.push(value)stack.pop()stack.peek()

У очереди:

queueпсевдокод
queue.enqueue(value)queue.dequeue()queue.front()

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

agesпсевдокод
ages['Alice'] = 24ages['Bob'] = 31

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

Одна структура – одно поведение

Рассмотрим знакомый пример из обычной программы – Undo. Пользователь выполняет действия:

  1. рисует круг;
  2. рисует прямоугольник;
  3. рисует стрелку.

После каждого действия программа сохраняет снимок холста. Когда пользователь нажимает Undo, первым должно отмениться последнее действие – стрелка. Следующее нажатие уберёт прямоугольник. И только потом – круг.

Undo – стек действий
draw('круг')
Пользователь рисует круг – на холсте появляется первая фигура.
history.push(snapshot())
draw('прямоугольник')
history.push(snapshot())
draw('стрелка')
history.push(snapshot())
undo()
undo()
undo()
вершинаистория

Последнее добавленное действие обрабатывается первым – именно такое поведение предоставляет стек.

Теперь другой пример. В программу поступают задачи: A, затем B, затем C. Если их нужно выполнять в том же порядке, сначала должна обработаться A, затем B, затем C:

Очередь задач – по шагам
queue.enqueue('A')
Задача A встаёт в очередь.
queue.enqueue('B')
queue.enqueue('C')
queue.dequeue()
queue.dequeue()
queue.dequeue()
ABCвыходвход

В обоих примерах мы храним последовательность элементов. Но нужный порядок обработки отличается. Поэтому отличаются и структуры данных.

Структура выбирается под задачу

Возьмём несколько совершенно разных требований.

Нужно быстро получать элемент по его позиции – для этого хорошо подходит массив. Нужно, чтобы последнее добавленное значение извлекалось первым, – так ведёт себя стек. Нужно обрабатывать элементы в порядке поступления – так работает очередь.

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

Это примеры, а не строгая таблица выбора

Как структуры данных связаны с алгоритмами

Алгоритм описывает способ решения задачи. Но он всегда работает с какими-то данными, и то, как эти данные организованы, может сильно влиять на само решение.

Допустим, нам нужно найти определённое значение. Если элементы хранятся в обычной неотсортированной последовательности:

[12, 4, 31, 7, 19]

может понадобиться проверять их один за другим. Если данные организованы иначе, у нас могут появиться дополнительные возможности – например, сразу исключать часть вариантов.

Поэтому структура данных и алгоритм тесно связаны. Структура определяет, как организованы данные. Алгоритм определяет, что мы с ними делаем. И хороший выбор структуры часто позволяет построить более удобный или эффективный алгоритм.

Именно поэтому в этом курсе алгоритмы и структуры данных изучаются вместе.

Как понять, какая структура нужна

Пока вам не нужно запоминать, какая структура для какой задачи используется. Важнее научиться задавать правильные вопросы:

  • Что именно нужно хранить?
  • Какие операции будут выполняться чаще всего?
  • Нужно ли быстро получать элемент по позиции?
  • Нужно ли часто искать значения?
  • Будут ли данные постоянно добавляться и удаляться?
  • Есть ли между элементами иерархия?
  • Могут ли элементы быть связаны друг с другом произвольным образом?

Ответы на такие вопросы постепенно приводят к выбору подходящей структуры данных.

// теперь вы знаете

  1. Структура данных – способ организации данных и работы с ними
  2. Одни и те же значения можно организовать по-разному: массивом, связным списком, деревом
  3. От структуры зависит, какие операции доступны и как они выполняются: numbers[3] у массива и проход по связям у списка
  4. Структуры, которая лучше подходит для всех задач, не существует – выбор определяют важные для задачи операции
  5. Структура данных описывает не только хранение, но и поведение: у стека – push, pop, peek, у очереди – enqueue, dequeue, front
  6. Стек отдаёт первым последнее добавленное, очередь – добавленное раньше всех
  7. Организация данных влияет на алгоритм: от неё зависит, какие шаги решения вообще возможны

Проверьте себя

Что лучше всего описывает структуру данных?

Почему одну структуру данных нельзя считать лучшей для всех задач?

В очередь последовательно добавили A, B и C. Какой элемент будет извлечён первым?

Выберите все правильные утверждения.

Выбери все верные варианты

Две структуры данных хранят одинаковый набор значений. Означает ли это, что доступ, добавление, удаление и поиск обязательно будут выполняться в них одинаково?