数据结构--栈和队列详解

数据结构--栈和队列详解

一、开篇引入:为什么要学栈和队列?、开篇引入:为什么要学栈和队列?

在数据结构学习里,栈和队列是最基础、最常用的两种线性结构。它们不像树、图那么复杂,但日常开发和算法设计中,到处都能见到它们的身影。很多初学者觉得,简单的线性表没必要深入学,其实不然。栈和队列的核心价值,就是"受限访问"------通过限制元素的插入和删除位置,让操作逻辑更简单,执行效率更高。这也是我们后续学复杂数据结构(比如树的遍历、图的搜索)和算法(比如DFS、BFS)的基础。

不管是前端的浏览器前进后退、后端的消息队列异步处理,还是算法题里的括号匹配、表达式求值,都离不开栈和队列。这篇文章面向编程初学者、备考的同学,尽量不用晦涩的专业术语,重点放在"理解+实操"上。从定义、实现、应用三个方面,帮你彻底搞懂栈和队列,看完就能上手用。

二、栈(Stack):后进先出(LIFO)的线性表

2.1 栈的核心定义与特性

先给大家举个通俗的例子:我们平时叠盘子,只能把新盘子放在最上面,也只能从最上面拿盘子,不能从中间抽或插。这就是栈的核心逻辑。

从专业角度说,栈是一种线性表,它只允许在表的一端做插入和删除操作。这一端叫"栈顶(top)",另一端固定不动,叫"栈底(bottom)"。栈的操作遵循"后进先出(LIFO)"原则,意思就是,最后插进去的元素,最先被删除。

这里有两个关键点,大家一定要记好:

  • 栈的插入(叫push,入栈)和删除(叫pop,出栈)操作,时间复杂度都是O(1)。因为只需要操作栈顶元素,不用遍历整个结构;

  • 栈的查找操作,比如找某个特定元素,时间复杂度是O(n)。因为得从栈顶开始,一个个遍历到栈底,直到找到目标元素。

再举个简单例子:我们依次把1、2、3、4入栈,栈里的元素从栈底到栈顶,就是1、2、3、4。这时候执行出栈操作,最先弹出来的是4,再弹3,接着弹2,最后弹1。这就是典型的"后进先出"。

2.2 栈的两种实现方式(附核心代码)

栈主要有两种实现方式:数组实现(顺序栈)和链表实现(链式栈)。两种方式各有好坏,大家可以根据实际需求选。下面结合C语言代码,详细讲一讲两种实现的核心思路和操作。

2.2.1 数组实现(顺序栈)

核心思路很简单:用数组当存储容器,定义一个top指针,初始值设为-1,用来标记栈顶位置。栈为空的时候,top就是-1;元素入栈,top加1,把元素存在数组对应位置;元素出栈,取出top位置的元素,top再减1。

核心操作有:入栈(push)、出栈(pop)、查看栈顶元素(peek)、判断栈是否为空(isEmpty)、判断栈是否已满(isFull)。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 10  // 栈的默认容量

// 定义顺序栈结构
typedef struct {
    int data[MAX_SIZE];  // 存储元素的数组
    int top;             // 栈顶指针,初始为-1
} SequenceStack;

// 初始化栈
void initStack(SequenceStack *stack) {
    stack->top = -1;  // 空栈标记
}

// 判断栈是否为空
int isEmpty(SequenceStack *stack) {
    return stack->top == -1;
}

// 判断栈是否已满
int isFull(SequenceStack *stack) {
    return stack->top == MAX_SIZE - 1;
}

// 入栈操作
void push(SequenceStack *stack, int item) {
    if (isFull(stack)) {
        printf("栈已满,无法入栈!\n");
        return;
    }
    stack->top++;
    stack->data[stack->top] = item;
    printf("元素%d入栈成功\n", item);
}

// 出栈操作,返回出栈元素,失败返回-1(可根据需求调整)
int pop(SequenceStack *stack) {
    if (isEmpty(stack)) {
        printf("栈为空,无法出栈!\n");
        return -1;
    }
    int item = stack->data[stack->top];
    stack->top--;
    printf("元素%d出栈成功\n", item);
    return item;
}

// 查看栈顶元素,不弹出,失败返回-1
int peek(SequenceStack *stack) {
    if (isEmpty(stack)) {
        printf("栈为空,无栈顶元素!\n");
        return -1;
    }
    return stack->data[stack->top];
}

// 测试顺序栈
int main() {
    SequenceStack stack;
    initStack(&stack);
    
    push(&stack, 1);
    push(&stack, 2);
    printf("栈顶元素:%d\n", peek(&stack));  // 输出2
    pop(&stack);  // 弹出2
    printf("栈顶元素:%d\n", peek(&stack));  // 输出1
    
    return 0;
}

顺序栈的优缺点很明显:

  • 优点:实现简单,数组访问速度快,入栈、出栈操作效率高;

  • 缺点:容量固定,元素数量超过容量就会溢出。如果要扩容,就得创建新数组,把原数组元素拷贝过去,会消耗额外时间。

2.2.2 链表实现(链式栈)

核心思路:用单链表存储元素,把链表的头节点当作栈顶。这样一来,入栈、出栈都能在头节点完成,不用遍历整个链表。先定义一个链表节点,再定义一个头节点(head),初始时头节点为空,就是空栈。入栈用头插法,把新节点插在头节点前面,成为新的头节点;出栈就删除头节点,把原头节点的下一个节点当作新的头节点。

核心操作和顺序栈一样:入栈(push)、出栈(pop)、查看栈顶元素(peek)、判断栈是否为空(isEmpty)。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构 typedef struct Node {
    int data;          // 节点数据
    struct Node *next; // 指向 next 节点的指针 } Node;

// 定义链式栈结构(仅需头节点) typedef struct {
    Node *head;  // 栈顶指针(指向头节点) } LinkedStack;

// 初始化链式栈 void initStack(LinkedStack *stack) {
    stack->head = NULL;  // 空栈,头节点为NULL }

// 判断栈是否为空 int isEmpty(LinkedStack *stack) {
    return stack->head == NULL; }

// 入栈操作(头插法) void push(LinkedStack *stack, int item) {
    // 创建新节点
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败,无法入栈!\n");
        return;
    }
    newNode->data = item;
    newNode->next = stack->head;  // 新节点指向原头节点
    stack->head = newNode;        // 新节点成为新的头节点(栈顶)
    printf("元素%d入栈成功\n", item); }

// 出栈操作,返回出栈元素,失败返回-1 int pop(LinkedStack *stack) {
    if (isEmpty(stack)) {
        printf("栈为空,无法出栈!\n");
        return -1;
    }
    Node *temp = stack->head;     // 暂存头节点(栈顶)
    int item = temp->data;        // 取出栈顶元素
    stack->head = stack->head->next;  // 头节点指向原头节点的下一个节点
    free(temp);  // 释放原头节点内存
    printf("元素%d出栈成功\n", item);
    return item; }

// 查看栈顶元素,不弹出,失败返回-1 int peek(LinkedStack *stack) {
    if (isEmpty(stack)) {
        printf("栈为空,无栈顶元素!\n");
        return -1;
    }
    return stack->head->data; }

// 测试链式栈 int main() {
    LinkedStack stack;
    initStack(&stack);
    
    push(&stack, 3);
    push(&stack, 4);
    printf("栈顶元素:%d\n", peek(&stack));  // 输出4
    pop(&stack);  // 弹出4
    printf("栈顶元素:%d\n", peek(&amp;stack));  // 输出3
    
    return 0; }

链式栈的优缺点和顺序栈正好相反:

  • 优点:容量灵活,不用考虑扩容,只要内存足够,就能一直入栈;

  • 缺点:每个节点都要额外存一个next指针,占用更多内存;访问效率比顺序栈略低,因为要通过指针找下一个节点。

2.3 栈的经典应用场景(结合实例)

栈的"后进先出"特性,让它在很多场景里都不可替代。下面给大家讲4个最经典、最常用的场景,结合实例帮大家理解。

场景1:括号匹配(核心场景)

这是面试里最常考的栈的应用,比如LeetCode第20题"有效括号"。题目是:给定一个只包含'(', ')', '{', '}', '[', ']'的字符串,判断这个字符串是否有效。有效字符串要满足:左括号必须用对应的右括号闭合,而且顺序要正确。

核心思路很简单:用栈存左括号,遍历字符串的时候,遇到左括号就入栈;遇到右括号,就弹出栈顶元素,判断两者是否匹配。如果遍历完,栈是空的,而且所有括号都匹配,那这个字符串就是有效的。

举个例子,字符串"({[]})",遍历过程是这样的:

  • 遇到'(', 入栈,栈里有['('];

  • 遇到'{', 入栈,栈里有['(', '{'];

  • 遇到'[', 入栈,栈里有['(', '{', '['];

  • 遇到']', 弹出栈顶的'[', 两者匹配,栈里剩下['(', '{'];

  • 遇到'}', 弹出栈顶的'{', 两者匹配,栈里剩下['('];

  • 遇到')', 弹出栈顶的'(', 两者匹配,栈为空,这个字符串有效。

场景2:浏览器前进后退功能

我们平时用浏览器,点击"前进""后退"就能切换页面,背后其实是两个栈在配合工作:

  • 栈1(后退栈):存当前页面之前访问过的页面。比如我们依次访问A→B→C,后退栈里就有[A, B];

  • 栈2(前进栈):存当前页面之后访问过的页面,比如后退之后再前进;

  • 操作逻辑:点击后退,就把当前页面(C)入前进栈,再从后退栈弹出栈顶元素(B),作为当前页面;点击前进,就把当前页面(B)入后退栈,再从前进栈弹出栈顶元素(C),作为当前页面。

场景3:表达式求值

我们平时算数学表达式,比如"3+4*2-6",计算机没法直接识别这种中缀表达式。它得先把表达式转换成后缀表达式(也叫逆波兰表达式),再用栈来计算。

核心思路:用栈存操作数,遍历后缀表达式,遇到数字就入栈;遇到运算符,就弹出栈顶的两个元素,做运算后,把结果再入栈。遍历完,栈里剩下的那个元素,就是表达式的结果。

举个例子,后缀表达式"3 4 2 * + 6 -",计算过程如下:

  • 3、4、2依次入栈,栈里有[3, 4, 2];

  • 遇到'',弹出2和4,算42=8,把8入栈,栈里有[3, 8];

  • 遇到'+',弹出8和3,算3+8=11,把11入栈,栈里有[11];

  • 遇到'-',弹出11和6,算11-6=5,栈里只剩[5],结果就是5。

场景4:函数调用栈

程序执行的时候,函数的调用顺序,就是用栈来维护的,这也是递归调用的本质。比如我们调用函数A,A又调用函数B,B又调用函数C。系统会把A、B依次入栈,等执行完C,C出栈,再执行B,B出栈后,再执行A,最后A出栈,程序就结束了。

比如用递归算阶乘(n! = n * (n-1)!),每次递归调用,都会把当前的n入栈,直到n=1(递归终止条件),然后依次出栈,计算每个n的阶乘值。

2.4 栈的常见问题与避坑点

用栈的时候,新手很容易踩坑。这里总结3个最常见的坑,帮大家避开:

  • 空栈pop、满栈push的异常处理:如果对空栈执行出栈,链式栈会出现空指针异常,顺序栈会出现数组越界;对满栈执行入栈,会出现溢出。所以实现push和pop方法时,一定要先判断栈是否为空、是否已满。

  • 顺序栈扩容的思路:如果用固定容量的顺序栈,元素超过容量就需要扩容。常用的方法是,创建一个容量更大的新数组(比如原容量的2倍),把原数组的元素拷贝过去,再用新数组当栈的存储容器。

  • 递归栈溢出问题:递归调用本质是用系统栈,如果递归深度太大(比如递归算10000的阶乘),系统栈会被占满,出现栈溢出。解决办法就是把递归改成循环,手动用栈模拟递归过程。

三、队列(Queue):先进先出(FIFO)的线性表

3.1 队列的核心定义与特性

同样给大家举个通俗的例子:排队买票,先排队的人先买票,后排队的人后买票,不能插队,也不能从队伍中间退出。这就是队列的核心逻辑。

从专业角度说,队列也是一种线性表,它只允许在表的一端(队尾rear)做插入操作,在另一端(队头front)做删除操作。队列遵循"先进先出(FIFO)"原则,意思就是,最先插进去的元素,最先被删除。

和栈类似,队列也有两个关键点:

  • 队列的插入(叫enqueue,入队)和删除(叫dequeue,出队)操作,时间复杂度都是O(1);

  • 队列的查找操作,时间复杂度是O(n),得从队头开始,一个个遍历到队尾。

举个例子:我们依次把1、2、3、4入队,队列里的元素从队头到队尾,就是1、2、3、4。这时候执行出队操作,最先弹出来的是1,再弹2,接着弹3,最后弹4。这就是"先进先出"。

3.2 队列的常见实现方式(附核心代码)

队列也有两种常用实现方式:数组实现(顺序队列)和链表实现(链式队列)。其中,顺序队列会有"假溢出"问题,需要改成循环队列,这也是面试的重点。下面详细给大家讲。

3.2.1 数组实现(顺序队列与循环队列)

顺序队列的核心思路:用数组当存储容器,定义两个指针,front(队头指针)和rear(队尾指针),初始值都是0。入队时,rear加1,把元素存在数组rear的位置;出队时,front加1,取出数组front位置的元素。

这里有个问题,就是假溢出。当rear达到数组末尾时,队列看起来是满的,但实际上数组前面可能还有空位置------因为有些元素已经出队,front指针已经后移了。这种情况就是假溢出,会浪费数组空间。

改进方法就是循环队列(也叫环形队列)。把数组看成一个环形,rear和front指针绕着数组循环移动,用取模运算(%)实现"循环"。这样一来,当rear达到数组末尾,而数组前面有空位置时,rear就会回到数组开头,避免假溢出。

循环队列的关键,是判断空和满。因为当front == rear时,既可能是队列空,也可能是队列满。常用的有两种判满方式:

  • 方式1:预留一个空位置,当(rear + 1) % capacity == front时,队列满;当front == rear时,队列空。

  • 方式2:用一个计数器count,记录队列里的元素个数,count == 0时为空,count == capacity时为满。

下面用方式1(预留空位置)实现循环队列,附C语言代码:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 10  // 队列默认容量

// 定义循环队列结构
typedef struct {
    int data[MAX_SIZE];  // 存储元素的数组
    int front;           // 队头指针
    int rear;            // 队尾指针
} CircularQueue;

// 初始化循环队列
void initQueue(CircularQueue *queue) {
    queue->front = 0;
    queue->rear = 0;
}

// 判断队列是否为空
int isEmpty(CircularQueue *queue) {
    return queue->front == queue->rear;
}

// 判断队列是否已满(预留一个空位置)
int isFull(CircularQueue *queue) {
    return (queue->rear + 1) % MAX_SIZE == queue->front;
}

// 入队操作
void enqueue(CircularQueue *queue, int item) {
    if (isFull(queue)) {
        printf("队列已满,无法入队!\n");
        return;
    }
    queue->data[queue->rear] = item;
    queue->rear = (queue->rear + 1) % MAX_SIZE;  // 循环移动rear
    printf("元素%d入队成功\n", item);
}

// 出队操作,返回出队元素,失败返回-1
int dequeue(CircularQueue *queue) {
    if (isEmpty(queue)) {
        printf("队列为空,无法出队!\n");
        return -1;
    }
    int item = queue->data[queue->front];
    queue->front = (queue->front + 1) % MAX_SIZE;  // 循环移动front
    printf("元素%d出队成功\n", item);
    return item;
}

// 获取队列中元素个数
int getSize(CircularQueue *queue) {
    return (queue->rear - queue->front + MAX_SIZE) % MAX_SIZE;
}

// 测试循环队列
int main() {
    CircularQueue queue;
    initQueue(&queue);
    
    enqueue(&queue, 1);
    enqueue(&queue, 2);
    enqueue(&queue, 3);
    enqueue(&queue, 4);
    printf("队列元素个数:%d\n", getSize(&queue));  // 输出4
    enqueue(&queue, 5);  // 队列未满,可入队(MAX_SIZE=10,预留1个空位置,实际可存9个)
    dequeue(&queue);  // 弹出1
    enqueue(&queue, 6);  // 入队6
    printf("队列元素个数:%d\n", getSize(&queue));  // 输出5
    
    return 0;
}

3.2.2 链表实现(链式队列)

核心思路:用单链表存储元素,定义两个指针,front(队头指针,指向链表头节点)和rear(队尾指针,指向链表尾节点)。初始时,front和rear都指向空,就是空队列。入队时,在链表尾部插入新节点,更新rear指针;出队时,删除链表头节点,更新front指针。

核心操作:入队(enqueue)、出队(dequeue)、判断队列是否为空(isEmpty)、获取队列大小(getSize)。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node {
    int data;          // 节点数据
    struct Node *next; // 指向next节点的指针
} Node;

// 定义链式队列结构
typedef struct {
    Node *front;  // 队头指针
    Node *rear;   // 队尾指针
    int size;     // 队列元素个数
} LinkedQueue;

// 初始化链式队列
void initQueue(LinkedQueue *queue) {
    queue->front = NULL;
    queue->rear = NULL;
    queue->size = 0;
}

// 判断队列是否为空
int isEmpty(LinkedQueue *queue) {
    return queue->size == 0;
}

// 获取队列元素个数
int getSize(LinkedQueue *queue) {
    return queue->size;
}

// 入队操作(尾插法)
void enqueue(LinkedQueue *queue, int item) {
    // 创建新节点
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败,无法入队!\n");
        return;
    }
    newNode->data = item;
    newNode->next = NULL;
    
    if (isEmpty(queue)) {
        // 空队列时,front和rear都指向新节点
        queue->front = newNode;
        queue->rear = newNode;
    } else {
        // 非空队列时,将新节点插入队尾
        queue->rear->next = newNode;
        queue->rear = newNode;
    }
    queue->size++;
    printf("元素%d入队成功\n", item);
}

// 出队操作,返回出队元素,失败返回-1
int dequeue(LinkedQueue *queue) {
    if (isEmpty(queue)) {
        printf("队列为空,无法出队!\n");
        return -1;
    }
    Node *temp = queue->front;  // 暂存队头节点
    int item = temp->data;       // 取出队头元素
    queue->front = queue->front->next;  // 队头指针后移
    free(temp);  // 释放原队头节点内存
    queue->size--;
    
    // 出队后为空队列,将rear置为NULL
    if (isEmpty(queue)) {
        queue->rear = NULL;
    }
    
    printf("元素%d出队成功\n", item);
    return item;
}

// 测试链式队列
int main() {
    LinkedQueue queue;
    initQueue(&queue);
    
    enqueue(&queue, 10);
    enqueue(&queue, 20);
    enqueue(&queue, 30);
    printf("队列元素个数:%d\n", getSize(&queue));  // 输出3
    dequeue(&queue);  // 弹出10
    printf("队列元素个数:%d\n", getSize(&queue));  // 输出2
    printf("队头元素:%d\n", queue.front->data);  // 输出20
    
    return 0;
}

链式队列的优缺点:

  • 优点:容量灵活,没有假溢出问题,只要内存足够,就能一直入队;实现也简单,不用处理循环逻辑。

  • 缺点:每个节点要额外存一个next指针,占用更多内存;访问效率比顺序队列略低。

3.2.3 拓展:双端队列(Deque)

双端队列(简称Deque)是队列的拓展形式,它允许在队头和队尾同时做插入和删除操作,兼顾了栈和队列的特性。

核心特性:队头(front)和队尾(rear)都能执行入队和出队操作。既可以当栈用(只操作队头或队尾),也可以当普通队列用(队尾入队、队头出队)。

常见应用:

  • 滑动窗口问题:比如LeetCode第239题"滑动窗口最大值",用双端队列维护窗口内的最大值,队头存当前窗口的最大值,队尾用来删除比当前元素小的元素,提升效率。

  • LRU缓存淘汰策略(简化版):LRU的核心是"淘汰最近最少使用的元素",用双端队列能快速实现元素的插入、删除和移动。

C语言里没有内置的双端队列,大家可以自己用链表实现,核心就是在队头和队尾都添加插入、删除的操作,和前面讲的链式栈、链式队列逻辑类似。

3.3 队列的经典应用场景(结合实例)

队列的"先进先出"特性,适合用来处理"按顺序执行"的场景。下面给大家讲4个最常用的场景,结合实际开发案例,帮大家理解。

场景1:消息队列(MQ)

消息队列是后端开发里常用的异步通信组件,核心作用是"解耦、削峰、异步",它的底层就是队列的实现。比如用户提交订单后,系统要处理支付、发通知、更新库存等操作。如果同步处理,用户要等很久;这时候就可以用消息队列,把这些操作封装成消息,入队后异步处理。

流程很简单:用户提交订单 → 订单系统把消息(支付、通知等)入队 → 各个处理模块从队列里依次取消息,执行对应操作 → 处理完,消息出队。这样既提升了用户体验,也能避免某个模块故障,导致整个系统崩溃。

场景2:任务调度(生产者-消费者模型)

多线程开发里,生产者-消费者模型很经典。其中队列就是"缓冲区",用来存生产者产生的任务,消费者从队列里依次取任务执行,避免生产者和消费者直接绑定。

举个例子,一个爬虫系统:生产者线程负责抓取网页数据,把抓取到的任务(比如解析网页、存储数据)入队;消费者线程从队列里取任务,执行解析和存储。这样能平衡两者的速度,避免生产者生产太快导致内存溢出,或者消费者消费太快导致空闲。

场景3:BFS算法(广度优先搜索)

BFS(广度优先搜索)是遍历树、图的一种算法,核心就是用队列存当前层的节点。先访问当前层的所有节点,再访问下一层的节点,确保"逐层遍历"。

比如遍历一棵二叉树的层序遍历(从上到下、从左到右),就是用队列实现的:

  • 把根节点入队;

  • 弹出队头节点,访问它,再把它的左、右子节点(如果有)入队;

  • 重复第二步,直到队列为空,就完成遍历了。

#场景4:日常场景(排队系统)

生活里很多排队场景,背后都是队列的逻辑。比如医院叫号系统、银行排队系统、打印机队列等。以医院叫号为例,患者挂号后,会拿到一个号码,系统把号码入队,医生依次叫号(出队),确保患者按挂号顺序就诊,不插队、不混乱。

3.4 队列的常见问题与避坑点

和栈一样,队列使用时也有一些常见的坑,尤其是循环队列的实现,新手很容易出错。这里总结3个重点:

  • 顺序队列的假溢出问题:大家一定要记住,普通顺序队列会有假溢出,实际开发中尽量用循环队列,避免浪费数组空间。

  • 链式队列的空满判断:链式队列判断空,就是size == 0(或者front == NULL),它没有满的问题(除非内存不足)。但要注意,出队后如果队列为空,要把rear置为NULL,避免后续操作出错。

  • 循环队列的判满技巧:循环队列中,front和rear相等时,没法区分是空还是满。一定要预留一个空位置,用(rear + 1) % capacity == front来判满,这是面试常考的考点。

四、栈和队列的对比与总结

4.1 核心区别(表格对比)

栈和队列都是线性表,都有数组和链表两种实现方式,但核心逻辑和适用场景差别很大。下面用表格清晰对比,大家一看就懂:

对比维度 栈(Stack) 队列(Queue)
访问规则 后进先出(LIFO) 先进先出(FIFO)
操作端 仅栈顶(top)可插入、删除 队尾(rear)插入,队头(front)删除
实现难度 较简单,不用处理循环或双指针同步 较复杂,循环队列要处理判满/判空,链式队列要维护双指针
适用场景 回溯、递归、括号匹配、表达式求值、浏览器前进后退 异步通信(消息队列)、任务调度、BFS算法、排队系统

4.3 学习总结

学习栈和队列,核心是理解"受限访问"的设计思想------通过限制元素的操作位置,让逻辑更简单、效率更高。很多初学者觉得它们太简单,没必要深入学,但其实它们是后续学复杂数据结构和算法的基础。比如DFS依赖栈,BFS依赖队列,没有扎实的栈和队列基础,后续学习会很吃力。

关于实现方式的选择,给大家一个简单的建议:

  • 如果元素数量固定、追求访问效率,就选顺序栈/循环队列;

  • 如果元素数量不固定、追求容量灵活,就选链式栈/链式队列;

  • 如果需要同时操作两端元素,就选双端队列(Deque)。

最后提醒大家:学习数据结构,不只是理解定义和实现,更要掌握应用场景,多做实战题,才能真正把知识变成自己的能力。

五、实战练习(提升博客实用性)

为了帮大家巩固所学知识,下面推荐4道经典的栈和队列实战题,都来自LeetCode,难度适中,适合初学者上手。每道题给大家讲核心思路,再附上C语言简化代码,大家可以自己动手实现完整代码。

5.1 栈的实战题

题目1:有效括号(LeetCode 20)

题目描述:给定一个只包含'(', ')', '{', '}', '[', ']'的字符串s,判断s是否有效。有效字符串要满足:左括号必须用相同类型的右括号闭合,而且顺序要正确。

思路提示:用栈存左括号,遍历字符串,遇到左括号就入栈;遇到右括号,弹出栈顶元素,判断两者是否匹配。遍历完,栈为空,就是有效的。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_STACK_SIZE 100

// 判断括号是否匹配
int isMatch(char left, char right) {
    if ((left == '(' && right == ')') || 
        (left == '{' && right == '}') || 
        (left == '[' && right == ']')) {
        return 1;
    }
    return 0;
}

// 有效括号判断函数
int isValid(char *s) {
    char stack[MAX_STACK_SIZE];
    int top = -1;  // 栈顶指针
    
    int len = strlen(s);
    for (int i = 0; i < len; i++) {
        char c = s[i];
        // 遇到左括号,入栈
        if (c == '(' || c == '{' || c == '[') {
            if (top < MAX_STACK_SIZE - 1) {
                stack[++top] = c;
            } else {
                return 0;  // 栈满,无效
            }
        } else {
            // 遇到右括号,判断匹配
            if (top == -1) {
                return 0;  // 空栈,无左括号匹配
            }
            char topChar = stack[top--];
            if (!isMatch(topChar, c)) {
                return 0;
            }
        }
    }
    // 遍历结束,栈为空则有效
    return top == -1;
}

// 测试
int main() {
    char s1[] = "({[]})";
    char s2[] = "([)]";
    printf("%d\n", isValid(s1));  // 输出1(有效)
    printf("%d\n", isValid(s2));  // 输出0(无效)
    return 0;
}

题目2:最小栈(LeetCode 155)

题目描述:设计一个支持push、pop、top操作,并且能在常数时间内找到最小元素的栈。

思路提示:用两个栈,一个普通栈存元素,一个辅助栈存当前栈中的最小值。push时,辅助栈同步push当前的最小值;pop时,辅助栈也同步pop。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_STACK_SIZE 100

// 定义最小栈结构(两个栈:普通栈+辅助栈)
typedef struct {
    int stack[MAX_STACK_SIZE];    // 普通栈,存元素
    int minStack[MAX_STACK_SIZE]; // 辅助栈,存当前最小值
    int top;                      // 栈顶指针(两个栈共用)
} MinStack;

// 初始化最小栈
MinStack* minStackCreate() {
    MinStack *stack = (MinStack *)malloc(sizeof(MinStack));
    stack->top = -1;
    return stack;
}

// 入栈操作
void minStackPush(MinStack* obj, int val) {
    if (obj->top < MAX_STACK_SIZE - 1) {
        obj->top++;
        obj->stack[obj->top] = val;
        // 辅助栈push当前最小值
        if (obj->top == 0) {
            obj->minStack[obj->top] = val;
        } else {
            int min = obj->minStack[obj->top - 1] < val ? obj->minStack[obj->top - 1] : val;
            obj->minStack[obj->top] = min;
        }
    }
}

// 出栈操作
void minStackPop(MinStack* obj) {
    if (obj->top >= 0) {
        obj->top--;
    }
}

// 获取栈顶元素
int minStackTop(MinStack* obj) {
    if (obj->top >= 0) {
        return obj->stack[obj->top];
    }
    return INT_MIN;  // 异常返回
}

// 获取当前最小值
int minStackGetMin(MinStack* obj) {
    if (obj->top >= 0) {
        return obj->minStack[obj->top];
    }
    return INT_MIN;  // 异常返回
}

// 释放栈内存
void minStackFree(MinStack* obj) {
    free(obj);
}

// 测试
int main() {
    MinStack* obj = minStackCreate();
    minStackPush(obj, -2);
    minStackPush(obj, 0);
    minStackPush(obj, -3);
    printf("%d\n", minStackGetMin(obj));  // 输出-3
    minStackPop(obj);
    printf("%d\n", minStackTop(obj));     // 输出0
    printf("%d\n", minStackGetMin(obj));  // 输出-2
    minStackFree(obj);
    return 0;
}

5.2 队列的实战题

题目3:用栈实现队列(LeetCode 232)

题目描述:仅使用两个栈,实现一个先进先出的队列。队列要支持push、pop、peek、empty这四个操作。

思路提示:用两个栈,栈1用来入队,栈2用来出队。当栈2为空时,把栈1的所有元素弹出,压入栈2,这时栈2的栈顶就是队列的队头。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_STACK_SIZE 100

// 定义栈结构
typedef struct {
    int data[MAX_STACK_SIZE];
    int top;
} Stack;

// 初始化栈
void stackInit(Stack *stack) {
    stack->top = -1;
}

// 栈是否为空
int stackIsEmpty(Stack *stack) {
    return stack->top == -1;
}

// 入栈
void stackPush(Stack *stack, int val) {
    if (stack->top < MAX_STACK_SIZE - 1) {
        stack->data[++stack->top] = val;
    }
}

// 出栈,返回出栈元素
int stackPop(Stack *stack) {
    if (stackIsEmpty(stack)) {
        return INT_MIN;
    }
    return stack->data[stack->top--];
}

// 查看栈顶元素
int stackPeek(Stack *stack) {
    if (stackIsEmpty(stack)) {
        return INT_MIN;
    }
    return stack->data[stack->top];
}

// 用两个栈实现队列
typedef struct {
    Stack stack1;  // 用于入队
    Stack stack2;  // 用于出队
} MyQueue;

// 初始化队列
MyQueue* myQueueCreate() {
    MyQueue *queue = (MyQueue *)malloc(sizeof(MyQueue));
    stackInit(&queue->stack1);
    stackInit(&queue->stack2);
    return queue;
}

// 入队
void myQueuePush(MyQueue* obj, int x) {
    stackPush(&obj->stack1, x);
}

// 出队
int myQueuePop(MyQueue* obj) {
    if (stackIsEmpty(&obj->stack2)) {
        // 栈2为空,将栈1元素全部压入栈2
        while (!stackIsEmpty(&obj->stack1)) {
            int val = stackPop(&obj->stack1);
            stackPush(&obj->stack2, val);
        }
    }
    return stackPop(&obj->stack2);
}

// 查看队头元素
int myQueuePeek(MyQueue* obj) {
    if (stackIsEmpty(&obj->stack2)) {
        while (!stackIsEmpty(&obj->stack1)) {
            int val = stackPop(&obj->stack1);
            stackPush(&obj->stack2, val);
        }
    }
    return stackPeek(&obj->stack2);
}

// 判断队列是否为空
int myQueueEmpty(MyQueue* obj) {
    return stackIsEmpty(&obj->stack1) && stackIsEmpty(&obj->stack2);
}

// 释放队列内存
void myQueueFree(MyQueue* obj) {
    free(obj);
}

// 测试用栈实现的队列
int main() {
    MyQueue* obj = myQueueCreate();
    myQueuePush(obj, 1);
    myQueuePush(obj, 2);
    printf("队头元素:%d\n", myQueuePeek(obj));  // 输出1
    printf("出队元素:%d\n", myQueuePop(obj));  // 输出1
    printf("队列是否为空:%d\n", myQueueEmpty(obj));  // 输出0(非空)
    myQueueFree(obj);
    return 0;
}
相关推荐
小年糕是糕手2 小时前
【35天从0开始备战蓝桥杯 -- 刷题包】
c语言·jvm·数据结构·c++·算法·蓝桥杯
C雨后彩虹2 小时前
最小矩阵宽度
java·数据结构·算法·华为·面试
liuyao_xianhui2 小时前
动态规划_最长递增子序列_C++
java·开发语言·数据结构·c++·算法·链表·动态规划
XW01059992 小时前
5-11字典合并
数据结构·python·算法
qyzm2 小时前
Codeforces Round 927 (Div. 3)
数据结构·python·算法
自信150413057592 小时前
数据结构之二叉树算法题
c语言·数据结构·算法
尽兴-2 小时前
从零到精通:Redis 7 核心数据结构实战与单机部署指南
数据结构·数据库·redis·部署·redis7
见叶之秋2 小时前
【数据结构】详解双向链表
数据结构·链表