🔥 数据结构修炼场 🔥
💥 栈与队列 · 终极试炼 💥
🚀 理论已加载完毕,代码之魂觉醒时刻!
⚡️ 是时候用实战
点燃你的算法之力了——
「题目风暴,来袭!」
(握紧键盘,接受挑战吧 ✨)
“Talk is cheap. Show me the code.”
—— Linus Torvalds
1.有效的括号
题目:
画图分析:
具体思路如下:
- 使用栈来处理括号匹配:栈是一种后进先出(LIFO)的数据结构,非常适合处理括号匹配问题。当遇到左括号([、(、{)时,将其压入栈中;当遇到右括号(]、)、})时,从栈中弹出一个左括号进行匹配。
- 遍历字符串:从字符串的第一个字符开始,逐个字符进行处理。
- 处理左括号:如果当前字符是左括号,将其压入栈中。
- 处理右括号:如果当前字符是右括号,检查栈是否为空。如果栈为空,说明没有对应的左括号,字符串无效;如果栈不为空,弹出栈顶元素,检查栈顶元素是否与当前右括号匹配。如果不匹配,字符串无效。
- 检查栈是否为空:遍历完字符串后,如果栈为空,说明所有的左括号都有对应的右括号,字符串有效;否则,字符串无效。
代码分析:
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 置为 falseStackDestory(&st);return ret;
}int main()
{//定义一个测试字符串 testchar 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)的原则。要利用两个队列模拟栈,关键在于合理运用两个队列的入队和出队操作,以此实现栈的入栈、出栈、获取栈顶元素以及判断栈是否为空等操作。
- 入栈操作:把新元素添加到
非空
的队列里。若两个队列都为空
,可任选一个队列添加元素。 - 出栈操作:把
非空
队列里除最后一个元素之外
的所有元素
转移到另一个空队列中,接着将最后一个元素出队,这个元素就是栈顶
元素。 - 获取栈顶元素:直接获取非空队列的队尾元素。
- 判断栈是否为空:当两个队列都为空时,栈为空。
代码分析:
// 定义用两个队列模拟的栈结构体
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.用栈实现队列
题目:
代码分析:
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;
}
详细解析:
- 结构体定义
typedef struct
{//两个栈Stack _pushST; // 用于入队操作的栈Stack _popST; // 用于出队操作的栈
}MyQueue;
- 定义了一个名为
MyQueue
的结构体,该结构体包含两个Stack
类型的成员变量_pushST
和_popST
。 _pushST
栈主要用于元素的入队
操作,新元素会被压入这个栈。_popST
栈用于元素的出队
操作,当需要出队时,会从这个栈中弹出元素。
- 队列创建函数
MyQueue* mQueueCreate()
{MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));StackInit(&q->_pushST);StackInit(&q->_popST);return q;
}
mQueueCreate
函数的作用是创建一个新的MyQueue
实例。- 首先使用
malloc
函数为MyQueue
结构体分配内存空间。 - 然后调用
StackInit
函数分别对_pushST
和_popST
栈进行初始化。 - 最后返回指向新创建的
MyQueue
实例的指针。
- 入队操作函数
//入栈
void myQueuePush(MyQueue* obj, int x)
{StackPush(&obj->_pushST, x);
}
myQueuePush
函数用于将一个整数x
插入到队列中。- 直接调用
StackPush
函数将元素x
压入_pushST
栈,因为_pushST
栈专门用于入队操作
- 出队操作函数
//出栈
int myQueuePop(MyQueue* obj)
{int front = myQueuePeek(obj);StackPop(&obj->_popST);return front;
}
myQueuePop
函数用于从队列中移除
并返回
队头元素。- 首先调用
myQueuePeek
函数获取队头
元素的值,并将其存储在变量front
中。 - 然后调用
StackPop
函数从_popST
栈中弹出队头元素。 - 最后返回
队头
元素的值。
- 获取队头元素函数
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.设计循环队列
题目:
画图分析:
代码分析:
#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;//记录队列的最大容量 kreturn 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;
}
运行结果:
🤔🤔🤔
思考一个问题
:
这里是怎么使用取模运算实现循环的
入队操作中的取模运算
// 插入数据
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
又回到了数组的起始位置,达成了循环的效果。
出队操作中的取模运算
// 删除数据
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
又回到了数组的起始位置,实现了循环
。
总结:在循环队列中,取模运算能够把指针的移动范围限制在数组的有效下标范围之内,当指针移动到数组末尾时,通过取模运算可以让指针回到数组的起始位置,从而实现循环的效果。这样就能高效地利用数组空间,避免 “假溢出” 问题。
🎉🎉🎉
在这里本章就结束啦~
我们下期见~