mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-08 05:01:01 +08:00
First version.
This commit is contained in:
430
ru/docs/chapter_stack_and_queue/stack.md
Normal file
430
ru/docs/chapter_stack_and_queue/stack.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# Стек
|
||||
|
||||
<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 раза), и емкость после расширения также может превышать фактические потребности. Поэтому **стек, реализованный на основе массива, может приводить к некоторым потерям пространства**.
|
||||
|
||||
Однако, так как узлы связного списка требуют дополнительного хранения указателей, **занимаемое ими пространство сравнительно больше**.
|
||||
|
||||
Таким образом, нельзя однозначно определить, какая реализация более экономична в плане памяти, необходимо анализировать конкретные ситуации.
|
||||
|
||||
## Типичные сценарии применения стека
|
||||
|
||||
- **Возврат и переход вперед в браузере, отмена и повтор в программном обеспечении**. Каждый раз, когда открывается новая веб-страница, браузер выполняет добавление предыдущей страницы в стек, что позволяет вернуться к ней с помощью операции возврата. Операция возврата фактически является выполнением удаления из стека. Если требуется поддержка как возврата, так и перехода вперед, необходимо использовать два стека.
|
||||
- **Управление памятью программы**. Каждый раз при вызове функции система добавляет на вершину стека фрейм для записи контекстной информации функции. В рекурсивных функциях на этапе нисходящей рекурсии постоянно выполняется добавление в стек, а на этапе восходящей рекурсии -- удаление из стека.
|
||||
Reference in New Issue
Block a user