Stack<T> это стопка: вы кладёте элементы наверх через Push и снимаете их сверху через Pop, поэтому последний положенный элемент выходит первым (LIFO). Доступна только вершина, и каждая операция с ней выполняется за постоянное время.
Push, Pop и Peek
Вывод:
On top: green
Count: 3
Took green
On top: red
Took red
Took blue
Count: 0
green положили последним, поэтому он снимается первым. Peek возвращает вершину, не меняя стек; так можно посмотреть, что даст Pop, прежде чем решить, забирать ли элемент.
Исключение пустого стека и TryPop
Извлечение или просмотр вершины пустого стека выбрасывает InvalidOperationException. Чаще всего это случается в парсерах и алгоритмах, получивших на вход больше закрывающих элементов, чем открывающих.
Вывод:
Caught InvalidOperationException
True 10
False 0
TryPop и TryPeek (.NET Core 2.0 и новее) возвращают false для пустого стека и задают переменной out значение по умолчанию, здесь 0. В .NET Framework сначала проверяйте Count > 0.
Порядок перебора: сначала вершина
Перебор стека ничего не удаляет и идёт сверху вниз, в том порядке, в каком элементы вернул бы Pop:
Вывод:
checkout products home
checkout > products > home
True
home
checkout
Перевёрнутая копия многих застаёт врасплох: конструктор принимает любой IEnumerable<T> и кладёт его элементы по порядку, а стек перебирается с вершины, поэтому старая вершина оказывается на дне копии. Если сначала перевернуть последовательность (Reverse() из LINQ возвращает элементы начиная со дна), получится копия с той же вершиной.
Если положить список элементов в новый стек, они тоже перевернутся, и это быстрый способ перевернуть последовательность: new Stack<char>("hello") отдаёт обратно o, l, l, e, h.
Пример: история отмены
Редакторы кладут каждое изменение в стек. Отмена извлекает последнее изменение и откатывает его; повтор хранит второй стек отменённых изменений.
Вывод:
Hello, world!
Hello, world
Hello
Hello, world
Хранение целых снимков это самый простой вариант. Настоящие редакторы вместо этого кладут небольшие объекты-команды (что вставлено и куда), у каждого из которых есть метод для отката, но два стека работают так же.
Пример: сбалансированные скобки
Проверка, что (, [ и { закрыты в правильном порядке, это стандартное упражнение на стек, и та же логика работает внутри каждого компилятора и JSON-парсера.
Вывод:
"f(a[i], {x: 1})" -> True
"(]" -> False
"((a)" -> False
"a)b(" -> False
"" -> True
Три проверки на ошибку соответствуют трём способам, которыми скобки могут быть нарушены: закрывающая скобка без открытой (a)b(, её ловит Count == 0, а не исключение из Pop), закрывающая скобка не того вида ((]) и открывающие скобки, которые так и не закрыли (((a), их ловит финальная проверка).
Другие применения
- Поиск в глубину. Замените очередь в поиске в ширину на стек, и обход пойдёт вглубь, а не вширь. Явный стек также заменяет рекурсию, когда входные данные достаточно глубоки, чтобы рисковать
StackOverflowException, который нельзя перехватить. - Вычисление выражений. Постфиксная запись (
3 4 + 2 *) вычисляется так: числа кладутся в стек, а для каждого оператора извлекаются два числа. - Возврат с отменой (backtracking). История навигации, прохождение лабиринта и состояния парсера кладут позицию в стек и возвращаются к ней в тупике.
Аналог «первым пришёл, первым вышел» описан на странице Queue.
Stack, Queue или List
Stack<T> | Queue<T> | List<T> | |
|---|---|---|---|
| Порядок выдачи | Сначала новые | Сначала старые | Любой, по индексу |
| Добавить | Push | Enqueue | Add, Insert |
| Удалить | Pop (вершина) | Dequeue (начало) | Remove, RemoveAt |
| Посмотреть | Peek | Peek | list[i] |
| Безопасные варианты | TryPop, TryPeek | TryDequeue, TryPeek | не нужны |
Для нескольких потоков ConcurrentStack<T> из System.Collections.Concurrent предлагает Push, TryPop и TryPeek без блокировок.
Частые ошибки
- Извлечение без проверки. Пустой стек выбрасывает
InvalidOperationException; проверяйтеCountили используйтеTryPop. - Ожидание, что
foreachначнёт с первого положенного элемента. Он начинает с вершины. - Копирование через
new Stack<T>(stack). Копия получается перевёрнутой. - Добавление внутри
foreachпо тому же стеку. Выбрасывает исключение; используйте циклwhile (stack.Count > 0).
Часто задаваемые вопросы
Что такое Stack в C#?
Stack<T> из System.Collections.Generic это коллекция «последним пришёл, первым вышел» (LIFO). Push кладёт элемент наверх, Pop удаляет и возвращает верхний элемент, а Peek возвращает верхний элемент, не удаляя его. Все три операции выполняются за постоянное время.
Что происходит при Pop из пустого стека в C#?
Pop и Peek выбрасывают InvalidOperationException, когда стек пуст. Сначала проверьте stack.Count > 0 или используйте TryPop(out var item) и TryPeek(out var item), которые вместо исключения возвращают false (.NET Core 2.0 и новее).
В каком порядке foreach проходит по Stack?
Сверху вниз: первым идёт элемент, добавленный последним, в том же порядке, в каком их вернул бы Pop. ToArray() использует тот же порядок. Как следствие, new Stack<T>(otherStack) создаёт перевёрнутую копию, потому что конструктор кладёт элементы в порядке их перебора.
Чем Stack отличается от Queue в C#?
Stack<T> возвращает сначала самый новый элемент (последним пришёл, первым вышел), а Queue<T> сначала самый старый (первым пришёл, первым вышел). Используйте стек для истории отмены, вложенных структур и поиска в глубину; используйте очередь для обработки работы в порядке поступления и поиска в ширину.
Как проверить сбалансированность скобок в C#?
Пройдите строку один раз. Кладите каждую открывающую скобку в Stack<char>. Для каждой закрывающей скобки стек должен быть непустым, а его верхний элемент должен быть соответствующей открывающей скобкой, которую вы затем извлекаете. Строка сбалансирована, если по окончании прохода стек пуст.