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