mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-04 23:22:27 +08:00
798 lines
51 KiB
Markdown
798 lines
51 KiB
Markdown
# Стек и очередь
|
||
|
||
{width="3.71873687664042in" height="4.8125in"}
|
||
|
||
1. **Стек**
|
||
|
||
> *Стек* -- это линейная структура данных, которая следует логике «первый во- шел -- последний вышел».
|
||
>
|
||
> Стек можно сравнить со стопкой тарелок на столе: чтобы достать тарелку снизу, нужно сначала убрать все тарелки сверху. Заменив тарелки на элементы различных типов (например, целые числа, символы, объекты и т. д.), мы полу- чим структуру данных, называемую стеком.
|
||
>
|
||
> Верх стопки элементов называется вершиной стека, а низ -- основанием сте- ка, как показано на рис. 5.1. Операция добавления элемента на вершину стека называется вставка, а удаление элемента с вершины -- извлечение.
|
||
|
||
Помещение
|
||
|
||
> Помещение
|
||
>
|
||
> Извлечение
|
||
>
|
||
> Извлечение
|
||
>
|
||
> **Рис. 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. Этот метод вставки узла называется встав- ка в голову. Для операции извлечения элемента достаточно удалить головной узел из связного списка.
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 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).
|
||
|
||

|
||
|
||
> **Рис. 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. Операция добавления элемента в конец очереди называется добавление в очередь, а уда- ление элемента из начала очереди -- удаление из очереди.
|
||
|
||

|
||
|
||
> **Рис. 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.
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 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).
|
||
|
||

|
||
|
||
> **Рис. 5.6.** Операции добавления и удаления в очереди, реализованной на основе массива
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 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)**
|
||
>
|
||
> **Голова очереди**
|
||
>
|
||
> Добавление элемента в голову
|
||
>
|
||
> 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.
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 5.8.** Операции добавления и удаления в двусторонней очереди на основе связного списка
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 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.
|
||
|
||

|
||
|
||
> **Рис. 5.9.** Операции добавления и удаления в двусторонней очереди на основе массива
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 5.9.** *Продолжение*
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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
|