一、概要
栈和队列都是常见的线性数据结构,但是它们之间有很大的区别。
栈(Stack)是一种后进先出(Last In First Out,LIFO)的数据结构。栈顶是最后一个入栈的元素,只能从栈顶进行插入和删除操作。栈的应用非常广泛,例如表达式求值、递归函数调用等。
队列(Queue)是一种先进先出(First In First Out,FIFO)的数据结构。队尾是最后一个入队的元素,队头是最先出队的元素。队列的应用也非常广泛,例如进程调度、缓存等。
在实际应用中,我们需要根据不同的需求选择使用栈或队列。如果需要按照后进先出的顺序进行处理,就应该使用栈;如果需要按照先进先出的顺序进行处理,就应该使用队列。当然,在某些情况下,栈和队列也可以通过一些特殊的实现方式来满足相反的需求。例如,双端队列(Deque)可以同时支持队列和栈两种操作。
二、基础回顾
1.数据结构-利用动态数组实现栈
2.数据结构-利用链表实现队(包含源码+详细解析)
3.指针的进阶
另外,大家还要复习一下结构体传参相关的知识点!
三、经典问题
1.括号的匹配
力扣https://leetcode.cn/problems/valid-parentheses/
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
bool isValid(char * s){
int len = strlen(s);
char stack[len+1]; // 定义一个栈
int top = -1; // 栈顶指针初始化为-1
for (int i = 0; i < len; i++) {
if (s[i] == '(' || s[i] == '[' || s[i] == '{') { // 左括号入栈
stack[++top] = s[i];
} else { // 右括号出栈并判断是否匹配
if (top == -1) { // 栈为空,无法匹配
return false;
}
if (s[i] == ')' && stack[top] == '(') { // 括号匹配
top--;
} else if (s[i] == ']' && stack[top] == '[') {
top--;
} else if (s[i] == '}' && stack[top] == '{') {
top--;
} else { // 括号不匹配,返回 false
return false;
}
}
}
if (top == -1) { // 栈为空说明所有括号都匹配成功
return true;
} else { // 栈不为空说明还有括号未匹配成功
return false;
}
}
这段代码实现了一个判断输入字符串中括号是否匹配的函数 isValid。
isValid 函数中,首先使用 strlen 函数获取字符串 s 的长度,然后定义了一个 char 类型的数组 stack,作为栈的存储结构,并初始化栈顶指针 top 为 -1。
接着,函数使用 for 循环遍历字符串 s 中的每个字符。当遇到左括号时,将其入栈;当遇到右括号时,从栈中取出栈顶元素并进行匹配。如果栈为空或者当前右括号与栈顶元素不匹配,则返回 false。否则,栈顶元素出栈,继续遍历字符串。
最后,判断栈是否为空。如果为空,说明所有括号都匹配成功,返回 true;否则,还有括号未匹配成功,返回 false。
2.用队列实现栈
力扣https://leetcode.cn/problems/implement-stack-using-queues/description/
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDatatype;
typedef struct QueueNode{
struct QueueNode* next;
QDatatype data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
void QueueInit(Queue*pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDatatype x);
void QueuePop(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
QDatatype QueueFront(Queue* pq);
QDatatype QueueBack(Queue* pq);
void QueueInit(Queue* pq) {
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq) {
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
pq->tail = 0;
}
void QueuePush(Queue* pq, QDatatype x) {//向队列尾部插入元素
QNode* newcode = (QNode*)malloc(sizeof(QNode));
if (newcode==NULL)
{
perror("malloc fail");
return;
}
newcode->data = x;
newcode->next = NULL;
if (pq->head == NULL) {
assert(pq->tail == NULL);
pq->head = pq->tail = newcode;
}
else {
pq->tail->next = newcode;
pq->tail = newcode;
}
pq->size++;
}
void QueuePop(Queue* pq) {
assert(pq);
assert(pq->head != NULL);
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
if (pq->head== NULL)
pq->tail = next;
pq->size--;
}
//QueueInit:初始化队列
//QueueDestroy:销毁队列
//QueuePush:向队列尾部插入元素
//QueuePop:弹出队列头部元素
int QueueSize(Queue* pq)//QueueSize:获取队列大小
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq)//QueueEmpty:判断队列是否为空
{
assert(pq);
return pq->size == 0;
}
QDatatype QueueFront(Queue* pq)//Queueont:获取队列头部元素
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDatatype QueueBack(Queue* pq)//QueueBack:获取队列尾部元素
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
//用两个队列实现一个栈
typedef struct {
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate() {
MyStack*pst=(MyStack*)malloc(sizeof(MyStack));
if(pst==NULL){
perror("malloc fail");
return NULL;
}
QueueInit(&pst->q1);
QueueInit(&pst->q2);
return pst;
}
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);
}
这段代码实现了一个基于队列的栈,即使用两个队列 q1 和 q2 来模拟栈的操作。每次 push 元素时,将元素插入到非空的队列中,如果两个队列都为空,则默认将元素插入到 q1 中。pop 元素时,先将非空队列中的元素取出并压入另一个空队列,直到只有一个元素时弹出即可。该栈支持 push、pop、top 和 empty 四种操作。
具体来说,这个程序中定义了两个队列:Queue 类型,包括队列头部指针 head、队列尾部指针 tail、当前队列中元素个数 size,和节点结构体 QNode,包括数据 data 和下一个节点指针 next。程序中还定义了一些与队列相关的函数,具体如下:
- QueueInit(Queue*pq):用来初始化队列,将队列头部指针、队列尾部指针和队列大小全部设为 0。
- QueueDestroy(Queue* pq):用来销毁队列,释放队列中所有节点的内存空间,并将队列头部指针、队列尾部指针和队列大小全部设为初始值。
- QueuePush(Queue* pq, QDatatype x):向队列尾部插入元素,即在队列末尾插入一个节点,并将队列尾部指针指向该节点。
- QueuePop(Queue* pq):弹出队列头部元素,即将队列头部指针指向下一个节点,并释放原来头部节点的内存空间。
- QueueSize(Queue* pq):获取队列大小,即返回当前队列中元素的个数。
- QueueEmpty(Queue* pq):判断队列是否为空,当队列大小为 0 时返回 true。
- QueueFront(Queue* pq):获取队列头部元素,即返回队列头部节点包含的数据 data。
- QueueBack(Queue* pq):获取队列尾部元素,即返回队列尾部节点包含的数据 data。
此外,该程序还定义了 MyStack 结构体,包括两个 Queue 类型的变量 q1 和 q2。MyStack 结构体还实现了以下函数:
- myStackCreate():用来创建基于队列的栈。在函数中,首先通过 malloc 分配内存空间,然后分别初始化两个 Queue 变量 q1 和 q2,并将它们作为成员变量保存在 MyStack 结构体中。
- myStackPush(MyStack* obj, int x):用来向栈中压入一个元素。在函数中,首先判断 q1 是否为空,如果非空则将元素插入到 q1 中,否则插入到 q2 中。
- myStackPop(MyStack* obj):用来从栈中弹出一个元素。在函数中,首先找出非空队列和空队列,然后将非空队列中的所有元素都取出并插入到空队列中,直到非空队列中剩余一个元素,最后将该元素弹出并返回。
- myStackTop(MyStack* obj):用来获取栈顶元素,即返回 q1 或 q2 中的最后一项。
- myStackEmpty(MyStack* obj):用来判断栈是否为空,当 q1 和 q2 队列的大小均为 0 时返回 true。
- myStackFree(MyStack* obj):用来销毁基于队列的栈,释放 q1 和 q2 队列的内存空间,并将 MyStack 结构体指针设置为 NULL。
在实际使用过程中,需要注意以下几点:
- 对 Queue 相关函数的理解和掌握程度可能会影响到该栈的正确性,需要非常熟悉这些函数的实现原理以及使用方式。
- 内存管理非常重要,需要始终保持严谨的编码习惯,避免内存泄漏、野指针等问题的发生。
- 可能会遇到由于队列操作不当而导致的性能问题,可以适时进行优化。
3.用栈实队列
力扣https://leetcode.cn/problems/implement-queue-using-stacks/
#include<stdlib.h>
#include<stdio.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps);//初始化
void STDesroy(ST* ps);//销毁
void STPush(ST* ps,STDataType x);//插入
void STPop(ST* ps);//删除
int STSize(ST* ps);//数据的长度
bool STEmpty(ST* ps);//判断是否为空值
STDataType STTop(ST* ps);//取栈顶的数据
void STInit(ST* ps)//初始化
{
assert(ps);
ps->a =(STDataType*)malloc(sizeof(STDataType)*4);
if (ps->a == NULL) {
printf("malloc fail\n");
return;
}
ps->capacity = 4;
ps->top = 0;//ps->top = 1;
//top=0,意味着top指向栈顶数据的下一个,top=-1指向的是栈顶数据
}
void STDesroy(ST* ps)//销毁
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)//插入
{
assert(ps);
//判断空间是否满的
if (ps->top == ps->capacity) {
STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) *ps->capacity*2);
if (tmp==NULL) {
printf("relloc fail\n");
return;
}
//把数据放到top的位置
ps->a = tmp;
ps->capacity*=2;
}
ps->a[ps->top] = x;
ps->top++;
}
void STPop(ST* ps)//删除
{
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
int STSize(ST* ps) {
assert(ps);
return ps->top;
}
bool STEmpty(ST* ps)//判断是否为空值
{
assert(ps);
return ps->top == 0;
}
STDataType STTop(ST* ps)//取栈顶的数据
{
assert(ps);
assert(!STEmpty(ps));//加断言解释一下
return ps->a[ps->top - 1];
}
typedef struct {
ST pushst;
ST popst;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue*obj=(MyQueue*)malloc(sizeof(MyQueue));
if(obj==NULL){
perror("malloc fail");
return NULL;
}
STInit(&obj->pushst);
STInit(&obj->popst);
return obj;
}
void myQueuePush(MyQueue* obj, int x) {
STPush(&obj->pushst,x);
}
int myQueuePop(MyQueue* obj) {
//倒·数据
if(STEmpty(&obj->popst))
{
while(!STEmpty(&obj->pushst)){
STPush(&obj->popst,STTop(&obj->pushst));
STPop(&obj->pushst);
}
}
int front =STTop(&obj->popst);
STPop(&obj->popst);
return front;
}
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) {
STDesroy(&obj->pushst);
STDesroy(&obj->popst);
free(obj);
}
这是一个使用两个栈来实现队列的 C 语言代码,即 MyQueue 结构体中包含两个 ST(Stack)类型的栈 pushst 和 popst。其中,pushst 用于入队操作,popst 用于出队操作。该代码主要实现了以下几个函数:
MyQueue* myQueueCreate()
:创建一个新的队列并返回指向该队列的指针。在该函数中,我们通过调用STInit
函数初始化了pushst
和popst
两个栈,并将它们封装到MyQueue
结构体中。
void myQueuePush(MyQueue* obj, int x)
:将整数 x 入队。在该函数中,我们只需要调用STPush
函数将 x 放入pushst
栈顶即可。
int myQueuePop(MyQueue* obj)
:将队首元素出队并返回该元素的值。为了实现这一功能,我们需要使用两个栈,将pushst
中的所有元素倒序放入popst
中,并从popst
的栈顶取出队首元素。在此之后,我们可以通过将popst
的元素再次倒序放入pushst
中维护队列的状态。
int myQueuePeek(MyQueue* obj)
:返回队首元素的值,但并不将其出队。与myQueuePop
函数相似,我们需要先将pushst
中的元素倒序放入popst
中,并从popst
的栈顶取出队首元素。但与myQueuePop
函数不同的是,该函数并不会将元素从popst
中删除。
bool myQueueEmpty(MyQueue* obj)
:判断队列是否为空。在该函数中,我们只需要检查pushst
和popst
两个栈是否均为空即可。
void myQueueFree(MyQueue* obj)
:释放队列的内存空间。在该函数中,我们通过调用STDesroy
函数销毁pushst
和popst
两个栈,并通过free
函数释放obj
所指向的内存空间。
3.设计循环队列
typedef struct {
int*a;
int front;
int rear;
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue*obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->front=obj->rear=0;
obj->a=(int*)malloc(sizeof(int)*(k+1));
obj->k=k;
return obj;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->front==obj->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->k+1);
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 x =obj->rear==0?obj->k:obj->rear-1;
// return obj->a[x];
return obj->a[(obj->rear-1+obj->k+1)%(obj->k+1)];
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
这是一个使用数组实现循环队列的 C 语言代码,即 MyCircularQueue 结构体中包含一个 int 类型的数组 a、两个整型变量 front 和 rear 分别表示队首和队尾的下标位置,以及变量 k 表示队列的容量大小。该代码主要实现了以下几个函数:
MyCircularQueue* myCircularQueueCreate(int k)
:创建一个容量为 k 的循环队列并返回指向该队列的指针。在该函数中,我们通过调用malloc
函数分配数组的内存空间,并初始化 front 和 rear 变量为 0。
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
:判断队列是否为空。在该函数中,我们只需要检查 front 和 rear 两个变量是否相等即可。
bool myCircularQueueIsFull(MyCircularQueue* obj)
:判断队列是否已满。由于采用了循环队列的实现方式,因此队列最多只能存储 k 个元素。在该函数中,我们使用求模运算判断队列是否已满。
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
:将元素 value 入队。在该函数中,我们先判断队列是否已满,若已满则返回 false,否则将元素放入 rear 所指向的位置并更新 rear 的值。由于采用了循环队列的实现方式,因此我们需要对 rear 的值取模操作以满足队列“循环”的特性。
bool myCircularQueueDeQueue(MyCircularQueue* obj)
:将队首元素出队。在该函数中,我们先判断队列是否为空,若为空则返回 false,否则更新 front 的值即可。同样由于采用了循环队列的实现方式,我们需要对 front 的值取模操作以满足队列“循环”的特性。
int myCircularQueueFront(MyCircularQueue* obj)
:返回队首元素的值。在该函数中,我们先判断队列是否为空,若为空则返回 -1,否则返回数组 a 中 front 所指向的位置的元素。
int myCircularQueueRear(MyCircularQueue* obj)
:返回队尾元素的值。在该函数中,我们先判断队列是否为空,若为空则返回 -1,否则返回数组 a 中 rear-1 所指向的位置的元素。此处注意由于采用了循环队列的实现方式,因此我们需要对 rear 进行一定的计算以获取准确的位置。
void myCircularQueueFree(MyCircularQueue* obj)
:释放队列的内存空间。在该函数中,我们通过调用free
函数释放数组所占用的内存空间,并通过free
函数释放obj
所指向的内存空间。
return obj->a[(obj->rear-1+obj->k+1)%(obj->k+1)]
在
myCircularQueueRear
函数中,代码行return obj->a[(obj->rear-1+obj->k+1)%(obj->k+1)]
用于返回循环队列的队尾元素。对于该行代码,我们可以进行如下解释:
首先,由于采用的是循环队列的实现方式,因此队列的队尾位置可能会“循环”到数组的前面。因此,为了避免出现队尾元素先“循环”到数组前面再回到数组尾部的情况,我们需要对数组的下标进行取模运算,即
(obj->rear-1+obj->k+1)%(obj->k+1)
。其次,由于队尾指针在入队时是指向队尾元素的下一个位置,因此我们需要将 rear 指针减 1。因此,代码中的
(obj->rear-1+obj->k+1)
表示计算出队尾元素在数组中的下标。最后,通过
obj->a[...]
访问数组 a 中相应位置的元素并将它作为函数的返回值。即obj->a[(obj->rear-1+obj->k+1)%(obj->k+1)]
返回循环队列的队尾元素。