mirror of
https://github.com/krahets/hello-algo.git
synced 2026-06-30 01:24:21 +00:00
430 lines
20 KiB
Markdown
430 lines
20 KiB
Markdown
# Стек
|
|
|
|
<u>Стек (stack)</u> -- это линейная структура данных, которая следует логике «первый вошел -- последний вышел».
|
|
|
|
Стек можно сравнить со стопкой тарелок на столе: чтобы достать тарелку снизу, нужно сначала убрать все тарелки сверху. Заменив тарелки на элементы различных типов (например, целые числа, символы, объекты и т. д.), мы получим структуру данных, называемую стеком.
|
|
|
|
Как показано на рисунке ниже, верх стопки элементов называется вершиной стека, а низ -- основанием стека. Операция добавления элемента на вершину стека называется вставка, а удаление элемента с вершины -- извлечение.
|
|
|
|

|
|
|
|
## Основные операции со стеком
|
|
|
|
Основные операции со стеком представлены в таблице ниже, конкретные имена методов зависят от используемого языка программирования. Здесь в качестве примера используются распространенные имена `push()`, `pop()`, `peek()`.
|
|
|
|
<p align="center"> Таблица <id> Эффективность операций со стеком </p>
|
|
|
|
| Метод | Описание | Временная сложность |
|
|
| -------- | ------------------------------------------------ | ------------------- |
|
|
| `push()` | Вставка элемента (добавление на вершину стека) | $O(1)$ |
|
|
| `pop()` | Извлечение элемента с вершины стека | $O(1)$ |
|
|
| `peek()` | Доступ к элементу на вершине стека | $O(1)$ |
|
|
|
|
Обычно достаточно использовать классы стека, встроенные в язык программирования. Однако в некоторых языках может не быть специального класса для стека. Тогда можно использовать массив или связный список в качестве стека, игнорируя операции, не связанные со стеком.
|
|
|
|
=== "Python"
|
|
|
|
```python title="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
|
|
```
|
|
|
|
=== "C++"
|
|
|
|
```cpp title="stack.cpp"
|
|
/* 初始化栈 */
|
|
stack<int> stack;
|
|
|
|
/* 元素入栈 */
|
|
stack.push(1);
|
|
stack.push(3);
|
|
stack.push(2);
|
|
stack.push(5);
|
|
stack.push(4);
|
|
|
|
/* 访问栈顶元素 */
|
|
int top = stack.top();
|
|
|
|
/* 元素出栈 */
|
|
stack.pop(); // 无返回值
|
|
|
|
/* 获取栈的长度 */
|
|
int size = stack.size();
|
|
|
|
/* 判断是否为空 */
|
|
bool empty = stack.empty();
|
|
```
|
|
|
|
=== "Java"
|
|
|
|
```java title="stack.java"
|
|
/* 初始化栈 */
|
|
Stack<Integer> stack = new Stack<>();
|
|
|
|
/* 元素入栈 */
|
|
stack.push(1);
|
|
stack.push(3);
|
|
stack.push(2);
|
|
stack.push(5);
|
|
stack.push(4);
|
|
|
|
/* 访问栈顶元素 */
|
|
int peek = stack.peek();
|
|
|
|
/* 元素出栈 */
|
|
int pop = stack.pop();
|
|
|
|
/* 获取栈的长度 */
|
|
int size = stack.size();
|
|
|
|
/* 判断是否为空 */
|
|
boolean isEmpty = stack.isEmpty();
|
|
```
|
|
|
|
=== "C#"
|
|
|
|
```csharp title="stack.cs"
|
|
/* 初始化栈 */
|
|
Stack<int> stack = new();
|
|
|
|
/* 元素入栈 */
|
|
stack.Push(1);
|
|
stack.Push(3);
|
|
stack.Push(2);
|
|
stack.Push(5);
|
|
stack.Push(4);
|
|
|
|
/* 访问栈顶元素 */
|
|
int peek = stack.Peek();
|
|
|
|
/* 元素出栈 */
|
|
int pop = stack.Pop();
|
|
|
|
/* 获取栈的长度 */
|
|
int size = stack.Count;
|
|
|
|
/* 判断是否为空 */
|
|
bool isEmpty = stack.Count == 0;
|
|
```
|
|
|
|
=== "Go"
|
|
|
|
```go title="stack_test.go"
|
|
/* 初始化栈 */
|
|
// 在 Go 中,推荐将 Slice 当作栈来使用
|
|
var stack []int
|
|
|
|
/* 元素入栈 */
|
|
stack = append(stack, 1)
|
|
stack = append(stack, 3)
|
|
stack = append(stack, 2)
|
|
stack = append(stack, 5)
|
|
stack = append(stack, 4)
|
|
|
|
/* 访问栈顶元素 */
|
|
peek := stack[len(stack)-1]
|
|
|
|
/* 元素出栈 */
|
|
pop := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
|
|
/* 获取栈的长度 */
|
|
size := len(stack)
|
|
|
|
/* 判断是否为空 */
|
|
isEmpty := len(stack) == 0
|
|
```
|
|
|
|
=== "Swift"
|
|
|
|
```swift title="stack.swift"
|
|
/* 初始化栈 */
|
|
// Swift 没有内置的栈类,可以把 Array 当作栈来使用
|
|
var stack: [Int] = []
|
|
|
|
/* 元素入栈 */
|
|
stack.append(1)
|
|
stack.append(3)
|
|
stack.append(2)
|
|
stack.append(5)
|
|
stack.append(4)
|
|
|
|
/* 访问栈顶元素 */
|
|
let peek = stack.last!
|
|
|
|
/* 元素出栈 */
|
|
let pop = stack.removeLast()
|
|
|
|
/* 获取栈的长度 */
|
|
let size = stack.count
|
|
|
|
/* 判断是否为空 */
|
|
let isEmpty = stack.isEmpty
|
|
```
|
|
|
|
=== "JS"
|
|
|
|
```javascript title="stack.js"
|
|
/* 初始化栈 */
|
|
// JavaScript 没有内置的栈类,可以把 Array 当作栈来使用
|
|
const stack = [];
|
|
|
|
/* 元素入栈 */
|
|
stack.push(1);
|
|
stack.push(3);
|
|
stack.push(2);
|
|
stack.push(5);
|
|
stack.push(4);
|
|
|
|
/* 访问栈顶元素 */
|
|
const peek = stack[stack.length-1];
|
|
|
|
/* 元素出栈 */
|
|
const pop = stack.pop();
|
|
|
|
/* 获取栈的长度 */
|
|
const size = stack.length;
|
|
|
|
/* 判断是否为空 */
|
|
const is_empty = stack.length === 0;
|
|
```
|
|
|
|
=== "TS"
|
|
|
|
```typescript title="stack.ts"
|
|
/* 初始化栈 */
|
|
// TypeScript 没有内置的栈类,可以把 Array 当作栈来使用
|
|
const stack: number[] = [];
|
|
|
|
/* 元素入栈 */
|
|
stack.push(1);
|
|
stack.push(3);
|
|
stack.push(2);
|
|
stack.push(5);
|
|
stack.push(4);
|
|
|
|
/* 访问栈顶元素 */
|
|
const peek = stack[stack.length - 1];
|
|
|
|
/* 元素出栈 */
|
|
const pop = stack.pop();
|
|
|
|
/* 获取栈的长度 */
|
|
const size = stack.length;
|
|
|
|
/* 判断是否为空 */
|
|
const is_empty = stack.length === 0;
|
|
```
|
|
|
|
=== "Dart"
|
|
|
|
```dart title="stack.dart"
|
|
/* 初始化栈 */
|
|
// Dart 没有内置的栈类,可以把 List 当作栈来使用
|
|
List<int> stack = [];
|
|
|
|
/* 元素入栈 */
|
|
stack.add(1);
|
|
stack.add(3);
|
|
stack.add(2);
|
|
stack.add(5);
|
|
stack.add(4);
|
|
|
|
/* 访问栈顶元素 */
|
|
int peek = stack.last;
|
|
|
|
/* 元素出栈 */
|
|
int pop = stack.removeLast();
|
|
|
|
/* 获取栈的长度 */
|
|
int size = stack.length;
|
|
|
|
/* 判断是否为空 */
|
|
bool isEmpty = stack.isEmpty;
|
|
```
|
|
|
|
=== "Rust"
|
|
|
|
```rust title="stack.rs"
|
|
/* 初始化栈 */
|
|
// 把 Vec 当作栈来使用
|
|
let mut stack: Vec<i32> = Vec::new();
|
|
|
|
/* 元素入栈 */
|
|
stack.push(1);
|
|
stack.push(3);
|
|
stack.push(2);
|
|
stack.push(5);
|
|
stack.push(4);
|
|
|
|
/* 访问栈顶元素 */
|
|
let top = stack.last().unwrap();
|
|
|
|
/* 元素出栈 */
|
|
let pop = stack.pop().unwrap();
|
|
|
|
/* 获取栈的长度 */
|
|
let size = stack.len();
|
|
|
|
/* 判断是否为空 */
|
|
let is_empty = stack.is_empty();
|
|
```
|
|
|
|
=== "C"
|
|
|
|
```c title="stack.c"
|
|
// C 未提供内置栈
|
|
```
|
|
|
|
=== "Kotlin"
|
|
|
|
```kotlin title="stack.kt"
|
|
/* 初始化栈 */
|
|
val stack = Stack<Int>()
|
|
|
|
/* 元素入栈 */
|
|
stack.push(1)
|
|
stack.push(3)
|
|
stack.push(2)
|
|
stack.push(5)
|
|
stack.push(4)
|
|
|
|
/* 访问栈顶元素 */
|
|
val peek = stack.peek()
|
|
|
|
/* 元素出栈 */
|
|
val pop = stack.pop()
|
|
|
|
/* 获取栈的长度 */
|
|
val size = stack.size
|
|
|
|
/* 判断是否为空 */
|
|
val isEmpty = stack.isEmpty()
|
|
```
|
|
|
|
=== "Ruby"
|
|
|
|
```ruby title="stack.rb"
|
|
# 初始化栈
|
|
# Ruby 没有内置的栈类,可以把 Array 当作栈来使用
|
|
stack = []
|
|
|
|
# 元素入栈
|
|
stack << 1
|
|
stack << 3
|
|
stack << 2
|
|
stack << 5
|
|
stack << 4
|
|
|
|
# 访问栈顶元素
|
|
peek = stack.last
|
|
|
|
# 元素出栈
|
|
pop = stack.pop
|
|
|
|
# 获取栈的长度
|
|
size = stack.length
|
|
|
|
# 判断是否为空
|
|
is_empty = stack.empty?
|
|
```
|
|
|
|
??? pythontutor "可视化运行"
|
|
|
|
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
|
|
|
## Реализация стека
|
|
|
|
Чтобы глубже понять механизм работы стека, попробуем реализовать собственный класс стека.
|
|
|
|
Стек следует принципу «первый вошел -- последний вышел», поэтому добавление и удаление элементов возможно только на вершине стека. Однако в массивах и связных списках элементы можно добавлять и удалять в любом месте, **поэтому стек можно рассматривать как ограниченный массив или связный список**. Иными словами, можно скрыть часть операций массива или связного списка, чтобы их внешняя логика соответствовала характеристикам стека.
|
|
|
|
### Реализация на основе связного списка
|
|
|
|
При использовании для реализации стека связного списка можно считать головной узел связного списка вершиной стека, а хвостовой узел -- основанием стека.
|
|
|
|
Как показано на рисунке ниже, для операции вставки элемента достаточно вставить его в начало связного списка. Этот метод вставки узла называется вставка в голову. Для операции извлечения элемента достаточно удалить головной узел из связного списка.
|
|
|
|
=== "LinkedListStack"
|
|

|
|
|
|
=== "push()"
|
|

|
|
|
|
=== "pop()"
|
|

|
|
|
|
Ниже приведен пример кода для реализации стека на основе связного списка:
|
|
|
|
```src
|
|
[file]{linkedlist_stack}-[class]{linked_list_stack}-[func]{}
|
|
```
|
|
|
|
### Реализация на основе массива
|
|
|
|
При использовании для реализации стека массива можно считать конец массива вершиной стека. Как показано на рисунке ниже, операции вставки и извлечения соответствуют добавлению и удалению элементов в конце массива. Временная сложность этих операций составляет $O(1)$.
|
|
|
|
=== "ArrayStack"
|
|

|
|
|
|
=== "push()"
|
|

|
|
|
|
=== "pop()"
|
|

|
|
|
|
Поскольку количество вставляемых элементов может постоянно увеличиваться, можно использовать динамический массив, чтобы не заниматься расширением массива самостоятельно. Ниже приведен пример кода:
|
|
|
|
```src
|
|
[file]{array_stack}-[class]{array_stack}-[func]{}
|
|
```
|
|
|
|
## Сравнение двух реализаций
|
|
|
|
**Поддерживаемые операции**
|
|
|
|
Обе реализации поддерживают все операции, определенные для стека. Реализация на основе массива дополнительно поддерживает произвольный доступ, но это выходит за рамки определения стека, поэтому обычно не используется.
|
|
|
|
**Временная сложность**
|
|
|
|
В реализации на основе массива операции добавления и удаления элемента выполняются в заранее выделенной непрерывной памяти, что обеспечивает хорошую локальность кеша и, следовательно, высокую эффективность. Однако, если при добавлении элемента превышается емкость массива, срабатывает механизм расширения, что приводит к увеличению временной сложности данной операции до $O(n)$.
|
|
|
|
В реализации на основе связного списка расширение происходит очень гибко, и не возникает проблемы снижения эффективности, как в случае расширения массива. Однако операция добавления элемента требует инициализации объекта узла и изменения указателя, что делает ее относительно менее эффективной. Тем не менее, если добавляемый элемент уже является объектом узла, можно избежать шага инициализации, что повысит эффективность.
|
|
|
|
Таким образом, если элементы операций добавления и удаления являются примитивными типами данных, такими как `int` или `double`, можно сделать следующие выводы:
|
|
|
|
- Стек, реализованный на основе массива, при срабатывании механизма расширения теряет в эффективности, но, так как расширение является редкой операцией, средняя эффективность выше.
|
|
- Стек, реализованный на основе связного списка, обеспечивает более стабильную эффективность.
|
|
|
|
**Пространственная сложность**
|
|
|
|
При инициализации массива система выделяет для него начальную емкость, которая может превышать фактические потребности. Кроме того, механизм расширения обычно осуществляется с определенным коэффициентом (например, в 2 раза), и емкость после расширения также может превышать фактические потребности. Поэтому **стек, реализованный на основе массива, может приводить к некоторым потерям пространства**.
|
|
|
|
Однако, так как узлы связного списка требуют дополнительного хранения указателей, **занимаемое ими пространство сравнительно больше**.
|
|
|
|
Таким образом, нельзя однозначно определить, какая реализация более экономична в плане памяти, необходимо анализировать конкретные ситуации.
|
|
|
|
## Типичные сценарии применения стека
|
|
|
|
- **Возврат и переход вперед в браузере, отмена и повтор в программном обеспечении**. Каждый раз, когда открывается новая веб-страница, браузер выполняет добавление предыдущей страницы в стек, что позволяет вернуться к ней с помощью операции возврата. Операция возврата фактически является выполнением удаления из стека. Если требуется поддержка как возврата, так и перехода вперед, необходимо использовать два стека.
|
|
- **Управление памятью программы**. Каждый раз при вызове функции система добавляет на вершину стека фрейм для записи контекстной информации функции. В рекурсивных функциях на этапе нисходящей рекурсии постоянно выполняется добавление в стек, а на этапе восходящей рекурсии -- удаление из стека. |