Как реализовать дек: пошаговое решение
Дано: дек на кольцевом массиве ёмкостью , в начале пустой, , . Семь операций подряд: push_back 10, push_back 20, push_back 30, push_front 40, push_front 50, pop_back, pop_front. Найти: индекс головы, индекс хвоста, размер и порядок элементов после последнего шага.
Метод: хранить только два числа, индекс головы и количество элементов , а индекс хвоста считать формулой . Ответ: , , , дек от головы к хвосту равен 40, 10, 20; при этом pop_back вернул 30, а pop_front вернул 50. Калькулятор сверху прогоняет эту же последовательность по шагам и показывает массив после каждого, а ниже те же семь шагов расписаны руками.
Решение по шагам
Дано. Массив из восьми ячеек с индексами от 0 до 7, счётчики и . Правила четырёх операций записываем один раз и дальше только подставляем числа:
Шаги 1-3. Три push_back подряд. Свободное место в хвосте всегда лежит по индексу . Пока голова стоит в нуле, это просто , , : числа 10, 20 и 30 ложатся в три первые ячейки, доходит до трёх, не двигается.
Шаг 4. push_front 40 уводит голову через край. Свободной ячейки слева от нуля в массиве нет, поэтому индекс уходит по кругу: . Значение 40 пишется в последнюю ячейку массива, хотя логически стоит первым. Слагаемое здесь обязательно: в большинстве языков остаток от отрицательного числа отрицателен, и без него получился бы индекс .
Шаг 5. push_front 50. По той же формуле , значение 50 попадает в ячейку 6, . Теперь дек занимает ячейки 6, 7, 0, 1, 2 и физически разорван на два куска, но логически это единый непрерывный отрезок.
Шаг 6. pop_back снимает элемент с хвоста. Сначала уменьшаем размер: . Затем берём значение по индексу , то есть . Порядок важен: если сначала прочитать, а потом уменьшить, придётся писать в формуле и легко ошибиться на единицу.
Шаг 7. pop_front снимает элемент с головы. Читаем , сдвигаем голову вперёд: , размер становится равен трём.
| Шаг | Операция | Индекс по формуле | Ячейка | head | size | tail | Дек от головы к хвосту |
|---|---|---|---|---|---|---|---|
| 1 | push_back 10 | (0 + 0) mod 8 | 0 | 0 | 1 | 0 | 10 |
| 2 | push_back 20 | (0 + 1) mod 8 | 1 | 0 | 2 | 1 | 10, 20 |
| 3 | push_back 30 | (0 + 2) mod 8 | 2 | 0 | 3 | 2 | 10, 20, 30 |
| 4 | push_front 40 | (0 - 1 + 8) mod 8 | 7 | 7 | 4 | 2 | 40, 10, 20, 30 |
| 5 | push_front 50 | (7 - 1 + 8) mod 8 | 6 | 6 | 5 | 2 | 50, 40, 10, 20, 30 |
| 6 | pop_back вернул 30 | (6 + 4) mod 8 | 2 | 6 | 4 | 1 | 50, 40, 10, 20 |
| 7 | pop_front вернул 50 | (6 + 1) mod 8 | 6 | 7 | 3 | 1 | 40, 10, 20 |
Индекс хвоста в последней строке проверяем формулой: . Обход дека читаем как при от 0 до : ячейки 7, 0, 1, то есть 40, 10, 20.
Ответ. , , , содержимое дека от головы к хвосту: 40, 10, 20. Снятые элементы: 30 с хвоста и 50 с головы.
Формула и откуда она берётся
Дек (deque, double ended queue) отличается от очереди тем, что добавлять и удалять можно с обоих концов. Значит, нужны две точки роста, и обе должны двигаться, не задевая элементы посередине. Сдвигать массив после каждой операции нельзя: это на шаг. Остаётся один выход: не двигать данные, а двигать индексы, замкнув массив в кольцо.
Замыкание и даёт остаток от деления. Выражение после индекса возвращает 0, а после нуля возвращает . Массив превращается в кольцо, где у последней ячейки соседом справа оказывается нулевая. Ровно тот же приём лежит в основе кольцевого буфера, только там движение одностороннее.
Пару полей выбирают одну из двух: либо и , либо и . Второй вариант удобнее. Если хранить два индекса, состояния «пусто» и «полностью занято» выглядят одинаково: совпадает с , и отличить их нельзя без дополнительного флага или жертвы одной ячейки. Счётчик снимает двусмысленность сразу: дек пуст при и полон при , а индекс хвоста всегда восстанавливается формулой.
Две проверки границ пишутся до арифметики. Любой push начинается со сравнения : если места нет, массив расширяют или бросают исключение. Любой pop начинается со сравнения : снимать с пустого дека нечего. В нашей задаче ни та, ни другая ветка не сработала, потому что пять элементов в восьми ячейках помещаются свободно.
Дек на двусвязном списке
Второй канонический способ реализации - двусвязный список. Узел хранит значение и два указателя, на предыдущий и на следующий; сама структура держит указатели и на крайние узлы и счётчик длины. Добавление в голову создаёт узел, вешает его перед текущей головой и переставляет ; добавление в хвост делает то же самое с другого конца. Удаление сдвигает соответствующий указатель на соседа и обнуляет ссылку назад.
Все четыре операции здесь честно константные, без всяких оговорок: правок указателей ровно три-четыре, независимо от длины. Плюс ёмкость не ограничена заранее, переполнения не бывает в принципе. Минусы платные: на каждый элемент уходит два указателя (16 лишних байт на 64-битной платформе), узлы разбросаны по куче, и обход дека промахивается мимо кэша процессора на каждом шаге.
Практический вывод такой. Если размер известен или ограничен сверху, а элементы читают часто, выигрывает кольцевой массив: данные лежат подряд, обращение по индексу мгновенное. Если размер скачет непредсказуемо, а по индексу лазить не нужно, проще список. Стандартные библиотеки чаще берут третий, гибридный вариант: список блоков по несколько десятков элементов, где внутри блока работает индексная арифметика, а между блоками - указатели.
Сложность операций и расширение массива
Все четыре операции дека - push_front, push_back, pop_front, pop_back - стоят : несколько сложений, одно взятие остатка и одна запись в память. Доступ к произвольному элементу по логическому номеру тоже константный, , но вставка в середину дека не предусмотрена вовсе - это не список.
Исключение одно: push в полный массив. Тогда заводят новый массив вдвое большей ёмкости и переносят элементы от головы к хвосту, выпрямляя кольцо, - голова нового массива снова встаёт в ноль. Одна такая операция стоит , но происходит она всё реже: после добавлений суммарно скопировано не больше элементов, поэтому амортизированная стоимость push остаётся константной. Тот же приём разобран на примере стека в статье про реализацию стека на списке, а как считать такие оценки в общем случае - в разборе сложности алгоритма.
Дек обобщает сразу две структуры. Если пользоваться только парой push_back и pop_back, получается стек с дисциплиной LIFO. Если брать push_back и pop_front - очередь FIFO. Переключи сценарий в калькуляторе сверху: последовательность операций поменяется, а формулы индексов останутся теми же. Отсюда и типовое применение дека: скользящее окно максимума, обход в ширину с приоритетом 0-1, история навигации вперёд и назад.
Частые ошибки
- Забыли при сдвиге головы влево. Выражение при даёт в C, C++, Java и JavaScript: выход за границу или исключение. Писать нужно .
- Хранят и без счётчика. Тогда пустой и полный дек неразличимы: в обоих случаях указатели совпадают. Либо добавляйте , либо сознательно жертвуйте одной ячейкой, оставляя её всегда свободной.
- Путают порядок действий в pop_back. Сначала уменьшить , потом читать по . При обратном порядке формула обязана быть , и именно здесь чаще всего теряется единица.
- Считают, что освободившаяся ячейка очистилась. После pop старое значение физически остаётся в массиве. Логически ячейка свободна, но в языках со сборкой мусора ссылку стоит обнулить, иначе объект не освободится.
- Расширяют массив копированием «как лежит». Если дек разорван через край, простое побайтовое копирование в новый массив перепутает порядок. Переносить надо по одному элементу через .
- Берут ёмкость, не равную степени двойки, и оптимизируют остаток битовой маской. Замена на работает только при .
FAQ
Чем дек отличается от очереди и стека? Очередь позволяет добавлять только в хвост и снимать только с головы, стек работает одним концом. Дек снимает это ограничение: доступны все четыре операции, поэтому и стек, и очередь получаются из него частным случаем.
Можно ли реализовать дек на односвязном списке? Частично. Три операции из четырёх выйдут константными, а pop_back потребует найти предпоследний узел, то есть пройти весь список за . Именно поэтому берут двусвязный список или кольцевой массив.
Как обойти дек, не разрушая его? Циклом по от 0 до с чтением . Индексы и при этом не меняются, так что дек остаётся в прежнем состоянии.
Какую ёмкость брать на старте? Обычно 8 или 16 ячеек: меньше не имеет смысла из-за накладных расходов на сам объект, а больше - просто память впустую, если дек так и останется коротким. Дальше ёмкость удваивается по мере заполнения.
Коротко
- Дек на массиве хранит два числа: индекс головы и размер ; индекс хвоста считается как .
- push_back пишет в , push_front сдвигает голову по и пишет туда же.
- pop_front читает и двигает голову вперёд, pop_back сначала уменьшает , потом читает .
- В примере после семи операций получилось , , , дек равен 40, 10, 20; снялись 30 с хвоста и 50 с головы.
- Все операции ; альтернатива - двусвязный список, где константность честная, но на каждый элемент уходит два указателя.
Похожие задачи
Как сложить числа в двоичной системе: разбор столбиком
Как сложить двоичные числа столбиком: правило для одного разряда, перенос в старший разряд, трассировка по всем восьми битам, проверка через десятичную систему и переполнение байта.
Программирование/алгоритмыКак вычислить числа Фибоначчи: решение по шагам
Как вычислить числа Фибоначчи: рекуррентная формула, итеративный проход за n минус одно сложение, проверка формулой Бине через золотое сечение и цена наивной рекурсии.
Программирование/алгоритмыКак определить сложность алгоритма: пошаговое решение
Как определить сложность алгоритма по коду: считаем итерации вложенных циклов и цикла с делением пополам, оставляем главный член, проверяем оценку удвоением n и кратко разбираем основную теорему.
Орг./аналит. химияОкисление перманганатом калия: реакции в трёх средах
Как написать окисление перманганатом калия в кислой, нейтральной и щелочной средах: продукты восстановления марганца, метод электронного баланса, расстановка коэффициентов, расчёт титранта.
Химия (физич./структурная)Как найти активность иона: расчёт по Дебаю-Хюккелю
Как найти активность иона в растворе: ионная сила по всем ионам, коэффициент активности по предельному закону Дебая-Хюккеля, произведение f на c, разбор с числами и калькулятор.
ГенетикаКак найти частоту генотипов: закон Харди-Вайнберга
Разбор задачи по популяционной генетике: как найти частоту генотипов по закону Харди-Вайнберга, формула p2 плюс 2pq плюс q2, расчёт числа особей и калькулятор частот.