Files
hello-algo/ru/chapters/chapter_05.md
2026-03-25 16:54:42 +08:00

798 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Стек и очередь
![](ru/docs/assets/media/image142.jpeg){width="3.71873687664042in" height="4.8125in"}
1. **Стек**
> *Стек* -- это линейная структура данных, которая следует логике «первый во- шел -- последний вышел».
>
> Стек можно сравнить со стопкой тарелок на столе: чтобы достать тарелку снизу, нужно сначала убрать все тарелки сверху. Заменив тарелки на элементы различных типов (например, целые числа, символы, объекты и т. д.), мы полу- чим структуру данных, называемую стеком.
>
> Верх стопки элементов называется вершиной стека, а низ -- основанием сте- ка, как показано на рис. 5.1. Операция добавления элемента на вершину стека называется вставка, а удаление элемента с вершины -- извлечение.
![](ru/docs/assets/media/image144.jpeg)Помещение
> Помещение
>
> Извлечение
>
> Извлечение
>
> **Рис. 5.1.** Правило «первый вошел -- последний вышел» для стека
### Основные операции со стеком
> Основные операции со стеком представлены в табл. 5.1, конкретные имена методов зависят от используемого языка программирования. Здесь в качестве примера используются распространенные имена push(), pop(), peek().
>
> **Таблица 5.1.** Эффективность операций со стеком
+-----------+--------------------------------------------------+---------------------------+
| **Метод** | > **Описание** | > **Временная сложность** |
+==========:+==================================================+:=========================:+
| push() | > Вставка элемента (добавление на вершину стека) | > *O*(1) |
+-----------+--------------------------------------------------+---------------------------+
| pop() | > Извлечение элемента с вершины стека | > *O*(1) |
+-----------+--------------------------------------------------+---------------------------+
| peek() | > Доступ к элементу на вершине стека | > *O*(1) |
+-----------+--------------------------------------------------+---------------------------+
> Обычно достаточно использовать классы стека, встроенные в язык програм- мирования. Однако в некоторых языках может не быть специального класса для стека. Тогда можно использовать массив или связный список в качестве стека, игнорируя операции, не связанные со стеком.
>
> \# === File: stack.py ===
>
> \# Инициализация стека.
>
> \# В Python нет встроенного класса стека, можно использовать list. stack: list\[int\] = \[\]
>
> \# Вставка элемента. stack.append(1) stack.append(3) stack.append(2) stack.append(5) stack.append(4)
>
> \# Доступ к элементу на вершине стека. peek: int = stack\[-1\]
>
> \# Извлечение элемента. pop: int = stack.pop()
>
> \# Получение длины стека. size: int = len(stack)
>
> \# Проверка на пустоту.
>
> is_empty: bool = len(stack) == 0
### Реализация стека
> Чтобы глубже понять механизм работы стека, попробуем реализовать соб- ственный класс стека.
>
> Стек следует принципу «первый вошел -- последний вышел», поэтому до- бавление и удаление элементов возможно только на вершине стека. Однако в массивах и связных списках элементы можно добавлять и удалять в любом месте, **поэтому стек можно рассматривать как ограниченный массив или связный список**. Иными словами, можно скрыть часть операций мас- сива или связного списка, чтобы их внешняя логика соответствовала харак- теристикам стека.
##### Реализация на основе связного списка
> При использовании для реализации стека связного списка можно считать головной узел связного списка вершиной стека, а хвостовой узел -- основа- нием стека.
>
> Для операции вставки элемента достаточно вставить его в начало связного списка, как показано на рис. 5.2. Этот метод вставки узла называется встав- ка в голову. Для операции извлечения элемента достаточно удалить головной узел из связного списка.
>
> ![](ru/docs/assets/media/image146.jpeg)
![](ru/docs/assets/media/image148.jpeg)![](ru/docs/assets/media/image150.jpeg)
> **Рис. 5.2.** Операции вставки и извлечения в стеке на основе связного списка
>
> Ниже приведен пример кода для реализации стека на основе связного списка.
>
> \# === File: linkedlist_stack.py === class LinkedListStack:
>
> \"\"\"Стек на основе связного списка.\"\"\"
>
> def init (self): \"\"\"Конструктор.\"\"\"
>
> self.\_peek: ListNode \| None = None self.\_size: int = 0
>
> def size(self) -\> int: \"\"\"Получение длины стека.\"\"\" return self.\_size
>
> def is_empty(self) -\> bool: \"\"\"Проверка стека на пустоту.\"\"\" return self.\_size == 0
>
> def push(self, val: int): \"\"\"Вставка элемента.\"\"\" node = ListNode(val) node.next = self.\_peek
>
> self.\_peek = node self.\_size += 1
>
> def pop(self) -\> int: \"\"\"Извлечение элемента.\"\"\" num = self.peek()
>
> self.\_peek = self.\_peek.next self.\_size -= 1
>
> return num
>
> def peek(self) -\> int:
>
> \"\"\"Доступ к элементу на вершине стека.\"\"\" if self.is_empty():
>
> raise IndexError(\"Стек пуст\") return self.\_peek.val
>
> def to_list(self) -\> list\[int\]: \"\"\"Преобразование в список для печати.\"\"\" arr = \[\]
>
> node = self.\_peek
>
> while node:
>
> arr.append(node.val) node = node.next
>
> arr.reverse() return arr
##### Реализация на основе массива
> При использовании для реализации стека массива можно считать конец мас- сива вершиной стека. Операции вставки и извлечения соответствуют добавле-
>
> нию и удалению элементов в конце массива, как показано на рис. 5.3. Времен- ная сложность этих операций составляет *O*(1).
![](ru/docs/assets/media/image152.jpeg)![](ru/docs/assets/media/image154.jpeg)![](ru/docs/assets/media/image156.jpeg)
> **Рис. 5.3.** Операции вставки и извлечения в стеке на основе массива
>
> Поскольку количество вставляемых элементов может постоянно увеличи- ваться, можно использовать динамический массив, чтобы не заниматься рас- ширением массива самостоятельно. Ниже приведен пример кода.
>
> \# === File: array_stack.py ===
>
> class ArrayStack:
>
> \"\"\"Стек на основе массива.\"\"\"
>
> def init (self): \"\"\"Конструктор.\"\"\" self.\_stack: list\[int\] = \[\]
>
> def size(self) -\> int: \"\"\"Получение длины стека.\"\"\" return len(self.\_stack)
>
> def is_empty(self) -\> bool: \"\"\"Проверка стека на пустоту.\"\"\" return self.size() == 0
>
> def push(self, item: int): \"\"\"Вставка элемента.\"\"\" self.\_stack.append(item)
>
> def pop(self) -\> int: \"\"\"Извлечение элемента.\"\"\" if self.is_empty():
>
> raise IndexError(\"Стек пуст\") return self.\_stack.pop()
>
> def peek(self) -\> int:
>
> \"\"\"Доступ к элементу на вершине стека.\"\"\" if self.is_empty():
>
> raise IndexError(\"Стек пуст\") return self.\_stack\[-1\]
>
> def to_list(self) -\> list\[int\]: \"\"\"Возврат списка для печати.\"\"\" return self.\_stack
### Сравнение двух реализаций
##### Поддерживаемые операции
> Обе реализации поддерживают все операции, определенные для стека. Реали- зация на основе массива дополнительно поддерживает произвольный доступ, но это выходит за рамки определения стека, поэтому обычно не используется.
##### Временная сложность
> В реализации на основе массива операции добавления и удаления элемента выполняются в заранее выделенной непрерывной памяти, что обеспечивает хорошую локальность кеша и, следовательно, высокую эффективность. Одна- ко, если при добавлении элемента превышается емкость массива, срабатыва- ет механизм расширения, что приводит к увеличению временной сложности данной операции до *O*(*n*).
>
> В реализации на основе связного списка расширение происходит очень гиб- ко, и не возникает проблемы снижения эффективности, как в случае расшире- ния массива. Однако операция добавления элемента требует инициализации объекта узла и изменения указателя, что делает ее относительно менее эффек- тивной. Тем не менее, если добавляемый элемент уже является объектом узла, можно избежать шага инициализации, что повысит эффективность.
>
> Таким образом, если элементы операций добавления и удаления являются примитивными типами данных, такими как int или double, можно сделать сле- дующие выводы:
1) стек, реализованный на основе массива, при срабатывании механизма расширения теряет в эффективности, но, так как расширение является редкой операцией, средняя эффективность выше;
2) стек, реализованный на основе связного списка, обеспечивает более ста- бильную эффективность.
##### Пространственная сложность
> При инициализации массива система выделяет для него начальную емкость, которая может превышать фактические потребности. Кроме того, механизм расширения обычно осуществляется с определенным коэффициентом (на- пример, в 2 раза), и емкость после расширения также может превышать фак- тические потребности. Поэтому **стек**, **реализованный на основе массива**, **может приводить к некоторым потерям пространства**.
>
> Однако, так как узлы связного списка требуют дополнительного хранения указателей, **занимаемое ими пространство сравнительно больше**.
>
> Таким образом, нельзя однозначно определить, какая реализация более эко- номична в плане памяти, необходимо анализировать конкретные ситуации.
### Типичные сценарии применения стека
- Возврат и переход вперед в браузере, отмена и повтор в программном обеспечении. Каждый раз, когда открывается новая веб-страница, бра- узер выполняет добавление предыдущей страницы в стек, что позволя- ет вернуться к ней с помощью операции возврата. Операция возврата фактически является выполнением удаления из стека. Если требуется поддержка как возврата, так и перехода вперед, необходимо использо- вать два стека.
- Управление памятью программы. Каждый раз при вызове функции си- стема добавляет на вершину стека фрейм для записи контекстной ин- формации функции. В рекурсивных функциях на этапе нисходящей ре-
> курсии постоянно выполняется добавление в стек, а на этапе восходящей рекурсии -- удаление из стека.
#### Очередь
> *Очередь* -- это линейная структура данных, следующая правилу «первый при- шел -- первый вышел». Как следует из названия, очередь моделирует реаль- ную очереди, когда новые элементы постоянно добавляются в конец очереди, а элементы в начале очереди покидают ее последовательно.
>
> Начало очереди называется голова, а конец -- хвост, см. рис. 5.4. Операция добавления элемента в конец очереди называется добавление в очередь, а уда- ление элемента из начала очереди -- удаление из очереди.
![](ru/docs/assets/media/image158.jpeg)
> **Рис. 5.4.** Правило очереди «первый пришел -- первый вышел»
### Основные операции с очередью
> Основные операции с очередью представлены в табл. 5.2. Следует отметить, что имена методов могут различаться в зависимости от языка программиро- вания. Здесь используются те же названия методов, что и для стека.
+-------------+---------------------------------------------------+---------------------------+
| > **Метод** | > **Описание** | > **Временная сложность** |
+=============+===================================================+===========================+
| > push() | > Добавление элемента в очередь, т. е. добавление | > *O*(1) |
+-------------+---------------------------------------------------+---------------------------+
| > pop() | > Удаление элемента из головы очереди | > *O*(1) |
+-------------+---------------------------------------------------+---------------------------+
| > peek() | > Доступ к элементу в голове очереди | > *O*(1) |
+-------------+---------------------------------------------------+---------------------------+
> элемента в конец очереди
>
> Можно использовать готовый класс очереди в языке программирования.
>
> \# === File: queue.py ===
>
> from collections import deque \# Инициализация очереди-
>
> \# В Python обычно используется класс двусторонней очереди deque.
>
> \# Хотя queue.Queue() является полноценным классом очереди, он не очень удобен, поэтому не рекомендуется к использованию.
>
> que: deque\[int\] = deque()
>
> \# Добавление элемента в очередь. que.append(1)
>
> que.append(3) que.append(2) que.append(5) que.append(4)
>
> \# Доступ к элементу в голове очереди. front: int = que\[0\]
>
> \# Удаление элемента из очереди. pop: int = que.popleft()
>
> \# Получение длины очереди. size: int = len(que)
>
> \# Проверка очереди на пустоту. is_empty: bool = len(que) == 0
### Реализация очереди
> Для реализации очереди требуется структура данных, которая позволяет до- бавлять элементы с одного конца и удалять с другого конца. И связный список, и массив соответствуют этим требованиям.
##### Реализация на основе связного списка
> Можно рассматривать головной узел и хвостовой узел связного списка как на- чало очереди и конец очереди соответственно. А также установить правило, что добавление узлов возможно только в конец очереди, а удаление узлов -- только из начала очереди, как показано на рис. 5.5.
>
> ![](ru/docs/assets/media/image160.jpeg)
![](ru/docs/assets/media/image162.jpeg)![](ru/docs/assets/media/image164.jpeg)
> **Рис. 5.5.** Операции добавления и удаления в очереди, реализованной на основе связного списка
>
> Ниже приведен код реализации очереди с использованием связного списка.
>
> \# === File: linkedlist_queue.py === class LinkedListQueue:
>
> \"\"\"Очередь на основе связного списка.\"\"\"
>
> def init (self): \"\"\"Конструктор.\"\"\"
>
> self.\_front: ListNode \| None = None \# Головной узел front. self.\_rear: ListNode \| None = None \# Хвостовой узел rear. self.\_size: int = 0
>
> def size(self) -\> int: \"\"\"Получение длины очереди.\"\"\" return self.\_size
>
> def is_empty(self) -\> bool: \"\"\"Проверка очереди на пустоту.\"\"\" return self.\_size == 0
>
> def push(self, num: int): \"\"\"Добавление в очередь.\"\"\"
>
> \# Добавление num после хвостового узла. node = ListNode(num)
>
> \# Если очередь пуста, то головной и хвостовой узлы указывают на этот
>
> узел.
>
> if self.\_front is None: self.\_front = node self.\_rear = node
>
> \# Если очередь не пуста, то узел добавляется после хвостового узла.
>
> else:
>
> self.\_rear.next = node self.\_rear = node
>
> self.\_size += 1
>
> def pop(self) -\> int: \"\"\"Удаление из очереди.\"\"\" num = self.peek()
>
> \# Удаление головного узла. self.\_front = self.\_front.next self.\_size -= 1
>
> return num
>
> def peek(self) -\> int:
>
> \"\"\"Доступ к элементу в начале очереди.\"\"\" if self.is_empty():
>
> raise IndexError(\"Очередь пуста\") return self.\_front.val
>
> def to_list(self) -\> list\[int\]: \"\"\"Преобразование в список для печати.\"\"\" queue = \[\]
>
> temp = self.\_front while temp:
>
> queue.append(temp.val) temp = temp.next
>
> return queue
##### Реализация на основе массива
> Удаление первого элемента в массиве имеет временную сложность *O*(*n*), что снижает эффективность операции удаления из очереди. Однако можно ис- пользовать следующий изящный метод, чтобы избежать этой проблемы.
>
> Можно использовать переменную front для указания на индекс первого эле- мента очереди и поддерживать переменную size для записи длины очереди. Определим переменную rear = front + size. Тогда rear будет указывать на сле- дующий элемент после хвоста очереди.
>
> В этой схеме **эффективный диапазон элементов в массиве составляет**
\[front, rear - 1\]. Методы реализации различных операций показаны на рис. 5.6.
- **Добавление в очередь**: присвоение нового элемента индексу rear и уве- личение size на 1.
- **Удаление из очереди**: достаточно увеличить front на 1 и уменьшить size
> на 1.
>
> Можно заметить, что добавление в очередь и удаление из нее требуют толь- ко двух операций, временная сложность каждой из которых равна *O*(1).
![](ru/docs/assets/media/image166.jpeg)
> **Рис. 5.6.** Операции добавления и удаления в очереди, реализованной на основе массива
>
> ![](ru/docs/assets/media/image168.jpeg)
![](ru/docs/assets/media/image170.jpeg)
> **Рис. 5.6.** *Окончание*
>
> Может возникнуть трудность: в процессе постоянного добавления и удале- ния положения front и rear перемещаются вправо, и **когда они достигают конца массива**, **дальнейшее перемещение становится невозможным**. Чтобы решить эту проблему, можно рассматривать массив как кольцевой мас- сив с соединенными концами.
>
> Для кольцевого массива необходимо, чтобы front или rear, пересекая конец массива, возвращались к его началу для продолжения обхода. Этот цикличе- ский процесс можно реализовать с помощью операции взятия остатка, при- мер кода приведен ниже.
>
> \# === File: array_queue.py ===
>
> class ArrayQueue:
>
> \"\"\"Очередь на основе кольцевого массива.\"\"\"
>
> def init (self, size: int): \"\"\"Конструктор.\"\"\"
>
> self.\_nums: list\[int\] = \[0\] \* size \# Массив для хранения
>
> \# элементов очереди. self.\_front: int = 0 \# Указатель на начало очереди,
>
> \# указывает на первый элемент. self.\_size: int = 0 \# Длина очереди.
>
> def capacity(self) -\> int: \"\"\"Получение емкости очереди.\"\"\" return len(self.\_nums)
>
> def size(self) -\> int: \"\"\"Получение длины очереди.\"\"\" return self.\_size
>
> def is_empty(self) -\> bool: \"\"\"Проверка, пуста ли очередь.\"\"\" return self.\_size == 0
>
> def push(self, num: int): \"\"\"Добавление в очередь.\"\"\"
>
> if self.\_size == self.capacity(): raise IndexError(\"Очередь полна\")
>
> \# Вычисление указателя на конец очереди, указывает на индекс конца + 1. \# Реализация возврата rear к началу массива после пересечения конца
>
> \# с помощью операции взятия остатка.
>
> rear: int = (self.\_front + self.\_size) % self.capacity() \# Добавление num в конец очереди.
>
> self.\_nums\[rear\] = num self.\_size += 1
>
> def pop(self) -\> int: \"\"\"Удаление из очереди.\"\"\" num: int = self.peek()
>
> \# Указатель на начало очереди перемещается на одну позицию вперед,
>
> \# если пересекает конец, возвращается к началу массива. self.\_front = (self.\_front + 1) % self.capacity() self.\_size -= 1
>
> return num
>
> def peek(self) -\> int:
>
> \"\"\"Доступ к элементу в начале очереди.\"\"\" if self.is_empty():
>
> raise IndexError(\"Очередь пуста\") return self.\_nums\[self.\_front\]
>
> def to_list(self) -\> list\[int\]: \"\"\"Возврат списка для печати.\"\"\" res = \[0\] \* self.size()
>
> j: int = self.\_front
>
> for i in range(self.size()):
>
> res\[i\] = self.\_nums\[(j % self.capacity())\] j += 1
>
> return res
>
> Реализованная выше очередь все еще имеет ограничение: ее длина неиз- менна. Однако эту проблему несложно решить, заменить массив на динамиче- ский с помощью механизма расширения. Заинтересованные читатели могут попробовать реализовать это самостоятельно.
>
> Выводы о сравнении двух реализаций аналогичны выводам о стеке, поэто- му здесь мы не будем повторяться.
### Типичные сценарии применения очереди
1. Заказы на маркетплейсах. После оформления заказа покупателем он до- бавляется в очередь, и система затем обрабатывает заказы в порядке их поступления. В период распродаж за короткое время создается огром- ное количество заказов, и высокая нагрузка становится проблемой для разработчиков программного обеспечения.
2. Различные списки задач. Любая ситуация, требующая реализации прин- ципа «первым пришел -- первым обслужен». Например, очередь заданий в принтере, очередь заказов в ресторане и т. д. Очередь в этих ситуациях эффективно поддерживает порядок обработки.
#### двусторонняя очередь
> В обычной очереди можно удалять только элементы из начала и добавлять элементы только в конец. *Двусторонняя очередь* предоставляет большую гиб- кость, позволяя выполнять операции добавления или удаления элементов как в начале, так и в конце, см. рис. 5.7.
>
> **Двусторонняя очередь**
>
> **(Deque)**
>
> **Голова очереди**
>
> Добавление элемента в голову
>
> ![](ru/docs/assets/media/image172.jpeg)push_first(**1**)
>
> Извлечение элемента с головы
>
> pop_first()
>
> **Извлечение из очереди**
>
> **Добавление в очередь**
>
> **Добавление в очередь**
>
> **Извлечение из очереди**
>
> **Хвост очереди**
Добавление Извлечение элемента элемента
> push_last(**4**)
>
> с хвоста
>
> pop_last()
>
> **Рис. 5.7.** Операции в двусторонней очереди
### Основные операции с двусторонней очередью
Обычные операции с двусторонней очередью представлены в табл. 5.3, кон- кретные имена методов зависят от используемого языка программирования.
> **Таблица 5.3.** Эффективность операций с двусторонней очередью
+----------------+----------------------------------------+---------------------------+
| > **Метод** | > **Описание** | > **Временная сложность** |
+================+========================================+:=========================:+
| > push_first() | > Добавление элемента в начало очереди | > *O*(1) |
+----------------+----------------------------------------+---------------------------+
| > push_last() | > Добавление элемента в конец очереди | > *O*(1) |
+----------------+----------------------------------------+---------------------------+
| > pop_first() | > Удаление элемента из начала очереди | > *O*(1) |
+----------------+----------------------------------------+---------------------------+
| > pop_last() | > Удаление элемента из конца очереди | > *O*(1) |
+----------------+----------------------------------------+---------------------------+
| > peek_first() | > Доступ к элементу в начале очереди | > *O*(1) |
+----------------+----------------------------------------+---------------------------+
| > peek_last() | > Доступ к элементу в конце очереди | > *O*(1) |
+----------------+----------------------------------------+---------------------------+
> Аналогично обычной очереди можно использовать уже реализованный в языке программирования класс двусторонней очереди.
>
> \# === File: deque.py ===
>
> from collections import deque
>
> \# Инициализация двусторонней очереди. deq: deque\[int\] = deque()
>
> \# Добавление элементов в очередь. deq.append(2) \# Добавление в конец. deq.append(5)
>
> deq.append(4)
>
> deq.appendleft(3) \# Добавление в начало. deq.appendleft(1)
>
> \# Доступ к элементам.
>
> front: int = deq\[0\] \# Элемент в начале очереди. rear: int = deq\[-1\] \# Элемент в конце очереди.
>
> \# Удаление элементов из очереди.
>
> pop_front: int = deq.popleft() \# Удаление из начала очереди. pop_rear: int = deq.pop() \# Удаление из конца очереди.
>
> \# Получение длины двусторонней очереди. size: int = len(deq)
>
> \# Проверка на пустоту двусторонней очереди is_empty: bool = len(deq) == 0
### Реализация двусторонней очереди\*
> Реализация двусторонней очереди схожа с обычной очередью -- можно вы- брать в качестве базовой структуры данных связный список или массив.
##### Реализация на основе двусвязного списка
> В предыдущем разделе для реализации очереди использовался обычный од- носвязный список, так как он позволяет удобно удалять головной узел (соот- ветствует операции удаления из очереди) и добавлять новый узел после хво- стового узла (соответствует операции добавления в очередь).
>
> Для двусторонней очереди операции добавления и удаления можно выпол- нять как в начале, так и в конце. Иными словами, двусторонняя очередь требу- ет реализации операций в симметричном направлении. Для этого в качестве базовой структуры данных двусторонней очереди удобно использовать дву- связный список.
>
> Головной и хвостовой узлы двусвязного списка рассматриваются как начало и конец двусторонней очереди. При этом реализуется возможность добавле- ния и удаления узлов с обеих сторон, см. рис. 5.8.
>
> ![](ru/docs/assets/media/image174.jpeg)
![](ru/docs/assets/media/image176.jpeg)![](ru/docs/assets/media/image178.jpeg)
> **Рис. 5.8.** Операции добавления и удаления в двусторонней очереди на основе связного списка
>
> ![](ru/docs/assets/media/image180.jpeg)
![](ru/docs/assets/media/image182.jpeg)
> **Рис. 5.8.** *Окончание*
>
> Ниже представлен код реализации.
>
> \# === File: linkedlist_deque.py === class ListNode:
>
> \"\"\" Узел двусвязного списка.\"\"\"
>
> def init (self, val: int): \"\"\" Конструктор.\"\"\" self.val: int = val
>
> self.next: ListNode \| None = None \# Ссылка на следующий узел. self.prev: ListNode \| None = None \# Ссылка на предыдущий узел.
>
> class LinkedListDeque:
>
> \"\"\" Двусторонняя очередь на основе двусвязного списка.\"\"\"
>
> def init (self):
>
> \"\"\" Конструктор.\"\"\"
>
> self.\_front: ListNode \| None = None \# Головной узел front. self.\_rear: ListNode \| None = None \# Хвостовой узел rear. self.\_size: int = 0 \# Длина двусторонней очереди.
>
> def size(self) -\> int:
>
> \"\"\" Получение длины двусторонней очереди.\"\"\" return self.\_size
>
> def is_empty(self) -\> bool:
>
> \"\"\" Проверка на пустоту двусторонней очереди.\"\"\" return self.\_size == 0
>
> def push(self, num: int, is_front: bool): \"\"\" Операция добавления в очередь.\"\"\" node = ListNode(num)
>
> \# Если список пуст, front и rear указывают на node. if self.is_empty():
>
> self.\_front = self.\_rear = node \# Добавление в начало очереди.
>
> elif is_front:
>
> \# Добавление node в начало списка. self.\_front.prev = node
>
> node.next = self.\_front
>
> self.\_front = node \# Обновление головного узла. \# Добавление в конец очереди.
>
> else:
>
> \# Добавление node в конец списка. self.\_rear.next = node
>
> node.prev = self.\_rear
>
> self.\_rear = node \# Обновление хвостового узла. self.\_size += 1 \# Обновление длины очереди.
>
> def push_first(self, num: int):
>
> \"\"\" Добавление в начало очереди.\"\"\" self.push(num, True)
>
> def push_last(self, num: int):
>
> \"\"\" Добавление в конец очереди.\"\"\" self.push(num, False)
>
> def pop(self, is_front: bool) -\> int: \"\"\" Операция удаления из очереди.\"\"\" if self.is_empty():
>
> raise IndexError(\" Двусторонняя очередь пуста.\") \# Удаление из начала очереди.
>
> if is_front:
>
> val: int = self.\_front.val \# Временное сохранение значения
>
> \# головного узла.
>
> \# Удаление головного узла.
>
> fnext: ListNode \| None = self.\_front.next if fnext != None:
>
> fnext.prev = None self.\_front.next = None
>
> self.\_front = fnext \# Обновление головного узла. \# Удаление из конца очереди.
>
> else:
>
> val: int = self.\_rear.val \# Временное сохранение значения
>
> \# хвостового узла. \# Удаление хвостового узла.
>
> rprev: ListNode \| None = self.\_rear.prev if rprev != None:
>
> rprev.next = None self.\_rear.prev = None
>
> self.\_rear = rprev \# Обновление хвостового узла. self.\_size -= 1 \# Обновление длины очереди.
>
> return val
>
> def pop_first(self) -\> int:
>
> \"\"\" Удаление из начала очереди.\"\"\" return self.pop(True)
>
> def pop_last(self) -\> int:
>
> \"\"\" Удаление из конца очереди.\"\"\" return self.pop(False)
>
> def peek_first(self) -\> int:
>
> \"\"\" Доступ к элементу в начале очереди.\"\"\" if self.is_empty():
>
> raise IndexError(\" Двусторонняя очередь пуста.\") return self.\_front.val
>
> def peek_last(self) -\> int:
>
> \"\"\" Доступ к элементу в конце очереди.\"\"\" if self.is_empty():
>
> raise IndexError(\" Двусторонняя очередь пуста.\") return self.\_rear.val
>
> def to_array(self) -\> list\[int\]:
>
> \"\"\" Возврат массива для печати.\"\"\" node = self.\_front
>
> res = \[0\] \* self.size()
>
> for i in range(self.size()): res\[i\] = node.val
>
> node = node.next return res
##### Реализация на основе массива
> Аналогично реализации обычной очереди для двусторонней очереди можно использовать кольцевой массив, как показано на рис. 5.9.
![](ru/docs/assets/media/image184.jpeg)![](ru/docs/assets/media/image186.jpeg)
> **Рис. 5.9.** Операции добавления и удаления в двусторонней очереди на основе массива
>
> ![](ru/docs/assets/media/image188.jpeg)
![](ru/docs/assets/media/image190.jpeg)
> **Рис. 5.9.** *Продолжение*
>
> ![](ru/docs/assets/media/image192.jpeg)
>
> **Рис. 5.9.** *Окончание*
>
> По сравнению с реализацией обычной очереди необходимо лишь добавить методы для добавления в начало очереди и для удаления из конца очереди.
>
> \# === File: array_deque.py === class ArrayDeque:
>
> \"\"\" Двусторонняя очередь на основе кольцевого массива.\"\"\"
>
> def init (self, capacity: int): \"\"\" Конструктор.\"\"\"
>
> self.\_nums: list\[int\] = \[0\] \* capacity self.\_front: int = 0
>
> self.\_size: int = 0
>
> def capacity(self) -\> int:
>
> \"\"\" Получение емкости двусторонней очереди.\"\"\" return len(self.\_nums)
>
> def size(self) -\> int:
>
> \"\"\" Получение длины двусторонней очереди.\"\"\" return self.\_size
>
> def is_empty(self) -\> bool:
>
> \"\"\" Проверка, пуста ли двусторонняя очередь.\"\"\" return self.\_size == 0
>
> def index(self, i: int) -\> int:
>
> \"\"\" Вычисление индекса кольцевого массива.\"\"\"
>
> \# Реализация соединения начала и конца массива с помощью \# операции взятия остатка.
>
> \# Когда i превышает конец массива, возвращается к началу. \# Когда i превышает начало массива, возвращается к концу. return (i + self.capacity()) % self.capacity()
>
> def push_first(self, num: int):
>
> \"\"\" Добавление в начало очереди.\"\"\" if self.\_size == self.capacity():
>
> print(\" Двусторонняя очередь полна.\") return
>
> \# Перемещение указателя начала очереди на одну позицию влево.
>
> \# Реализация возврата front к концу массива после превышения начала. self.\_front = self.index(self.\_front - 1)
>
> \# Добавление num в начало очереди. self.\_nums\[self.\_front\] = num self.\_size += 1
>
> def push_last(self, num: int):
>
> \"\"\" Добавление в конец очереди.\"\"\" if self.\_size == self.capacity():
>
> print(\" Двусторонняя очередь полна.\") return
>
> \# Вычисление указателя конца очереди, указывает на индекс конца + 1. rear = self.index(self.\_front + self.\_size)
>
> \# Добавление num в конец очереди. self.\_nums\[rear\] = num
>
> self.\_size += 1
>
> def pop_first(self) -\> int:
>
> \"\"\" Удаление из начала очереди.\"\"\" num = self.peek_first()
>
> \# Перемещение указателя начала очереди на одну позицию вправо. self.\_front = self.index(self.\_front + 1)
>
> self.\_size -= 1 return num
>
> def pop_last(self) -\> int:
>
> \"\"\" Удаление из конца очереди.\"\"\" num = self.peek_last()
>
> self.\_size -= 1 return num
>
> def peek_first(self) -\> int:
>
> \"\"\" Доступ к элементу в начале очереди.\"\"\"
>
> if self.is_empty():
>
> raise IndexError(\" Двусторонняя очередь пуста.\") return self.\_nums\[self.\_front\]
>
> def peek_last(self) -\> int:
>
> \"\"\" Доступ к элементу в конце очереди.\"\"\" if self.is_empty():
>
> raise IndexError(\" Двусторонняя очередь пуста.\") \# Вычисление индекса последнего элемента.
>
> last = self.index(self.\_front + self.\_size - 1) return self.\_nums\[last\]
>
> def to_array(self) -\> list\[int\]:
>
> \"\"\" Возврат массива для печати.\"\"\"
>
> \# Преобразование только элементов в пределах действительной длины. res = \[\]
>
> for i in range(self.\_size): res.append(self.\_nums\[self.index(self.\_front + i)\])
>
> return res
### Сценарии применения двусторонней очереди
> Двусторонняя очередь сочетает в себе логику стека и очереди. **Поэтому она применима для всех сценариев этих двух структур**, **одновременно пре- доставляя большую степень свободы**.
>
> Известно, что функция отмены в программном обеспечении обычно реа- лизуется с помощью стека: система помещает каждое изменение в стек с по- мощью операции push, а затем выполняет отмену с помощью операции pop. Однако, учитывая ограничения системных ресурсов, программное обеспече- ние обычно ограничивает количество шагов отмены (например, позволяет со- хранить только 50 шагов). Когда длина стека превышает 50, программе нужно выполнить удаление внизу стека (в начале очереди). **Но стек не может реа- лизовать эту функцию**, **и в этом случае необходимо использовать дву- стороннюю очередь вместо стека**. Следует отметить, что основная логика отмены по-прежнему следует принципу стека «первым пришел -- последним вышел», просто двусторонняя очередь позволяет более гибко реализовать не- которые дополнительные логические операции.
#### резюме
##### Ключевые моменты
- Стек -- это структура данных, которая следует принципу «первым при- шел -- последним вышел» и может быть реализована с помощью массива или связного списка.
- В плане временной сложности реализация стека с использованием мас- сива обладает более высокой средней эффективностью, но во время рас- ширения сложность времени выполнения одной операции добавления
> 5.4. Резюме ❖ **145**
>
> [в]{.smallcaps} стек может ухудшиться до *O*(*n*). В сравнении с этим реализация стека с использованием связного списка обладает более стабильной эффек- тивностью.
- В плане пространственной сложности реализация стека с использовани- ем массива может привести к определенной степени потери простран- ства. Однако следует отметить, что память, занимаемая узлами связного списка, больше, чем у элементов массива.
- Очередь -- это структура данных, которая следует принципу «первым пришел -- первым вышел» и также может быть реализована с помощью массива или связного списка. В плане временной и пространственной сложности выводы по очереди схожи с выводами по стеку.
- Двусторонняя очередь -- это очередь с большей степенью свободы, она позволяет добавлять и удалять элементы с обоих концов.
##### Вопросы и ответы
> **Вопрос**. Реализована ли функция «вперед-назад» в браузере с помощью дву- стороннего связного списка?
>
> **Ответ**. Функция «вперед-назад» в браузере, по сути, является типичным проявлением стека. Когда пользователь посещает новую страницу, она добав- ляется на вершину стека. Когда пользователь нажимает кнопку **Назад**, страни- ца извлекается с вершины стека. Использование двусторонней очереди позво- ляет удобно реализовать некоторые дополнительные операции, что упомина- ется в разделе «Двусторонняя очередь».
>
> **Вопрос**. Нужно ли освобождать память узла после извлечения из стека?
>
> **Ответ**. Если в дальнейшем необходимо использовать извлеченный узел, то освобождать память не нужно. Если узел больше не нужен, в языках Java и Python имеется автоматический механизм сборки мусора, поэтому ручное освобожде- ние памяти не требуется. В C и C++ необходимо освобождать память вручную.
>
> **Вопрос**. Двусторонняя очередь похожа на два стека, соединенных вместе.
>
> Каково ее назначение?
>
> **Ответ**. Двусторонняя очередь подобна комбинации стека и очереди или двум стекам, соединенным вместе. Она представляет собой логику стека и очереди, поэтому подходит для всех сценариев их применения, но является более гибкой. **Вопрос**. Как конкретно реализуются функции отмены и повтора операций?
>
> **Ответ**. Используются два стека: стек A для отмены, стек B для повтора.
1. Когда пользователь выполняет операцию, она помещается в стек A, а стек
> B очищается.
2. Когда пользователь выполняет отмену, из стека A извлекается последняя операция и помещается в стек B.
3. Когда пользователь выполняет повтор, из стека B извлекается последняя операция и помещается в стек A.
> Глава 6