Основы алгоритмов и структур данных
Теория: Очередь и стек
Мы привыкли записывать математические выражения, опираясь на приоритет операций и на скобки. Именно поэтому
+ 2 * 4 + 5 = 16, но (3 + 2)* (4 + 5) = 45.Далеко не все помнят из школьной математики приоритеты сложения и умножения, поэтому в социальных сетях распространены задачи «Сколько будет 3 + 2 * 4 + 5?».
Польский математик Ян Лукасевич около ста лет назад предложил необычный способ записи выражений, в котором не нужны ни приоритеты операций, ни скобки. Сейчас этот способ называют обратной польской записью.
Обратная запись непривычна и не получила широкого распространения, но ее можно встретить в таких языках программирования, как Forth и PostScript. * В обратной польской записи знак операции записывается не между операндами, а после них. Посмотрим, как это выглядит на примерах:
- Обычная запись — 3 + 2, обратная — 3 2 +
- Обычная запись — (3 + 2) * (4 + 5), обратная — 3 2 4 5 + + *
Операторы в обратной записи не всегда должны быть в конце. Например, 3 + 2 * 4 + 5 можно записать так:
3 2 + 4 5 + *
Эта запись читается слева направо и воспринимается так:
- Число 3
- Число 2
- Операция сложения
- Число 4
- Число 5
- Операция сложения
- Операция умножения
Преимущество обратных выражений в том, что они не вызывают разночтения.
Чтобы разобраться на примере, дальше мы пошагово вычислим это выражение:
3 2 + 4 5 + *
Шаг 1. Берем из стопки два верхних числа — 3 и 2. Выполняем над ними первую операцию — сложение:
3 + 2 = 5
Шаг 2. Идем дальше по выражению — нужно взять следующие два значения и второй оператор. Берем эти значения и применяем к ним вторую операцию сложения:
4 + 5 = 9
Шаг 3. Вспомним изначальное выражение:
3 2 4 5 + + *
Мы провели две операции сложения. Оставим только умножение и запишем в выражение результаты первых двух вычислений:
5 9 *
Шаг 4. Проводим последнюю операцию и получаем результат:
5 * 9 = 45
Алгоритм вычисления очень прост, но требует новой для нас структуры данных. Представьте, что в вычислениях выше мы бы записывали каждое число на карточки и складывали бы из них стопку. Наверху стопки лежало бы число 45.
В программировании такие стопки называются стеком — от английского stack, то есть стопка или кипа.
В стеке, как и в стопке, мы имеем дело только с верхней карточкой — вершиной. Задача, которую решает стек — запомнить промежуточный результат для будущих вычислений.
В отличие от ранее изученных нами структур, стек обычно реализуют поверх других структур — массива или односвязного списка.
Реализация стека через массив
Реализуя структуру данных, разработчик ради удобства может добавить в нее дополнительные методы.
Разные реализации могут быть непохожи друг на друга, но мы всегда ожидаем найти основные методы — конечно, у разных структур они разные. У стека должны быть реализованы три обязательных метода:
- Метод
push()помещает элемент на вершину стека, как карточку наверх стопки - Метод
pop()убирает элемент с вершины и возвращает его - Метод
isEmpty()проверяет, пуст ли стек
В JavaScript методы push() и pop() уже присутствуют в массиве, поэтому мы можем использовать их как есть:
Javascript
Python
PHP
Java
Метод isEmpty() возвращает true, потому что массив пуст — содержит 0 элементов.
Воспользуемся нашим стеком, чтобы вычислить значение выражения 3 2 + 4 5 + *:
Javascript
Python
PHP
Java
Как видите, решение не очень сложное. Мы разбиваем строку с выражением на лексемы и обрабатываем каждую лексему в цикле.
Если лексема — это знак операции, то мы «снимаем» с вершины стека два числа, выполняем операцию и помещаем результат обратно на стек.
При выполнении операций важно обращать внимание на порядок чисел. Числа в стеке расположены в порядке, обратном тому, в котором мы их туда помещали. Поэтому сначала мы извлекаем второй операнд b, а потом первый a.
Порядок операндов не важен для таких операций, как сложение и умножение, потому что 3 + 2 равно 2 + 3. Но при делении это уже не так — 3 / 2 не равно 2 / 3.
Если лексема не похожа на знак операции, мы считаем, что это число. Тогда с помощью функции parseFloat() мы приводим строковую лексему к численному типу, чтобы ее можно было умножать и делить.
После завершения цикла на вершине стека должен храниться результат.
Реализация стека через односвязный список
Реализация стека с помощью массива выглядит простой, но вставка элементов в конец списка может приводить к задержкам. Чтобы избежать ресурсоемких операций, реализуем стек при помощи связного списка.
Метод push() будет добавлять узел в начало списка, а pop() — удалять узел из начала:
Javascript
Python
PHP
Java
Метод isEmpty() проверяет, есть ли у списка голова. Если в списке нет ни одного узла, поле head будет содержать null — это означает, что стек пуст.
Методы в старом и новом классах Stack выглядят совершенно одинаково: имеют одинаковые названия, параметры и поведение. Поэтому вместо старой реализации нового стека мы можем использовать новую. Она будет немного быстрее, но чтобы это заметить, нам потребуются достаточно большие выражения.
Накопление и отправка изменения
Сейчас мы практически никуда не выходим без смартфона. Кажется, что телефонные звонки — уже не самая нужная функция. В основном мы пользуемся мессенджерами, трекерами, календарями и программами учета калорий.
Программы часто хранят данные в облаке — то есть на серверах в интернете. Скажем, календарь нам нужен не только на смартфоне, но и на ноутбуке. А иногда нужно не только предоставить доступ с разных устройств, но и надежно хранить данные. В облаке надежнее, чем на смартфоне.
В теории, как только в программе появляются новые данные, она должна отправить их на сервер. Например, трекер понимает, что наша позиция изменилась и посылает в облако новые координаты.
Но мобильная связь не всегда надежна и мы регулярно оказываемся без интернета. Простые программы в этом случае показывают ошибку подключения.
Однако, кратковременная потеря связи — это штатная ситуация и современные программы научились с ней справляться. Если связи нет, они складывают данные во временное хранилище, а когда связь появляется, отправляют их на сервер.
Но для хранения данных подойдет не любое хранилище. Представим, что программа-трекер складывает координаты в стек. Из стека координаты извлекаются в обратном порядке, как будто человек двигался задом наперед.
Нам нужна структура данных, похожая на стек и проводящая те же операции — поместить и извлечь. При этом в отличие от стека, она не должна менять порядок элементов. Это похоже на очередь в магазине, где люди обслуживаются в том же порядке, в котором они подошли к кассе.
Такая структура действительно существует. Она называется очередь, а по английски — queue.
Реализация очереди через двусвязный список
Как и в стеке, в очереди есть три основных метода:
push()pop()isEmpty()
Отличие в том, что в стеке элементы вставляются и извлекаются с одного конца. В очереди элементы вставляются с одного конца, а извлекаются из другого.
Как мы помним, односвязный список — не симметричная структура данных. Вставка и удаление элементов в начале списка выполняются гораздо быстрее, чем в конце, поэтому односвязный список не очень подходит для реализации очереди.
Зато для этой цели подходит двусвязный список, одинаково быстро работающий как в начале, так и в конце:
Javascript
Python
PHP
Java
Операция push() сводится к вставке узла в начало двусвязного списка. А реализация pop() останется вам в виде упражнения.
LIFO и FIFO
Иногда в технической литературе вместо терминов стек и очередь, можно встретить другие термины:
- Стек или список LIFO — Last In First Out (последним пришел — первым ушел)
- Очередь или список FIFO — First In First Out (первым пришел — первым ушел)
Выводы
Повторим ключевые моменты из этого урока:
- Стеки и очереди полезны для решения широкого круга задач
- Стек может быть реализован либо с помощью массива, либо с помощью односвязного списка
- Очередь может быть реализована с помощью двусвязного списка
- Работу стека описывает принцип LIFO (последним пришел — первым ушел)
- Работу очереди описывает принцип FIFO (первым пришел — первым ушел)

