栈的概念和用法

前言

在日常编程中,你是否遇到过这样的场景:浏览器中的"后退"按钮、文本编辑器的"撤销"操作、函数调用的层层嵌套、表达式求值的优先级处理?这些看似不同的功能,背后都依赖于同一种数据结构------栈。

栈是计算机科学中最基础、最实用的数据结构之一,它体现了"后进先出"(LIFO)的简单哲学,却支撑着无数复杂的计算过程。本文将带你深入理解栈的奥秘。

一、栈的基本概念

1.1 什么是栈

栈(Stack)​ 是一种特殊的线性数据结构,它只允许在固定的一端(称为栈顶)进行插入和删除操作。另一端称为栈底。

1.2 栈的类比理解

想象一下现实生活中的几个场景:

现实场景 栈操作类比
餐厅盘子叠放 新洗的盘子放在最上面,取用时也从最上面拿
子弹夹装弹 最后压入的子弹最先被击发
书本堆叠 最后放上去的书最先被拿走

1.3 栈的核心特性:LIFO原则

LIFO(Last In, First Out,后进先出)是栈的灵魂。这意味着:

  • 最后一个进入栈的元素,将是第一个被取出的元素

  • 第一个进入栈的元素,将是最后一个被取出

二、栈的基本操作

栈支持以下几种基本操作,时间复杂度均为O(1):

2.1 核心操作

复制代码
class Stack:
    def __init__(self):
        self.items = []
    
    def push(self, item):
        """入栈:将元素添加到栈顶"""
        self.items.append(item)
    
    def pop(self):
        """出栈:移除并返回栈顶元素"""
        if not self.is_empty():
            return self.items.pop()
        return None
    
    def peek(self):
        """查看栈顶元素:返回但不移除"""
        if not self.is_empty():
            return self.items[-1]
        return None
    
    def is_empty(self):
        """判断栈是否为空"""
        return len(self.items) == 0
    
    def size(self):
        """返回栈的大小"""
        return len(self.items)

2.2 操作示例演示

复制代码
初始栈:[]
push(10) → 栈:[10]
push(20) → 栈:[10, 20]
push(30) → 栈:[10, 20, 30]
pop()    → 返回30,栈:[10, 20]
peek()   → 返回20,栈:[10, 20](不变)

三、栈的实现方式

因为栈是操作受限的线性表,因此栈的存储方式也有顺序存储和链式存储两种方式。

3.1 顺序栈及其实现

顺序存储方式使用数组保存栈元素,得到的是顺序栈。

具体的实现可以看这篇文章

优点:缓存友好,访问速度快

缺点:需要处理扩容问题

3.2 链式栈及其实现

链式存储方式使用单链表保存栈元素,得到的是链式栈。

具体的实现可以看这篇文章

优点:动态大小,无需预先分配

缺点:额外指针开销,缓存不友好

四、栈的经典应用场景

4.1 函数调用栈(Call Stack)

这是栈在编程语言中最核心的应用。每个函数调用都会创建一个栈帧(Stack Frame):

复制代码
function factorial(n) {
    if (n <= 1) return 1;          // 基本情况
    return n * factorial(n - 1);    // 递归调用
}

// 调用 factorial(3) 的栈帧变化:
// 1. factorial(3) 入栈
// 2. factorial(2) 入栈
// 3. factorial(1) 入栈 → 返回1 → factorial(1) 出栈
// 4. factorial(2) 返回 2 * 1=2 → 出栈
// 5. factorial(3) 返回 3 * 2=6 → 出栈

4.2 表达式求值

中缀表达式转后缀表达式

复制代码
def infix_to_postfix(expression):
    precedence = {'+':1, '-':1, '*':2, '/':2, '^':3}
    stack = []
    output = []
    
    for token in expression.split():
        if token.isdigit():
            output.append(token)
        elif token == '(':
            stack.append(token)
        elif token == ')':
            while stack and stack[-1] != '(':
                output.append(stack.pop())
            stack.pop()  # 弹出'('
        else:  # 运算符
            while (stack and stack[-1] != '(' and 
                   precedence.get(stack[-1], 0) >= precedence.get(token, 0)):
                output.append(stack.pop())
            stack.append(token)
    
    while stack:
        output.append(stack.pop())
    
    return ' '.join(output)

# 示例:中缀 "3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3"
# 转换后:后缀 "3 4 2 * 1 5 - 2 3 ^ ^ / +"

后缀表达式求值

复制代码
int evaluatePostfix(const string& expression) {
    stack<int> st;
    
    for (char c : expression) {
        if (isdigit(c)) {
            st.push(c - '0');
        } else {
            int b = st.top(); st.pop();
            int a = st.top(); st.pop();
            
            switch(c) {
                case '+': st.push(a + b); break;
                case '-': st.push(a - b); break;
                case '*': st.push(a * b); break;
                case '/': st.push(a / b); break;
            }
        }
    }
    return st.top();
}

4.3 括号匹配

这是栈的经典面试题,用于检查括号是否正确闭合:

复制代码
def is_valid_parentheses(s: str) -> bool:
    stack = []
    mapping = {')': '(', ']': '[', '}': '{'}
    
    for char in s:
        if char in mapping.values():  # 左括号
            stack.append(char)
        elif char in mapping:  # 右括号
            if not stack or stack[-1] != mapping[char]:
                return False
            stack.pop()
    
    return not stack  # 栈应为空

# 测试用例
print(is_valid_parentheses("()[]{}"))  # True
print(is_valid_parentheses("([)]"))    # False
print(is_valid_parentheses("{[]}"))    # True

4.4 浏览器历史记录

浏览器的前进后退功能可以用两个栈实现:

复制代码
class BrowserHistory {
    private backStack: string[] = [];
    private forwardStack: string[] = [];
    private current: string = "home";
    
    // 访问新页面
    visit(url: string): void {
        this.backStack.push(this.current);
        this.current = url;
        this.forwardStack = [];  // 清空前进栈
    }
    
    // 后退
    back(): string | null {
        if (this.backStack.length === 0) return null;
        this.forwardStack.push(this.current);
        this.current = this.backStack.pop()!;
        return this.current;
    }
    
    // 前进
    forward(): string | null {
        if (this.forwardStack.length === 0) return null;
        this.backStack.push(this.current);
        this.current = this.forwardStack.pop()!;
        return this.current;
    }
}

4.5 深度优先搜索(DFS)

栈是实现DFS的基础数据结构:

复制代码
def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    
    while stack:
        vertex = stack.pop()
        if vertex not in visited:
            visited.add(vertex)
            print(vertex)  # 处理节点
            
            # 将相邻节点入栈
            for neighbor in graph[vertex]:
                if neighbor not in visited:
                    stack.append(neighbor)

五、栈的变体与扩展

5.1 最小栈

在O(1)时间内获取栈中最小元素:

复制代码
class MinStack {
    private Stack<Integer> stack = new Stack<>();
    private Stack<Integer> minStack = new Stack<>();
    
    public void push(int x) {
        stack.push(x);
        if (minStack.isEmpty() || x <= minStack.peek()) {
            minStack.push(x);
        }
    }
    
    public void pop() {
        if (stack.pop().equals(minStack.peek())) {
            minStack.pop();
        }
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

5.2 单调栈

用于解决"下一个更大元素"类问题:

复制代码
def next_greater_element(nums):
    result = [-1] * len(nums)
    stack = []  # 存储索引
    
    for i in range(len(nums)):
        while stack and nums[i] > nums[stack[-1]]:
            idx = stack.pop()
            result[idx] = nums[i]
        stack.append(i)
    
    return result

# 示例:[2, 1, 2, 4, 3] → [4, 2, 4, -1, -1]

六、栈的复杂度分析

操作 时间复杂度 空间复杂度 说明
push O(1) O(1) 最好/最坏/平均情况都是O(1)
pop O(1) O(1) 同上
peek O(1) O(1) 同上
搜索 O(n) O(1) 需要遍历整个栈
内存 O(n) O(n) 存储n个元素

七、参考文章

  1. 《算法导论》- 栈与队列章节

  2. 《数据结构与算法分析:C语言描述》

  3. 相关LeetCode专题:栈与递归

相关推荐
Q741_1471 天前
每日一题 力扣 2751.机器人碰撞 映射 模拟 栈 C++ 题解
算法·leetcode·模拟··映射
Book思议-3 天前
【数据结构】栈与队列全方位对比 + C 语言完整实现
c语言·数据结构·算法··队列
Book思议-3 天前
【数据结构实战】栈的经典应用:后缀表达式求值 +中缀转后缀 ,原理 + 代码双通透
数据结构·算法··后缀表达式·后缀转中缀
Q741_1476 天前
每日一题 力扣 2946. 循环移位后的矩阵相似检查 力扣 155. 最小栈 数学 数组 模拟 C++ 题解
c++·算法·leetcode·矩阵·模拟·数组·
Book思议-8 天前
【数据结构实战】C语言实现栈的链式存储:从初始化到销毁,手把手教你写可运行代码
数据结构·算法·链表··408
Book思议-8 天前
【数据结构实战】川剧 “扯脸” 与栈的 LIFO 特性 :用 C 语言实现 3 种栈结构
c语言·数据结构·算法·
JCGKS10 天前
海量文档单词计数算法方案分析
golang·数据结构与算法·海量数据·搜索引起·倒排查找
罗超驿11 天前
Java数据结构_栈_算法题
java·数据结构·
I_LPL13 天前
hot100 栈专题
算法·