🔥 数据结构修炼场 🔥
💥 栈与队列 · 终极试炼 💥
🚀 理论已加载完毕,代码之魂觉醒时刻!
⚡️ 是时候用实战
点燃你的算法之力了------
「题目风暴,来袭!」
(握紧键盘,接受挑战吧 ✨)
"Talk is cheap. Show me the code."
------ Linus Torvalds
1.有效的括号
题目:
画图分析:
具体思路如下:
- 使用栈来处理括号匹配:栈是一种后进先出(LIFO)的数据结构,非常适合处理括号匹配问题。当遇到左括号([、(、{)时,将其压入栈中;当遇到右括号(]、)、})时,从栈中弹出一个左括号进行匹配。
- 遍历字符串:从字符串的第一个字符开始,逐个字符进行处理。
- 处理左括号:如果当前字符是左括号,将其压入栈中。
- 处理右括号:如果当前字符是右括号,检查栈是否为空。如果栈为空,说明没有对应的左括号,字符串无效;如果栈不为空,弹出栈顶元素,检查栈顶元素是否与当前右括号匹配。如果不匹配,字符串无效。
- 检查栈是否为空:遍历完字符串后,如果栈为空,说明所有的左括号都有对应的右括号,字符串有效;否则,字符串无效。
代码分析:
c
bool isValid(char*s)
{
//定义一个栈
Stack st;
StackInit(&st);
//初始化一个布尔变量 ret 为 true,用于记录字符串是否有效
bool ret = true;
// while 循环遍历字符串
while (*s != '\0')
{
//处理左括号
if (*s == '[' || *s == '(' || *s == '{')
{
StackPush(&st, *s);
++s;//如果当前字符是左括号,将其压入栈中,并将指针 s 指向下一个字符
}
//处理右括号
else
{
//先检查栈是否为空
if (StackEmpty(&st))//这里为空,说明没有对应的左括号
{
ret = false;
break;//跳出循环
}
//如果栈不为空,获取栈顶元素
char top = StackTop(&st);
//检查栈顶元素是否与当前右括号匹配
//不匹配,将 ret 置为 false 并跳出循环
if (*s == ']' && top != '[')
//当当前字符是右中括号 ] 时,检查栈顶元素是否为左中括号 [。
//如果不是,说明括号不匹配,字符串无效,将 ret 设为 false 并跳出循环。
{
ret = false;
break;
}
if (*s == ')' && top != '(')
{
ret = false;
break;
}
if (*s == '}' && top != '{')
{
ret = false;
break;
}
//匹配,弹出栈顶元素,并将指针 s 指向下一个字符
StackPop(&st);
++s;
}
}
//检查栈是否为空
if (*s == '\0')
{
ret = StackEmpty(&st);
}
//遍历完字符串后,如果栈为空,说明所有的左括号都有对应的右括号,将 ret 置为 true;
//否则,将 ret 置为 false
StackDestory(&st);
return ret;
}
int main()
{
//定义一个测试字符串 test
char test[] = "{[()]}";
//调用 isValid 函数判断该字符串是否有效,并将结果存储在 result 中
bool result= isValid(test);
if (result)
{
printf("字符串有效\n");
}
else
{
printf("字符串无效\n");
}
return 0;
}
问题:为什么要检查栈是否为空🤔🤔🤔
if (*s == '\0')
{
ret = StackEmpty(&st);
}
原因:
在遍历完字符串后,还需要检查栈是否为空。因为如果字符串是有效的,那么所有的左括号都应该有对应的右括号与之匹配,即栈中不应该有剩余的左括号。所以使用StackEmpty(&st)
来检查栈是否为空,如果栈为空,说明所有括号都匹配成功,将ret
设为 true
;如果栈不为空,说明还有左括号没有对应的右括号,字符串无效,将ret
设为 false
。
综上所述,这一步是为了确保字符串中所有的左括号都有对应的右括号,避免出现多余的左括号
2.用队列实现栈
题目:
解题思路
栈遵循后进先出(LIFO)的原则,而队列遵循先进先出(FIFO)的原则。要利用两个队列模拟栈,关键在于合理运用两个队列的入队和出队操作,以此实现栈的入栈、出栈、获取栈顶元素以及判断栈是否为空等操作。
- 入栈操作 :把新元素添加到
非空
的队列里。若两个队列都为空
,可任选一个队列添加元素。 - 出栈操作 :把
非空
队列里除最后一个元素之外
的所有元素
转移到另一个空队列中,接着将最后一个元素出队,这个元素就是栈顶
元素。 - 获取栈顶元素:直接获取非空队列的队尾元素。
- 判断栈是否为空:当两个队列都为空时,栈为空。
代码分析:
c
// 定义用两个队列模拟的栈结构体
typedef struct
{
Queue q1;
Queue q2;
} MyStack;
// 创建栈
MyStack* myStackCreate()
{
MyStack* st = (MyStack*)malloc(sizeof(MyStack));
if (st == NULL)
{
printf("内存分配失败\n");
exit(-1);
}
//调用 QueueInit 函数对 q1 和 q2 两个队列进行初始化
//&st->q1 表示取 st 所指向的结构体中 q1 成员的地址
QueueInit(&st->q1);
QueueInit(&st->q2);
return st;
//回指向新创建的 MyStack 结构体的指针
}
// 入栈
void myStackPush(MyStack* obj, int x)//MyStack* obj 是指向要操作的栈的指针
{
//检查 q1 队列是否为空
if (!QueueEmpty(&obj->q1)) //表示 q1 队列不为空
{
QueuePush(&obj->q1, x);//若 q1 队列不为空,就把元素 x 入队到 q1 中
}
else
{
QueuePush(&obj->q2, x);//若 q1 队列为空,将元素 x 入队到 q2 中
}
}
// 出栈
int myStackPop(MyStack* obj)
{
//初始化两个指针 emptyQ 和 nonemptyQ,分别指向 q1 和 q2 队列
Queue* emptyQ = &obj->q1;
Queue* nonemptyQ = &obj->q2;
//检查 q1 队列是否为空,若不为空,交换 emptyQ 和 nonemptyQ 的指向
if (!QueueEmpty(&obj->q1))
{
emptyQ = &obj->q2;
nonemptyQ = &obj->q1;
}
//当非空队列中的元素数量大于 1 时,执行循环
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))//检查 q1 队列是否为空,若不为空
{
return QueueBack(&obj->q1);//返回 q1 队列的队尾元素
}
else
{
return QueueBack(&obj->q2);//若 q1 队列为空,返回 q2 队列的队尾元素
}
}
// 判断栈是否为空
bool myStackEmpty(MyStack* obj)
{
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
//当且仅当 q1 和 q2 两个队列都为空时,返回 true,否则返回 false
}
// 销毁栈
void myStackFree(MyStack* obj)
{
if (obj == NULL)
{
return;
}
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}
int main()
{
MyStack* st = myStackCreate();
if (st == NULL)
{
return 1;检查栈是否创建成功,若失败,返回 1 表示程序异常退出
}
//依次将元素 1、2、3 入栈
myStackPush(st, 1);
myStackPush(st, 2);
myStackPush(st, 3);
while (!myStackEmpty(st))
{
printf("栈顶元素为: %d\n", myStackTop(st));
myStackPop(st);
}
myStackFree(st);
return 0;
}
运行结果:

3.用栈实现队列
题目:

代码分析:
c
typedef struct
{
//两个栈
Stack _pushST;//注意这里很容易错
Stack _popST;//要空格表示两个成员变量是栈类型
}MyQueue;
MyQueue* mQueueCreate()
{
MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&q->_pushST);
StackInit(&q->_popST);
return q;
}
//入栈
void myQueuePush(MyQueue* obj, int x)
{
StackPush(&obj->_pushST, x);
}
//出栈
int myQueuePop(MyQueue* obj)
{
int front = myQueuePeek(obj);
StackPop(&obj->_popST);
return front;
}
int myQueuePeek(MyQueue* obj)
{
//如果是非空的
if (!StackEmpty(&obj->_popST))
{
return StackTop(&obj->_popST);//返回_pushST队头的数据
}
//为空就要把另外一个栈里面的数据导过来
else
{
while (!StackEmpty(&obj->_pushST))
{
StackPush(&obj->_popST, StackTop(&obj->_pushST));
StackPop(&obj->_pushST);
//将_pushST里面的数据导到_popST里面
}
return StackTop(&obj->_popST);
}
}
//判断是不是为空
//判断队列是否为空函数
bool myQueueEmpty(MyQueue* obj)
{
return StackEmpty(&obj->_popST) && StackEmpty(&obj->_pushST);
}
void myQueueFree(MyQueue* obj)
{
StackDestory(&obj->_pushST);//调用 StackDestory 函数分别销毁 _pushST 和 _popST 栈,释放栈所占用的内存
StackDestory(&obj->_popST);
free(obj);//最后使用 free 函数释放 MyQueue 结构体本身所占用的内存
}
//栈是后进先出;队列是先进先出
int main()
{
MyQueue* queue = mQueueCreate();
// 入队操作
myQueuePush(queue, 1);
myQueuePush(queue, 2);
myQueuePush(queue, 3);
// 获取队头元素
//注意:在这段用两个栈模拟队列的代码里,获取队头元素主要是从 _popST 栈获取
printf("队头元素: %d\n", myQueuePeek(queue));
// 出队操作
printf("出队元素: %d\n", myQueuePop(queue));
printf("出队元素: %d\n", myQueuePop(queue));
// 再次入队
myQueuePush(queue, 4);
// 继续出队
while (!myQueueEmpty(queue))
{
printf("出队元素: %d\n", myQueuePop(queue));
}
// 释放队列内存
myQueueFree(queue);
return 0;
}
详细解析:
- 结构体定义
c
typedef struct
{
//两个栈
Stack _pushST; // 用于入队操作的栈
Stack _popST; // 用于出队操作的栈
}MyQueue;
- 定义了一个名为
MyQueue
的结构体,该结构体包含两个Stack
类型的成员变量_pushST
和_popST
。 _pushST
栈主要用于元素的入队
操作,新元素会被压入这个栈。_popST
栈用于元素的出队
操作,当需要出队时,会从这个栈中弹出元素。
- 队列创建函数
c
MyQueue* mQueueCreate()
{
MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&q->_pushST);
StackInit(&q->_popST);
return q;
}
mQueueCreate
函数的作用是创建一个新的MyQueue
实例。- 首先使用
malloc
函数为MyQueue
结构体分配内存空间。 - 然后调用
StackInit
函数分别对_pushST
和_popST
栈进行初始化。 - 最后返回指向新创建的
MyQueue
实例的指针。
- 入队操作函数
c
//入栈
void myQueuePush(MyQueue* obj, int x)
{
StackPush(&obj->_pushST, x);
}
myQueuePush
函数用于将一个整数x
插入到队列中。- 直接调用
StackPush
函数将元素x
压入_pushST
栈,因为_pushST
栈专门用于入队操作
- 出队操作函数
c
//出栈
int myQueuePop(MyQueue* obj)
{
int front = myQueuePeek(obj);
StackPop(&obj->_popST);
return front;
}
myQueuePop
函数用于从队列中移除
并返回
队头元素。- 首先调用
myQueuePeek
函数获取队头
元素的值,并将其存储在变量front
中。 - 然后调用
StackPop
函数从_popST
栈中弹出队头元素。 - 最后返回
队头
元素的值。
- 获取队头元素函数
c
int myQueuePeek(MyQueue* obj)
{
//如果是非空的
if (!StackEmpty(&obj->_popST))
{
return StackTop(&obj->_popST); // 返回_pushST队头的数据
}
//为空就要把另外一个栈里面的数据导过来
else
{
while (!StackEmpty(&obj->_pushST))
{
StackPush(&obj->_popST, StackTop(&obj->_pushST));
StackPop(&obj->_pushST);
//将_pushST里面的数据导到_popST里面
}
return StackTop(&obj->_popST);
}
}
myQueuePeek
函数用于返回
队列的队头
元素,但不
将其从队列中移除
。- 首先检查
_popST
栈是否为空。如果不为空 ,直接调用StackTop
函数返回_popST
栈的栈顶 元素,因为_popST
栈的栈顶元素就是队列的队头
元素。 - 如果
_popST
栈为空 ,则需要将_pushST
栈中的所有元素 依次弹出并压入_popST
栈中,这样_popST
栈中的元素顺序就与队列的顺序一致了。 - 最后返回
_popST
栈的栈顶元素。
代码运行:

4.设计循环队列
题目:
画图分析:
代码分析:
c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef struct
{
int* _a;//是一个整数指针,用于指向存储队列元素的动态数组
int _front;//表示队列的队头位置
int _rear;//表示队列的队尾位置
int _k;//表示队列的最大容量
}MyCircularQueue;
//初始化
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//为 MyCircularQueue 结构体分配内存
q->_a = (int*)malloc(sizeof(int) * (k + 1));//多开一个空间
//为存储队列元素的数组分配 k + 1 个整数的空间,多开一个空间是为了区分队列满和队列空的情况
q->_front = 0;
q->_rear = 0;
q->_k = k;//记录队列的最大容量 k
return q;
}
//空的
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
return obj->_front == obj->_rear;
//当 _front 和 _rear 相等时,队列为空
}
//满的
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
return (obj->_rear + 1) % (obj->_k + 1) == obj->_front;
}
//插入数据
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
//如果是满的
if (myCircularQueueIsFull(obj))
{
return false;
}
//入数据
obj->_a[obj->_rear] = value;//将元素插入到队尾位置
obj->_rear++;
obj->_rear %= (obj->_k + 1);//更新 _rear 的位置,使用取模运算实现循环
return true;
}
//删除数据
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
//为空
if (myCircularQueueIsEmpty(obj))
{
return false;
}
++obj->_front;
obj->_front %= (obj->_k + 1);
return true;
}
//获取队头的数据
int myCircularQueueFront(MyCircularQueue* obj)
{
//如果是空的
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
else
{
return obj->_a[obj->_front];//返回队头元素
}
}
//获取队尾的数据
int myCircularQueueRear(MyCircularQueue* obj)
{
//如果是空的
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
else
{
int tail = obj->_rear - 1;//计算队尾元素的位置,考虑循环的情况
if (tail == -1)
{
tail = obj->_k;
}
return obj->_a[tail];//返回队尾元素
}
}
//释放
void myCircularQueueFree(MyCircularQueue* obj)
{
free(obj->_a);
free(obj);
}
int main()
{
MyCircularQueue* queue = myCircularQueueCreate(3);
myCircularQueueEnQueue(queue, 1);
myCircularQueueEnQueue(queue, 2);
myCircularQueueEnQueue(queue, 3);
printf("Front: %d\n", myCircularQueueFront(queue));//打印队头
printf("Rear: %d\n", myCircularQueueRear(queue));//打印队尾
myCircularQueueDeQueue(queue);//队头元素 1 出队
printf("Front after dequeue: %d\n", myCircularQueueFront(queue));//那就2变成了队头
myCircularQueueFree(queue);
return 0;
}
运行结果:

🤔🤔🤔
思考一个问题
:
这里是怎么使用取模运算实现循环的
入队操作中的取模运算
c
// 插入数据
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
// 如果是满的
if (myCircularQueueIsFull(obj))
{
return false;
}
// 入数据
obj->_a[obj->_rear] = value;
obj->_rear++;
obj->_rear %= (obj->_k + 1);
return true;
}
- 元素入队:
obj->_a[obj->_rear] = value;
把新元素value
存到_rear
所指向的位置。 - 移动队尾指针:
obj->_rear++;
让_rear
指针向后移动一位。 - 取模运算实现循环:
obj->_rear %= (obj->_k + 1);
对_rear
进行取模运算,其除数为_k + 1
(_k 代表队列的最大容量)。
示例说明:
- 假设队列的最大容量
_k
为3
,那么数组的大小就是_k + 1 = 4
,数组下标范围是0
到3
。 - 初始时,
_rear = 0
,插入元素后,_rear
变为1
。 - 持续插入元素,当
_rear
变为3
时,再插入元素,_rear
先加1
变成4
,然后进行取模运算4 % 4 = 0
,这就使得_rear
又回到了数组的起始位置,达成了循环的效果。
出队操作中的取模运算
c
// 删除数据
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
// 为空
if (myCircularQueueIsEmpty(obj))
{
return false;
}
++obj->_front;
obj->_front %= (obj->_k + 1);
return true;
}
- 移动队头指针:
++obj->_front;
让_front
指针向后移动一位。 - 取模运算实现循环:
obj->_front %= (obj->_k + 1)
; 对_front
进行取模运算,除数同样是_k + 1
。
示例说明:
- 同样假设队列的最大容量
_k 为 3
,数组大小为4
,下标范围是0
到3
。 - 初始时,
_front = 0
,出队操作后,_front
变为1
。 - 不断进行出队操作,当
_front
变为3
时,再出队,_front
先加1
变成4
,接着进行取模运算4 % 4 = 0
,_front
又回到了数组的起始位置,实现了循环
。
总结:在循环队列中,取模运算能够把指针的移动范围限制在数组的有效下标范围之内,当指针移动到数组末尾时,通过取模运算可以让指针回到数组的起始位置,从而实现循环的效果。这样就能高效地利用数组空间,避免 "假溢出" 问题。
🎉🎉🎉
在这里本章就结束啦~
我们下期见~
