【85 道算法题实战・笔记 04】最小栈:从 LeetCode 模式到普通 OJ 的完整落地(双栈原理 + 避坑指南 + C++ 语法细节)

1. 前言

1.1 学习背景

大家好!我是刚学完 C 语言、正通过算法题过渡到 C++ 的计算机专业硕士新手。为了系统掌握 C++ 特性与算法基础,我开启了《图解算法与数据结构》85 道经典入门题的刷题计划,这是我的第 04 篇实战笔记。

本篇笔记会完整记录我从「 LeetCode 输入输出理解」到「自己写出普通 OJ 完整代码」的全过程,同时把我踩过的 C++ 语法坑、逻辑坑 全部整理出来,帮和我一样的初学者避坑、吃透核心知识点~

1.2 本篇目标

  1. 搞懂两种模式:彻底区分 LeetCode 的「类 + 函数」模式 vs 普通 OJ 的「完整程序」模式
  2. 掌握核心算法:用「双栈法」实现常数时间 getMin()
  3. 避过新手坑:变量作用域、函数调用、拼写错误等一个都不放过

2. 题目描述

2.1 LeetCode 原题(来源:LeetBook《图解算法与数据结构》)

设计一个支持 pushpoptop 操作,并能在 常数时间 内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象
  • void push(int val) 将元素 val 推入堆栈
  • void pop() 删除堆栈顶部的元素
  • int top() 获取堆栈顶部的元素
  • int getMin() 获取堆栈中的最小元素
示例 1:

plaintext

输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]
提示:
  • -2^31 <= val <= 2^31 - 1
  • poptopgetMin 操作总是在 非空栈 上调用
  • pushpoptopgetMin 最多被调用 3 * 10^4

2.2 普通 OJ 输入输出格式(学校上机 / 蓝桥杯 / 牛客常用)

如果这道题出在普通 OJ 上,输入输出会长这样:

输入格式:

第一行一个整数 n,表示操作个数接下来 n 行,每行一个操作:

plaintext

8
push -2
push 0
push -3
getMin
pop
top
getMin
输出格式:

对于 topgetMin 操作,每行输出一个结果:

plaintext

-3
0
-2

3. 我的解题思路

3.1 初始思路(有问题!)

一开始我想:“用 C 语言写过链表栈,C++ 直接用 STL stack 就行。”但写了两行发现不对:

  • pushpoptop 都没问题,都是 O (1)
  • getMin() 要遍历整个栈才能找到最小值,时间复杂度是 O (n),不符合题目 “常数时间” 的要求

3.2 优化思路(双栈法!直达本质)

核心痛点:getMin() 要 O (1),说明我们必须随时记录当前的最小值。解决方案:用两个栈

  1. 数据栈 A:存所有元素,实现正常的 push/pop/top
  2. 最小栈 B:只存 “当前最小值”,栈顶永远是整个栈的最小值

3.3 实现步骤

  1. push 操作
    • 数据栈 A 直接压入元素
    • 最小栈 B 只有在空 或 新元素 ≤ B 栈顶时才压入(保证 B 栈顶最小)
  2. pop 操作
    • 如果 A 栈顶 == B 栈顶(说明弹出的是当前最小值),两个栈一起弹
    • 否则只弹 A
  3. top 操作:直接返回 A 栈顶
  4. getMin 操作:直接返回 B 栈顶(O (1)完美!)

4. 代码实现(C++)+ 逐行注释

4.1 LeetCode 版(只写类,不用管输入输出)

class MinStack {
public:
    stack<int> A, B;
    MinStack() {}
    void push(int x) {
        A.push(x);
        if(B.empty() || B.top() >= x)
            B.push(x);
    }
    void pop() {
        if(A.top() == B.top())
            B.pop();
        A.pop();
    }
    int top() {
        return A.top();
    }
    int getMin() {
        return B.top();
    }
};

4.2 普通 OJ 完整版(带 main 函数,自己处理输入输出)

#include <iostream>
#include <stack>
#include <string>
using namespace std;

// 把栈定义为全局变量:让所有函数都能访问(普通OJ常用写法)
stack<int> A, B;

void push(int x) {
    A.push(x);
    if (B.empty() || x <= B.top()) {
        B.push(x);
    }
}

void pop() {
    if (A.top() == B.top()) {
        B.pop();
    }
    A.pop();
}

int top() {
    return A.top();
}

int getMin() {
    return B.top();
}

int main() {
    int n;
    cin >> n;  // 先读操作次数

    while (n--) {
        string op;
        cin >> op;

        if (op == "push") {
            int x;
            cin >> x;
            push(x);  // 调用我们自己写的 push 函数
        }
        else if (op == "pop") {
            pop();    // 调用我们自己写的 pop 函数
        }
        else if (op == "top") {
            cout << top() << endl;  // 注意:是 cout 不是 count!
        }
        else if (op == "getMin") {
            cout << getMin() << endl;
        }
    }

    return 0;
}

4.3 新手 “踩坑版” 代码 + 逐行纠错

#include <iostream>
#include <stack>
#include <string>
using namespace std;

// ❌ 错误1:栈定义在 main 里,上面的函数看不见!
// 应该放到全局,或者封装成类
void push(int x) {
    A.push(x);
    // ❌ 错误2:条件最好写 x <= B.top(),直接用 x 更清晰
    if(B.empty() || B.top()>A.top()){
    	B.push(x);}
    }
    
void pop() {
	if(B.top() == A.top()){
		B.pop();
	}
    A.pop();
    }
    
int top() {
    return A.top();
    }
    
int getMin() {
    return B.top();
    }
    
int main(){
	stack<int> A,B; // ❌ 错误1(续):这里定义的 A、B 只有 main 能用
	int n;
	cin >> n;
	
	while(n--){
		string op;
		cin >> op;
		if(op == "push"){
			int x;
			cin >> x;
			A.push(); // ❌ 错误3:没传参数 x!而且应该调用我们写的 push(x)
		}else if(op == "pop"){
			A.pop();  // ❌ 错误4:应该调用我们写的 pop(),否则最小栈 B 不会同步
		}else if(op == "top"){
			count << A.top(); // ❌ 错误5:拼写错误!是 cout 不是 count
                               // ❌ 错误6:没加 endl,OJ 判题会格式错误
		}else if(op == "getMin"){
			count << B.top(); // ❌ 错误5+6:同上
		}
	}
    // ❌ 错误7:main 函数最好加 return 0;
}

5. 核心知识点深度梳理

5.1 双栈法原理(用示例一步一步看)

就拿题目示例来说,我们看两个栈的变化:

操作数据栈 A最小栈 B当前最小值
push(-2)[-2][-2]-2
push(0)[-2, 0][-2](0 > -2,不压入)-2
push(-3)[-2, 0, -3][-2, -3](-3 ≤ -2,压入)-3
getMin()————-3(B 栈顶)
pop()[-2, 0][-2](弹出的 -3 是 B 栈顶,同步弹)-2
top()————0(A 栈顶)
getMin()————-2(B 栈顶)

完美!每一步都在 O (1) 时间内完成。


5.2 LeetCode 模式 vs 普通 OJ 模式(表格对比,一眼分清)

维度LeetCode 模式普通 OJ 模式
代码结构只写类 / 结构体 + 成员函数写完整程序,必须有 main
输入输出不用管,系统自动传参收返回值自己读输入,自己按格式输出
适用场景LeetCode 专属学校上机、蓝桥杯、PAT、牛客等

5.3 else if vs else(含 Python 的 elif

因为我之前学过Python,会混淆,现在一句话讲透:

  • else if(C++/Java)/ elif(Python):多个并列、平等的条件,只能命中一个
  • else前面所有条件都不满足时的 “兜底情况”

在这道题里,push/pop/top/getMin 是 4 个并列的操作,没有兜底,所以必须用 else if 明确判断每一个。


5.4 全局变量 vs 局部变量

  • 全局变量:定义在所有函数外面,所有函数都能访问(普通 OJ 写算法题常用,简单省事)
  • 局部变量:定义在函数内部(比如 main 里),只有该函数能用

这道题里,push/pop 等函数都要访问栈,所以把栈定义成全局变量最方便。


5.5 C++ 新手拼写易错点

  • 输出:是 cout,不是 count(count 是 “计数” 的意思)
  • 换行:输出结果一定要加 endl,否则 OJ 会判格式错误
  • 函数调用:要调用我们自己封装好的函数(比如 push(x)),不要直接调用 STL 的 A.push()

6. 标准解法对比

6.1 思路原理

除了我们学的双栈法,还有一种常见的单栈存 pair

  • 栈里每个元素存一个 pair<int, int>:第一个值是元素本身,第二个值是 “当前栈的最小值”
  • 本质和双栈法一样,只是把两个栈的信息合并到了一个栈里

6.2 完整代码实现(单栈 pair 法,作为拓展)

#include <stack>
#include <utility> // 要用 pair
using namespace std;

class MinStack {
private:
    // pair<元素值, 当前最小值>
    stack<pair<int, int>> st;
public:
    MinStack() {}

    void push(int val) {
        if (st.empty()) {
            st.push({val, val});
        } else {
            st.push({val, min(val, st.top().second)});
        }
    }

    void pop() {
        st.pop();
    }

    int top() {
        return st.top().first;
    }

    int getMin() {
        return st.top().second;
    }
};

6.3 两种解法复杂度对比

解法时间复杂度空间复杂度
双栈法O (1)(所有操作都是常数时间)O (n)(最坏情况最小栈和数据栈一样大)
单栈 pair 法O(1)O (n)(每个元素多存一个最小值)

两种方法效率一样,双栈法更直观,适合新手理解;pair 法代码更简洁。


7. 学习总结:从 C 到 C++ 的核心收获

这道题是我从 C 过渡到 C++ 的重要一步,核心收获有三个:

  1. STL 很好用:不用自己手写链表实现栈了,stack<int> 直接用!
  2. 模式要分清:LeetCode 专注算法,只写类;普通 OJ 要完整程序,自己处理输入输出
  3. 细节定成败:变量作用域、函数调用、拼写、换行…… 这些小地方一不注意就会让代码报错

后续我会继续刷 LeetBook 85 题,记录每道题的学习过程,也欢迎和我一样的新手一起交流、避坑~


如果你觉得这篇笔记对你有帮助,欢迎点赞、收藏、评论区交流! 🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值