栈与队列实战通关:3道经典OJ题深度解析

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划程序人生

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

栈与队列实战通关:3道经典OJ题深度解析✨

你好!欢迎来到线性表系列栈与队列篇的终篇内容~

在前两篇中,我们已经吃透了栈和队列的核心概念与底层实现,从"后进先出"的栈到"先进先出"的队列,完成了理论基础的搭建。但数据结构的学习最终要落地到实战------今天我们就聚焦LeetCode高频OJ题,用3道经典题目带你打通栈与队列的解题思路,把理论转化为真正的解题能力!💪


文章摘要

本文为线性表系列栈与队列篇终篇实战内容,聚焦3道高频OJ题深度解析。以有效的括号、用队列实现栈、用栈实现队列为核心,拆解每道题的解题思路,提供完整可运行的代码实现,详解栈与队列的特性适配、边界条件处理及报错调试技巧。从基础应用到结构转换,全方位锻炼栈与队列的灵活运用能力,零基础也能通关经典算法题。

阅读时长 :约25分钟
阅读建议

  1. 初学者:先理解解题思路,再逐行分析代码逻辑
  2. 刷题备考者:重点掌握"结构转换"类题目的核心思想
  3. 面试冲刺者:牢记括号匹配的解题模板,可直接复用
  4. 查漏补缺者:聚焦代码中的报错处理和边界判断细节

一、知识回顾:栈与队列核心解题思路

在刷OJ题之前,先梳理栈与队列的核心解题技巧:

数据结构 核心特性 典型应用场景 解题关键
后进先出 括号匹配、表达式求值、逆序处理 左入栈、右匹配,利用栈顶做对比
队列 先进先出 层序遍历、任务排队、结构转换 双队列/双栈联动,实现特性反转

核心原则:用栈处理"逆序"问题,用队列处理"顺序"问题,二者联动可实现特性互转


二、实战第一题:有效的括号(LeetCode 20)🔥

题目描述

给定只包含 '('、')'、'['、']'、'{'、'}' 的字符串 s,判断字符串是否有效。有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合
  2. 左括号必须以正确的顺序闭合

示例

  • 输入:s = "()" → 输出:true
  • 输入:s = "([)]" → 输出:false
  • 输入:s = "{()}" → 输出:true

解题思路(栈的经典应用)

✅ 核心逻辑:左括号入栈,右括号出栈匹配

  1. 遍历字符串,遇到左括号((/[/{)直接入栈
  2. 遇到右括号()/]/}):
    • 若栈为空 → 无左括号匹配,直接返回false
    • 弹出栈顶元素,判断是否与当前右括号匹配
    • 不匹配则返回false,匹配则继续遍历
  3. 遍历结束后,若栈为空 → 所有括号匹配成功;否则返回false

完整代码实现

c 复制代码
#include "stack.h" // 引入之前实现的栈头文件

bool isValid(char* s)
{
    ST st;
    StackInit(&st); // 初始化栈

    while (*s != '\0')
    {
        // 左括号入栈
        if (*s == '(' || *s == '[' || *s == '{')
        {
            StackPush(&st, *s);
            s++;
        }
        // 右括号匹配
        else
        {
            // 栈空但遇到右括号 → 无效
            if (StackEmpty(&st))
            {
                StackDestroy(&st);
                return false;
            }

            char top = StackTop(&st);
            StackPop(&st);

            // 判断是否匹配
            if ((*s == ')' && top != '(') || 
                (*s == ']' && top != '[') || 
                (*s == '}' && top != '{'))
            {
                StackDestroy(&st);
                return false;
            }
            s++;
        }
    }

    // 遍历结束后栈必须为空(避免左括号多余)
    bool ret = StackEmpty(&st);
    StackDestroy(&st); // 必须销毁栈,避免内存泄漏
    return ret;
}

易错点&调试技巧

  1. ❌ 忘记销毁栈:会导致内存泄漏,OJ可能不报错但工程中必出问题
  2. ❌ 忽略"栈空遇右括号":比如输入")(",需提前判空
  3. ❌ 遍历结束后未检查栈空:比如输入"({",左括号多余需返回false
  4. 📝 调试技巧:遇到报错先看测试用例,用纸笔模拟栈的入栈/出栈过程

三、实战第二题:用队列实现栈(LeetCode 225)🔄

题目描述

仅使用两个队列实现一个后进先出(LIFO)的栈,支持栈的pushpoptopempty操作。

解题思路(队列转栈)

✅ 核心逻辑:双队列联动,保留最后一个元素实现后进先出

  1. 准备两个队列q1q2,始终保持一个队列为空
  2. 入栈:往非空队列中插入元素(都空则随便选一个)
  3. 出栈:将非空队列的元素依次导入空队列,仅保留最后一个元素,弹出该元素
  4. 获取栈顶:直接取非空队列的队尾元素

完整代码实现

c 复制代码
#include "queue.h" // 引入之前实现的队列头文件

// 定义栈结构(包含两个队列)
typedef struct
{
    Queue q1;
    Queue q2;
} MyStack;

// 创建栈
MyStack* myStackCreate() 
{
    MyStack* ps = (MyStack*)malloc(sizeof(MyStack));
    if (ps == NULL)
    {
        perror("malloc fail");
        exit(1);
    }
    QueueInit(&ps->q1);
    QueueInit(&ps->q2);
    return ps;
}

// 入栈
void myStackPush(MyStack* obj, int x) 
{
    // 往非空队列插入
    if (!QueueEmpty(&obj->q1))
    {
        QueuePush(&obj->q1, x);
    }
    else
    {
        QueuePush(&obj->q2, x);
    }
}

// 出栈
int myStackPop(MyStack* obj) 
{
    // 区分空队列和非空队列
    Queue* emptyQ = &obj->q1;
    Queue* nonemptyQ = &obj->q2;
    if (!QueueEmpty(&obj->q1))
    {
        emptyQ = &obj->q2;
        nonemptyQ = &obj->q1;
    }

    // 非空队列仅保留最后一个元素
    while (QueueSize(nonemptyQ) > 1)
    {
        QueuePush(emptyQ, QueueFront(nonemptyQ));
        QueuePop(nonemptyQ);
    }

    // 弹出最后一个元素(栈顶)
    int top = QueueFront(nonemptyQ);
    QueuePop(nonemptyQ);
    return top;
}

// 获取栈顶
int myStackTop(MyStack* obj) 
{
    if (!QueueEmpty(&obj->q1))
    {
        return QueueBack(&obj->q1);
    }
    else
    {
        return QueueBack(&obj->q2);
    }
}

// 判断栈空
bool myStackEmpty(MyStack* obj) 
{
    return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}

// 销毁栈
void myStackFree(MyStack* obj) 
{
    QueueDestroy(&obj->q1);
    QueueDestroy(&obj->q2);
    free(obj);
}

四、实战第三题:用栈实现队列(LeetCode 232)🔄

题目描述

仅使用两个栈实现一个先进先出(FIFO)的队列,支持队列的pushpoppeekempty操作。

解题思路(栈转队列)

✅ 核心逻辑:双栈分工,入栈存数据,出栈做反转

  1. 准备两个栈pushST(入队专用)、popST(出队专用)
  2. 入队:直接往pushST中压栈
  3. 出队:若popST为空,将pushST的所有元素导入popST(实现反转),再弹出popST栈顶
  4. 获取队头:逻辑同出队,仅取栈顶不弹出

完整代码实现

c 复制代码
// 栈的基础实现(复用之前的栈结构)
typedef int STData;
typedef struct Stack
{
    STData* a;
    int top;
    int capacity;
}ST;

void STInit(ST* ps)
{
    assert(ps);
    ps->a = (STData*)malloc(sizeof(STData)*4);
    if (ps->a == NULL) { perror("malloc fail"); exit(1); }
    ps->capacity = 4;
    ps->top = 0;
}

void STDestroy(ST* ps)
{
    assert(ps);
    free(ps->a);
    ps->a = NULL;
    ps->capacity = 0;
    ps->top = 0;
}

void STPush(ST* ps, STData x)
{
    assert(ps);
    if (ps->top == ps->capacity)
    {
        STData* tmp = (STData*)realloc(ps->a, sizeof(STData)*ps->capacity*2);
        if (tmp == NULL) { perror("realloc fail"); exit(1); }
        ps->a = tmp;
        ps->capacity *= 2;
    }
    ps->a[ps->top++] = x;
}

void STPop(ST* ps) { assert(ps); assert(!(ps->top == 0)); ps->top--; }
STData STTop(ST* ps) { assert(ps); assert(!(ps->top == 0)); return ps->a[ps->top-1]; }
bool STEmpty(ST* ps) { assert(ps); return ps->top == 0; }

// 队列结构定义(包含两个栈)
typedef struct 
{
    ST pushst; // 入队栈
    ST popst;  // 出队栈
} MyQueue;

// 创建队列
MyQueue* myQueueCreate() 
{
    MyQueue *q = (MyQueue*)malloc(sizeof(MyQueue));
    if (q == NULL) { perror("malloc fail"); exit(1); }
    STInit(&q->pushst);
    STInit(&q->popst);
    return q;
}

// 入队
void myQueuePush(MyQueue* obj, int x) 
{
    STPush(&obj->pushst, x); // 只管往pushst插入
}

// 出队
int myQueuePop(MyQueue* obj) 
{
    // popst为空时,导入pushst的所有元素
    if (STEmpty(&obj->popst))
    {
        while (!STEmpty(&obj->pushst))
        {
            STPush(&obj->popst, STTop(&obj->pushst));
            STPop(&obj->pushst);
        }
    }
    int top = STTop(&obj->popst);
    STPop(&obj->popst);
    return top;
}

// 获取队头
int myQueuePeek(MyQueue* obj) 
{
    if (STEmpty(&obj->popst))
    {
        while (!STEmpty(&obj->pushst))
        {
            STPush(&obj->popst, STTop(&obj->pushst));
            STPop(&obj->pushst);
        }
    }
    return STTop(&obj->popst);
}

// 判断队列空
bool myQueueEmpty(MyQueue* obj) 
{
    return STEmpty(&obj->pushst) && STEmpty(&obj->popst);
}

// 销毁队列
void myQueueFree(MyQueue* obj) 
{
    STDestroy(&obj->pushst);
    STDestroy(&obj->popst);
    free(obj);
}

五、核心解题技巧总结🎯

  1. 括号匹配:栈是最优解,记住"左入右匹配"的模板,可直接应对所有括号类题目
  2. 结构转换
    • 队列实现栈:双队列"留最后一个元素"
    • 栈实现队列:双栈"入栈存、出栈转"
  3. 内存管理:OJ题也要注意销毁栈/队列,避免内存泄漏(工程中必现问题)
  4. 边界处理:所有操作前先判空,是避免空指针错误的关键

六、写在最后

恭喜你!栈与队列篇至此圆满结束🎉~

从栈和队列的核心概念,到底层工程化实现,再到经典OJ题实战,你已经完成了"理论→实现→应用"的完整闭环:

  • 掌握了栈"后进先出"、队列"先进先出"的核心特性
  • 能独立实现数组版栈、链表版队列的完整代码
  • 会用栈和队列解决括号匹配、结构转换等经典问题

栈和队列是数据结构的"入门基石",后续的二叉树、图、算法优化都离不开它们。建议你多敲几遍代码,吃透每一道题的解题思路------编程的核心,永远是"理解+实践"!

下一个系列,我们将进入二叉树的世界,开启更有趣的学习之旅~😜


点赞+收藏+关注,跟着系列内容一步步吃透数据结构!你的支持是我创作的最大动力~👍

相关推荐
你怎么知道我是队长8 小时前
C语言---枚举变量
c语言·开发语言
POLITE39 小时前
Leetcode 23. 合并 K 个升序链表 (Day 12)
算法·leetcode·链表
会员果汁10 小时前
leetcode-动态规划-买卖股票
算法·leetcode·动态规划
橘颂TA10 小时前
【剑斩OFFER】算法的暴力美学——二进制求和
算法·leetcode·哈希算法·散列表·结构与算法
尋有緣12 小时前
力扣1355-活动参与者
大数据·数据库·leetcode·oracle·数据库开发
kaikaile199513 小时前
基于拥挤距离的多目标粒子群优化算法(MO-PSO-CD)详解
数据结构·算法
不忘不弃13 小时前
求两组数的平均值
数据结构·算法
leaves falling13 小时前
迭代实现 斐波那契数列
数据结构·算法
2401_8769075213 小时前
USB TYPE-C 公头连接器设计规范总结:提升可靠性、降本增效的关键指南
c语言·开发语言·设计规范
Morwit13 小时前
*【力扣hot100】 647. 回文子串
c++·算法·leetcode