AIGC标识 栈(Stack)

栈(Stack)

一、栈的核心概念

栈是一种后进先出(LIFO, Last In First Out)的线性数据结构。可以把它想象成一摞盘子:你只能在顶部放盘子和取盘子,最先放进去的盘子最后才能取出来。

栈只有一端开放(称为栈顶 Top),所有操作都发生在这一端:

  • Push(入栈):将元素放到栈顶
  • Pop(出栈):移除并返回栈顶元素
  • Peek(查看栈顶):返回栈顶元素但不移除

栈的特性

操作 时间复杂度
Push 入栈 O(1)
Pop 出栈 O(1)
Peek 查看栈顶 O(1)
查找 O(n)
空间复杂度 O(n)

栈的典型应用场景

  1. 函数调用栈:程序运行时的函数嵌套调用就是栈结构
  2. 括号匹配:编译器检查 ()[]{} 是否配对
  3. 表达式求值:后缀表达式(逆波兰表示法)的计算
  4. 浏览器后退:将访问历史压栈,后退时弹栈
  5. DFS 深度优先搜索:用栈替代递归实现
  6. 撤销操作(Undo):每次操作压栈,撤销时弹栈

二、两种实现方式

栈可以用数组(或 Go 切片)或链表来实现,两种方式各有优劣:

2.1 基于切片(数组)的实现

利用 Go 切片的动态扩容特性,栈顶就是切片末尾。

优点

  • 实现简单,代码量少
  • 内存连续,CPU 缓存命中率高
  • Go 切片自动管理扩容

缺点

  • 扩容时需要拷贝数据(均摊 O(1))
  • 大量 Push 可能触发多次扩容

2.2 基于链表的实现

栈顶是链表头节点,Push 做头插,Pop 做头删。

优点

  • 不需要预分配内存,按需分配
  • 没有扩容拷贝开销
  • 每个 Push/Pop 都是严格的 O(1)

缺点

  • 每个节点额外存储指针,内存开销更大
  • 指针跳转导致缓存不友好

三、基于切片的栈实现

package main

import "fmt"

// ArrayStack 基于切片的栈
type ArrayStack struct {
    data []int
}

func NewArrayStack() *ArrayStack {
    return &ArrayStack{
        data: make([]int, 0),
    }
}

// Push 入栈 O(1) 均摊
func (s *ArrayStack) Push(val int) {
    s.data = append(s.data, val)
}

// Pop 出栈 O(1) 均摊
func (s *ArrayStack) Pop() (int, bool) {
    if len(s.data) == 0 {
        return 0, false
    }
    index := len(s.data) - 1
    val := s.data[index]
    s.data = s.data[:index]
    return val, true
}

// Peek 查看栈顶 O(1)
func (s *ArrayStack) Peek() (int, bool) {
    if len(s.data) == 0 {
        return 0, false
    }
    return s.data[len(s.data)-1], true
}

// IsEmpty 判空
func (s *ArrayStack) IsEmpty() bool {
    return len(s.data) == 0
}

// Size 返回栈大小
func (s *ArrayStack) Size() int {
    return len(s.data)
}

func main() {
    stack := NewArrayStack()

    // 入栈 1,2,3
    stack.Push(1)
    stack.Push(2)
    stack.Push(3)
    fmt.Println("栈大小:", stack.Size())        // 3
    fmt.Println("栈顶元素:", mustPeek(stack))    // 3

    // 出栈
    val, _ := stack.Pop()
    fmt.Println("出栈:", val)                     // 3
    val, _ = stack.Pop()
    fmt.Println("出栈:", val)                     // 2
    fmt.Println("栈大小:", stack.Size())          // 1

    // 括号匹配检测
    fmt.Println("\n--- 括号匹配检测 ---")
    fmt.Println("\"(a+b)*[c-d]\" 匹配?", isBracketMatch("(a+b)*[c-d]"))   // true
    fmt.Println("\"(a+b]*[c-d)\" 匹配?", isBracketMatch("(a+b]*[c-d)"))   // false
    fmt.Println("\"((()))\" 匹配?", isBracketMatch("((()))"))             // true
    fmt.Println("\"(()\" 匹配?", isBracketMatch("(()"))                   // false
}

func mustPeek(s *ArrayStack) int {
    v, _ := s.Peek()
    return v
}

运行结果

栈大小: 3
栈顶元素: 3
出栈: 3
出栈: 2
栈大小: 1

--- 括号匹配检测 ---
"(a+b)*[c-d]" 匹配? true
"(a+b]*[c-d)" 匹配? false
"((()))" 匹配? true
"(()" 匹配? false

四、基于链表的栈实现

package main

import "fmt"

// stackNode 链表栈节点
type stackNode struct {
    data int
    next *stackNode
}

// LinkedStack 基于链表的栈
type LinkedStack struct {
    top *stackNode // 栈顶指针,指向链表头
    len int
}

func NewLinkedStack() *LinkedStack {
    return &LinkedStack{}
}

// Push 入栈 O(1) — 头插法
func (s *LinkedStack) Push(val int) {
    s.top = &stackNode{data: val, next: s.top}
    s.len++
}

// Pop 出栈 O(1) — 头删法
func (s *LinkedStack) Pop() (int, bool) {
    if s.top == nil {
        return 0, false
    }
    val := s.top.data
    s.top = s.top.next
    s.len--
    return val, true
}

// Peek 查看栈顶 O(1)
func (s *LinkedStack) Peek() (int, bool) {
    if s.top == nil {
        return 0, false
    }
    return s.top.data, true
}

func (s *LinkedStack) IsEmpty() bool {
    return s.top == nil
}

func (s *LinkedStack) Size() int {
    return s.len
}

func main() {
    stack := NewLinkedStack()

    stack.Push(100)
    stack.Push(200)
    stack.Push(300)

    fmt.Println("栈大小:", stack.Size())  // 3
    fmt.Println("栈顶:", mustPeek2(stack)) // 300

    for !stack.IsEmpty() {
        val, _ := stack.Pop()
        fmt.Println("出栈:", val) // 300, 200, 100
    }
}

func mustPeek2(s *LinkedStack) int {
    v, _ := s.Peek()
    return v
}

运行结果

栈大小: 3
栈顶: 300
出栈: 300
出栈: 200
出栈: 100

五、实战应用:括号匹配检测

括号匹配是栈最经典的应用。算法思路:

  1. 遍历字符串的每个字符
  2. 遇到左括号 ( [ { → 压入栈中
  3. 遇到右括号 ) ] } → 弹出栈顶元素,检查是否匹配
  4. 遍历结束后,栈应为空
// isBracketMatch 检查字符串中的括号是否匹配
func isBracketMatch(s string) bool {
    stack := NewArrayStack()
    pairs := map[rune]rune{
        ')': '(',
        ']': '[',
        '}': '{',
    }
    for _, ch := range s {
        switch ch {
        case '(', '[', '{':
            stack.Push(int(ch))
        case ')', ']', '}':
            top, ok := stack.Pop()
            if !ok || top != int(pairs[ch]) {
                return false
            }
        }
    }
    return stack.IsEmpty()
}

算法分析

  • 时间复杂度:O(n),每个字符最多入栈出栈一次
  • 空间复杂度:O(n),最坏情况全部是左括号

六、实战应用:用栈实现队列

队列是 FIFO 结构,而栈是 LIFO 结构。用两个栈可以模拟一个队列:

  • 入队:直接压入 input 栈
  • 出队:如果 output 栈为空,将 input 栈所有元素依次弹出并压入 output 栈(此时顺序反转),然后从 output 栈弹出
// StackQueue 双栈实现队列
type StackQueue struct {
    inStack  *ArrayStack
    outStack *ArrayStack
}

func NewStackQueue() *StackQueue {
    return &StackQueue{
        inStack:  NewArrayStack(),
        outStack: NewArrayStack(),
    }
}

// Enqueue 入队 — 直接压入 inStack
func (q *StackQueue) Enqueue(val int) {
    q.inStack.Push(val)
}

// Dequeue 出队 — 从 outStack 弹出,空则倒灌
func (q *StackQueue) Dequeue() (int, bool) {
    if q.outStack.IsEmpty() {
        // 将 inStack 所有元素倒入 outStack
        for !q.inStack.IsEmpty() {
            val, _ := q.inStack.Pop()
            q.outStack.Push(val)
        }
    }
    return q.outStack.Pop()
}

每个元素最多被 Push/Pop 两次(一次进 inStack,一次进 outStack),均摊时间复杂度 O(1)。

七、两种实现对比

维度 切片实现 链表实现
Push O(1) 均摊 O(1) 严格
Pop O(1) 均摊 O(1) 严格
内存连续性 好(缓存友好) 差(指针跳转)
扩容开销 有(拷贝)
额外内存 每节点一个指针
代码复杂度
适用场景 通用、数据量可控 数据量极大或不确定

实践建议:日常开发中优先用切片实现,简单高效。只有当数据量极大且 Push 频率非常高(扩容拷贝成为瓶颈)时,才考虑链表实现。Go 标准库没有提供内置的 Stack 类型,但切片本身就足够好用。

八、总结

栈是一种"受限"的线性结构——只允许在栈顶操作。正是这个限制,赋予了它 LIFO 的特性和 O(1) 的 push/pop 效率。理解栈的关键在于:

  1. LIFO 特性:最后放入的最先取出
  2. 两种实现:切片(简单高效)和链表(严格 O(1))
  3. 核心应用:括号匹配、表达式求值、DFS、函数调用、Undo/Redo
  4. 双栈技巧:两个栈可以模拟队列,体现了"用受限工具构建复杂结构"的思维
posted @ 2026-07-27 09:02  FfHUCisI  阅读(19)  评论(0)    收藏  举报