51 KiB
Стек и очередь
{width="3.71873687664042in" height="4.8125in"}
- Стек
Стек -- это линейная структура данных, которая следует логике «первый во- шел -- последний вышел».
Стек можно сравнить со стопкой тарелок на столе: чтобы достать тарелку снизу, нужно сначала убрать все тарелки сверху. Заменив тарелки на элементы различных типов (например, целые числа, символы, объекты и т. д.), мы полу- чим структуру данных, называемую стеком.
Верх стопки элементов называется вершиной стека, а низ -- основанием сте- ка, как показано на рис. 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, можно сделать сле- дующие выводы:
-
стек, реализованный на основе массива, при срабатывании механизма расширения теряет в эффективности, но, так как расширение является редкой операцией, средняя эффективность выше;
-
стек, реализованный на основе связного списка, обеспечивает более ста- бильную эффективность.
Пространственная сложность
При инициализации массива система выделяет для него начальную емкость, которая может превышать фактические потребности. Кроме того, механизм расширения обычно осуществляется с определенным коэффициентом (на- пример, в 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
Реализованная выше очередь все еще имеет ограничение: ее длина неиз- менна. Однако эту проблему несложно решить, заменить массив на динамиче- ский с помощью механизма расширения. Заинтересованные читатели могут попробовать реализовать это самостоятельно.
Выводы о сравнении двух реализаций аналогичны выводам о стеке, поэто- му здесь мы не будем повторяться.
Типичные сценарии применения очереди
-
Заказы на маркетплейсах. После оформления заказа покупателем он до- бавляется в очередь, и система затем обрабатывает заказы в порядке их поступления. В период распродаж за короткое время создается огром- ное количество заказов, и высокая нагрузка становится проблемой для разработчиков программного обеспечения.
-
Различные списки задач. Любая ситуация, требующая реализации прин- ципа «первым пришел -- первым обслужен». Например, очередь заданий в принтере, очередь заказов в ресторане и т. д. Очередь в этих ситуациях эффективно поддерживает порядок обработки.
двусторонняя очередь
В обычной очереди можно удалять только элементы из начала и добавлять элементы только в конец. Двусторонняя очередь предоставляет большую гиб- кость, позволяя выполнять операции добавления или удаления элементов как в начале, так и в конце, см. рис. 5.7.
Двусторонняя очередь
(Deque)
Голова очереди
Добавление элемента в голову
Извлечение элемента с головы
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 для повтора.
- Когда пользователь выполняет операцию, она помещается в стек A, а стек
B очищается.
-
Когда пользователь выполняет отмену, из стека A извлекается последняя операция и помещается в стек B.
-
Когда пользователь выполняет повтор, из стека B извлекается последняя операция и помещается в стек A.
Глава 6
























