本篇将用数组实现栈、链表实现队列以及使用数组实现循环队列,然后实现了用栈实现队列和用队列实现栈以及一些其他的栈和队列的习题,加深队栈和队列的理解。
若只看对应所需,可直接看文章旁的目录。
1.栈
1.1栈的概念及结构
栈:一种特殊的线性表,其中只允许在固定的一端插入和删除元素,进行数据插入和删除操作的一端被称为栈顶,另一端被称为栈底。栈中的数据元素遵守先进后出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈,出数据也在栈顶。
上图就是出栈和压栈的示意图。
1.2栈的实现
栈的实现一般可以使用数据或者链表实现,但相对而言使用数组实现更优一些,因为数组在尾上插入数据的代价比较小。
所以本篇将使用数组实现栈。
注:对于数组实现栈,我们可以将栈顶指针指向栈顶元素,也可以指向栈顶元素的上一个位置,这两种实现方式都是可以的。本篇的栈顶指针指向的是栈的上一个元素。两种的实现方式都有略微的不同。
对于栈的实现,对于较难理解的将给出注释,如下:
1.2.1 Stack.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
//定义元素数据类型
typedef int STDataType;
//栈的数据结构
typedef struct Stack {
STDataType* arr;
int top;
int capacity;
}Stack;
//栈的初始化以及销毁
void STInit(Stack* ps);
void STDestroy(Stack* ps);
//压栈与出栈
void STPush(Stack* ps, STDataType x);
void STPop(Stack* ps);
//返回栈顶元素/栈的尺寸/栈是否为NULL
STDataType STTop(Stack* ps);
int STSize(Stack* ps);
bool STEmpty(Stack* ps);
1.2.2 Stack.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"
void STInit(Stack* ps) {
assert(ps);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
}
void STDestroy(Stack* ps) {
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
ps = NULL;
}
void STPush(Stack* ps, STDataType x) {
assert(ps);
//判断栈是否需要扩容
if (STSize(ps) == ps->capacity) {
//三目操作符进行扩容,第一次的容量为4,以后每次扩容一倍
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->arr, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc:");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
ps->arr[ps->top] = x;
ps->top++;
}
void STPop(Stack* ps) {
assert(ps);
assert(!STEmpty(ps));
//出栈只需要将栈的尺寸减小,下次压栈的元素直接进行覆盖
ps->top--;
}
STDataType STTop(Stack* ps) {
assert(ps);
assert(!STEmpty(ps));
STDataType ret = ps->arr[ps->top - 1];
return ret;
}
int STSize(Stack* ps) {
assert(ps);
//栈顶的编号其实就是栈的尺寸
return ps->top;
}
bool STEmpty(Stack* ps) {
assert(ps);
return ps->top == 0;
}
1.2.3 All of Code
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
//定义元素数据类型
typedef int STDataType;
//栈的数据结构
typedef struct Stack {
STDataType* arr;
int top;
int capacity;
}Stack;
//栈的初始化以及销毁
void STInit(Stack* ps);
void STDestroy(Stack* ps);
//压栈与出栈
void STPush(Stack* ps, STDataType x);
void STPop(Stack* ps);
//返回栈顶元素/栈的尺寸/栈是否为NULL
STDataType STTop(Stack* ps);
int STSize(Stack* ps);
bool STEmpty(Stack* ps);
void STInit(Stack* ps) {
assert(ps);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
}
void STDestroy(Stack* ps) {
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
ps = NULL;
}
void STPush(Stack* ps, STDataType x) {
assert(ps);
//判断栈是否需要扩容
if (STSize(ps) == ps->capacity) {
//三目操作符进行扩容,第一次的容量为4,以后每次扩容一倍
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->arr, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc:");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
ps->arr[ps->top] = x;
ps->top++;
}
void STPop(Stack* ps) {
assert(ps);
assert(!STEmpty(ps));
//出栈只需要将栈的尺寸减小,下次压栈的元素直接进行覆盖
ps->top--;
}
STDataType STTop(Stack* ps) {
assert(ps);
assert(!STEmpty(ps));
STDataType ret = ps->arr[ps->top - 1];
return ret;
}
int STSize(Stack* ps) {
assert(ps);
//栈顶的编号其实就是栈的尺寸
return ps->top;
}
bool STEmpty(Stack* ps) {
assert(ps);
return ps->top == 0;
}
int main() {
Stack st;
STInit(&st);
STPush(&st, 1);
STPush(&st, 2);
STPush(&st, 3);
while (!STEmpty(&st)) {
STDataType top = STTop(&st);
STPop(&st);
printf("%d ", top);
}
STPush(&st, 4);
STPush(&st, 5);
while (!STEmpty(&st)) {
STDataType top = STTop(&st);
STPop(&st);
printf("%d ", top);
}
STDestroy(&st);
return 0;
}
测试结果:
2.队列
2.1 队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)的性质。
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。
2.2队列的实现
对于队列的实现,都可以使用数组以及链表结构,但是使用链表结构实现出队以及入队将更方便,同时还会节省更多的空间。
如上图所示,当使用数组实现队列时,由于开辟的空间是固定的,所以出队时将会浪费一些空间。
2.2.1 Queue.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int QDataType;
//队列节点的数据结构
typedef struct Node {
QDataType val;
struct Node* next;
}QNode;
//队列的队首指针、队尾指针、尺寸
typedef struct Queue {
int size;
QNode* phead;
QNode* ptail;
}Queue;
//队列的初始化/销毁
void QueueInit(Queue* pQ);
void QueueDestory(Queue* pQ);
//队列的入队/出队
void QueuePush(Queue* pQ, QDataType x);
void QueuePop(Queue* pQ);
//返回队首元素/队尾元素
QDataType QueueFront(Queue* pQ);
QDataType QueueBack(Queue* pQ);
//判断队列是否为NULL/返回队列的大小
bool QueueEmpty(Queue* pQ);
int QueueSize(Queue* pQ);
2.2.2 Queue.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"
void QueueInit(Queue* pQ) {
assert(pQ);
pQ->phead = pQ->ptail = NULL;
pQ->size = 0;
}
void QueueDestory(Queue* pQ) {
assert(pQ);
QNode* cur = pQ->phead;
while (cur) {
QNode* next = cur->next;
free(cur);
cur = next;
}
pQ->phead = pQ->ptail = NULL;
pQ->size = 0;
}
void QueuePush(Queue* pQ, QDataType x) {
assert(pQ);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL) {
perror("malloc:");
exit(1);
}
newnode->val = x;
newnode->next = NULL;
if (pQ->phead == NULL) {
pQ->phead = pQ->ptail = newnode;
}
else {
pQ->ptail->next = newnode;
pQ->ptail = newnode;
}
pQ->size++;
}
void QueuePop(Queue* pQ) {
assert(pQ);
assert(pQ->phead);
QNode* cur = pQ->phead;
pQ->phead = pQ->phead->next;
free(cur);
cur = NULL;
pQ->size--;
}
QDataType QueueFront(Queue* pQ) {
assert(pQ);
assert(pQ->phead);
return pQ->phead->val;
}
QDataType QueueBack(Queue* pQ) {
assert(pQ);
assert(pQ->phead);
return pQ->ptail->val;
}
bool QueueEmpty(Queue* pQ) {
assert(pQ);
return pQ->phead == NULL;
}
int QueueSize(Queue* pQ) {
assert(pQ);
return pQ->size;
}
2.2.3 All of Code
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int QDataType;
//队列节点的数据结构
typedef struct Node {
QDataType val;
struct Node* next;
}QNode;
//队列的队首指针、队尾指针、尺寸
typedef struct Queue {
int size;
QNode* phead;
QNode* ptail;
}Queue;
//队列的初始化/销毁
void QueueInit(Queue* pQ);
void QueueDestory(Queue* pQ);
//队列的入队/出队
void QueuePush(Queue* pQ, QDataType x);
void QueuePop(Queue* pQ);
//返回队首元素/队尾元素
QDataType QueueFront(Queue* pQ);
QDataType QueueBack(Queue* pQ);
//判断队列是否为NULL/返回队列的大小
bool QueueEmpty(Queue* pQ);
int QueueSize(Queue* pQ);
void QueueInit(Queue* pQ) {
assert(pQ);
pQ->phead = pQ->ptail = NULL;
pQ->size = 0;
}
void QueueDestory(Queue* pQ) {
assert(pQ);
QNode* cur = pQ->phead;
while (cur) {
QNode* next = cur->next;
free(cur);
cur = next;
}
pQ->phead = pQ->ptail = NULL;
pQ->size = 0;
}
void QueuePush(Queue* pQ, QDataType x) {
assert(pQ);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL) {
perror("malloc:");
exit(1);
}
newnode->val = x;
newnode->next = NULL;
if (pQ->phead == NULL) {
pQ->phead = pQ->ptail = newnode;
}
else {
pQ->ptail->next = newnode;
pQ->ptail = newnode;
}
pQ->size++;
}
void QueuePop(Queue* pQ) {
assert(pQ);
assert(pQ->phead);
QNode* cur = pQ->phead;
pQ->phead = pQ->phead->next;
free(cur);
cur = NULL;
pQ->size--;
}
QDataType QueueFront(Queue* pQ) {
assert(pQ);
assert(pQ->phead);
return pQ->phead->val;
}
QDataType QueueBack(Queue* pQ) {
assert(pQ);
assert(pQ->phead);
return pQ->ptail->val;
}
bool QueueEmpty(Queue* pQ) {
assert(pQ);
return pQ->phead == NULL;
}
int QueueSize(Queue* pQ) {
assert(pQ);
return pQ->size;
}
int main() {
Queue queue;
QueueInit(&queue);
QueuePush(&queue, 1);
QueuePush(&queue, 2);
printf("%d ", QueueFront(&queue));
QueuePop(&queue);
QueuePush(&queue, 3);
QueuePush(&queue, 4);
printf("%d ", QueueFront(&queue));
QueuePop(&queue);
QueuePush(&queue, 5);
while (!QueueEmpty(&queue)) {
printf("%d ", QueueFront(&queue));
QueuePop(&queue);
}
QueueDestory(&queue);
return 0;
}
测试结果:
2.3循环队列的实现
在上文中我们提到,当我们使用数组实现队列的时候,会出现一些空间浪费的情况,这是因为出队时,出队下标将向后移动,前面的空间则浪费,当我们使用循环链表就可以很好的解决该问题。
循环链表:将数组的首尾相连,组成的一个特殊结构。我们用两个指针来表示队列的队首和队尾,front 表示队首,back表示队尾的下一个元素。
为了能使Q.front=Q.back来区别队列是否为空还是满,我们常常认为出现作图时的情况即为队满的情况,此时:Q.front=Q.back+1;
对于循环队列的代码实现如下,对于代码的解释在注释给出。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
int* arr;
int front;
int back;
int k;
} MyCircularQueue;
//循环队列的初始化
MyCircularQueue* myCircularQueueCreate(int k) {
//堆上分配空间,才能在出函数时仍然存在
MyCircularQueue* Queue=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if(Queue==NULL){
perror("malloc:");
exit(1);
}
//分配k+1个空间,便于我们判断是否为满队列还是空队列
Queue->arr=(int*)malloc(sizeof(int)*(k+1));
Queue->front=Queue->back=0;
Queue->k=k;
return Queue;
}
//判断队列是否为NULL队列
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->front==obj->back;
}
//判断队列是否为满队列
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return obj->front==(obj->back+1)%(obj->k+1);
}
//在队列中加入元素
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj)){
return false;
}
else{
obj->arr[obj->back]=value;
obj->back++;
obj->back%=(obj->k+1);
return true;
}
}
//队列中删除元素
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)){
return false;
}
else{
obj->front++;
obj->front%=(obj->k+1);
return true;
}
}
//返回队首元素
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)){
return -1;
}
return obj->arr[obj->front];
}
//返回队尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)){
return -1;
}
//对于队尾元素的返回,需要注意是否为队列中第k+1个元素,有以下两种写法
//return obj->arr[(obj->back+obj->k)%(obj->k+1)];
if(obj->back==0){
return obj->arr[obj->k];
}else{
return obj->arr[obj->back-1];
}
}
//释放空间
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->arr);
free(obj);
}
对于以上循环队列的实现,其实是一个习题,链接如下:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/design-circular-queue/description/ 测试结果:
3.栈与队列相关习题
注:在习题中使用的函数都是以上的代码函数。
3.1队列实现栈
使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
习题链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/implement-stack-using-queues 若要使用两个队列实现栈的操作,最难的两个操作及就是出栈和压栈。
对于压栈,我们只需将要压栈的元素压入一个非空队列,另一个队列保存空。
对于出栈,我们只需要除最后一个元素的队列进入另一个空队列,然后在删除最后一个元素及就实现了出栈。代码如下:
typedef struct {
Queue p1;
Queue p2;
} MyStack;
MyStack* myStackCreate() {
MyStack* stack=(MyStack*)malloc(sizeof(MyStack));
if(stack==NULL){
perror("stack's malloc:");
exit(1);
}
QueueInit(&stack->p2);
QueueInit(&stack->p1);
return stack;
}
void myStackPush(MyStack* obj, int x) {
if(!QueueEmpty(&obj->p1))
QueuePush(&obj->p1,x);
else
QueuePush(&obj->p2,x);
}
int myStackPop(MyStack* obj) {
Queue* empty=&obj->p1;
Queue* nonempty=&obj->p2;
if(!QueueEmpty(empty)){
empty=&obj->p2;
nonempty=&obj->p1;
}
while(QueueSize(nonempty)>1){
QueuePush(empty,QueueFront(nonempty));
QueuePop(nonempty);
}
int top=QueueFront(nonempty);
QueuePop(nonempty);
return top;
}
int myStackTop(MyStack* obj) {
if(!QueueEmpty(&obj->p1)){
return QueueBack(&obj->p1);
}else{
return QueueBack(&obj->p2);
}
}
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->p1)&&QueueEmpty(&obj->p2);
}
void myStackFree(MyStack* obj) {
QueueDestory(&obj->p1);
QueueDestory(&obj->p2);
free(obj);
obj=NULL;
}
测试结果:
3.2栈实现队列
使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
习题链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/implement-queue-using-stacks/ 使用两个栈实现队列,主要思想为:一组数据进栈两次即可实现先进先出。
一个栈作为入队栈,只用来将数据压入栈;
另一个栈作为出队栈,只用来将数据弹出栈;
当我们需要将数据出队时,我们将入队栈的数据全部压入出队栈,此时出栈的顺序正确,注意:这里只有当出队栈为空时才能将入队栈的数据压入出队栈,要不然会导致出队的顺序错乱。
typedef struct {
Stack pushst;
Stack popst;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* Queue=(MyQueue*)malloc(sizeof(MyQueue));
if(Queue==NULL){
perror("queue malloc:");
exit(1);
}
STInit(&Queue->pushst);
STInit(&Queue->popst);
return Queue;
}
void myQueuePush(MyQueue* obj, int x) {
//将元素压入push栈
STPush(&obj->pushst,x);
}
int myQueuePeek(MyQueue* obj) {
if(STEmpty(&obj->popst)){
while(!STEmpty(&obj->pushst)){
STPush(&obj->popst,STTop(&obj->pushst));
STPop(&obj->pushst);
}
}
int front=STTop(&obj->popst);
return front;
}
int myQueuePop(MyQueue* obj) {
int front=myQueuePeek(obj);
STPop(&obj->popst);
return front;
}
bool myQueueEmpty(MyQueue* obj) {
return STEmpty(&obj->popst)&&STEmpty(&obj->pushst);
}
void myQueueFree(MyQueue* obj) {
STDestroy(&obj->popst);
STDestroy(&obj->pushst);
free(obj);
obj=NULL;
}
测试结果:
3.3有效的括号匹配
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
习题链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/valid-parentheses/description/ 对于该题的实现,主要思想是将所有的左括号压入栈,只要遇到右括号,将栈的中左括号弹出,判断是否配对,若不配对则返回ERROR,一直配对和判断,若最后栈为NULL,说明完全配对,则我们可以判断括号匹配。代码如下:
bool isValid(char* s) {
Stack st;
STInit(&st);
while(*s){
if(*s=='['||*s=='('||*s=='{'){
STPush(&st,*s);
}else{
if(STEmpty(&st)){
STDestroy(&st);
return false;
}
char ch=STTop(&st);
STPop(&st);
if((ch=='('&&*s!=')')||(ch=='{'&&*s!='}')||(ch=='['&&*s!=']')){
return false;
}
}
s++;
}
bool ret=STEmpty(&st);
STDestroy(&st);
return ret;
}
测试结果: