文章目录
本文中我们来分析几道力扣中关于栈和队列的经典OJ题目.
如果对栈和队列的概念和基本操作实现有不熟悉的可以参考作者的上一篇文章
栈和队列的基本结构与实现
1.用栈实现队列
1.1代码逻辑
在上一篇文章中,我们已经了解了对列遵循FIFO(先进先出)原则,而栈遵循FILO(后进先出)原则,则如果我们想用栈实现队列,那么就需要用两个栈来实现队列的先进先出,其中一个栈(栈1)来模拟入队列,另一个(栈2)模拟出队列。
我们在栈1中先存入五个数据,要想实现先进先出,就要先将栈1当中的数据导入到栈2中,这时候原本顺序的数据就被倒过来了,这时候再出栈,就相当于是将原本顺序的数据进行了先进先出。
那我们要将栈1当中的数据1删除时,也是这样,顺序倒过来之后再删除栈顶元素,当我们删除栈顶元素后会发现,此时在栈2中继续删除就跟队列的删除操作是一致的了,所以我们就可以直接删除,那么根据我们上面的思想(一个栈模拟入队列,一个模拟出队列),栈2就是我们的Pop栈。
当我们在删除之后再插入时,根据队列的特性,我们只能从队尾插入,并且此时还不能影响我们之后的删除操作,那么我们就不能插入栈2中(以免它影响到我们原本的数据),所以我们将数据插入到栈1中。
这样既不会影响出数据,也不会影响入数据。
当栈2中的数据被删完时,需要将栈1中的数据导入到栈2中
1.2代码实现
我们先来定义两个栈,并且需要我们自己将栈的数据结构写进来,因为C语言没有自己的栈,需要我们自己来写;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
// 初始化和销毁
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
// top指向栈顶数据的下一个位置
pst->top = 0;
// top指向栈顶数据
//pst->top = -1;
pst->capacity = 0;
}
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
// 入栈 出栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
// 扩容
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
// 取栈顶数据
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
// 判空
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
// 获取数据个数
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
typedef struct {
ST st1;
ST st2;
} MyQueue;
我们需要用函数获取一个指向队列的指针,并对两个栈进行初始化
MyQueue* myQueueCreate() {
MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue));
STInit(&queue->st1);
STInit(&queue->st2);
return queue;
}
入队列
入队列时我们直接将数据push进战役就行了,因为当我们入栈1时是后进先出,再入栈2时由于顺序的颠倒,是不会影响我们原本数据的顺序,既然影响不到出数据就可以直接push
void myQueuePush(MyQueue* obj, int x)
{
STPush(&obj->st1, x);
}
出队列
出队列时我们只需要看栈2,在出队列之前判断栈2是否为空,如果为空就需要将栈1当中的数据导入到栈2中;
int myQueuePop(MyQueue* obj) {
int size = obj->st1.top;
while (size > 1) {
STPush(&obj->st2, STTop(&obj->st1));
STPop(&obj->st1);
size--;
}
int ret = STTop(&obj->st1);
STPop(&obj->st1);
size = obj->st2.top;
while (size > 0) {
STPush(&obj->st1, STTop(&obj->st2));
STPop(&obj->st2);
size--;
}
return ret;
}
获取队头元素
在获取之前需要检查栈2中是否为空,如果在栈2为空时,栈1也为空,那么要返回NULL,除此之外,它的代码和出队列的代码非常相似;
int myQueuePeek(MyQueue* obj)
{
return obj->st1.a[0];
}
判空
如果两个栈都为空时,那这个队列就为空;
bool myQueueEmpty(MyQueue* obj)
{
return STEmpty(&obj->st1);
}
销毁
销毁时(销毁要从内到外释放)要注意我们的队列是由两个栈构成的,所以要先释放两个栈的空间,再释放队列的空间;
void myQueueFree(MyQueue* obj) {
STDestroy(&obj->st1);
STDestroy(&obj->st2);
free(obj);
}
完整代码
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
// 初始化和销毁
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
// top指向栈顶数据的下一个位置
pst->top = 0;
// top指向栈顶数据
//pst->top = -1;
pst->capacity = 0;
}
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
// 入栈 出栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
// 扩容
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
// 取栈顶数据
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
// 判空
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
// 获取数据个数
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
typedef struct {
ST st1;
ST st2;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue));
STInit(&queue->st1);
STInit(&queue->st2);
return queue;
}
void myQueuePush(MyQueue* obj, int x) { STPush(&obj->st1, x); }
int myQueuePop(MyQueue* obj) {
int size = obj->st1.top;
while (size > 1) {
STPush(&obj->st2, STTop(&obj->st1));
STPop(&obj->st1);
size--;
}
int ret = STTop(&obj->st1);
STPop(&obj->st1);
size = obj->st2.top;
while (size > 0) {
STPush(&obj->st1, STTop(&obj->st2));
STPop(&obj->st2);
size--;
}
return ret;
}
int myQueuePeek(MyQueue* obj) { return obj->st1.a[0]; }
bool myQueueEmpty(MyQueue* obj) { return STEmpty(&obj->st1); }
void myQueueFree(MyQueue* obj) {
STDestroy(&obj->st1);
STDestroy(&obj->st2);
free(obj);
}
/**
* Your MyQueue struct will be instantiated and called as such:
* MyQueue* obj = myQueueCreate();
* myQueuePush(obj, x);
* int param_2 = myQueuePop(obj);
* int param_3 = myQueuePeek(obj);
* bool param_4 = myQueueEmpty(obj);
* myQueueFree(obj);
*/
2.用队列实现栈
2.1代码逻辑
通过上面用栈实现队列的逻辑,在用队列实现栈的时候,我们也可以用两个队列来实现;
但与上面不同的是我们的两个队列一个用来插入数据,另一个用来存数据;
1.空队列用来导数据;
2.非空队列用来插入删除数据;
下面来细说它的逻辑:
队列的特点是先进先出,而栈是后进先出;
倘若有一组数据*([1,2,3,4,5])*,要想通过队列来实现栈的删除,就要将栈顶的数据删除,可队列只能从队头删除,所以只用一个队列实现是不现实的;
这时候就要用到第二个队列,将队尾的数据(“5”)想办法变成队头数据,这样我们才能删除它,如果队列2为空队列根据队列先进先出的原则,我们就将数据5之前的四个数据push进队列2中去,这是队列1只剩下了5,它也就顺理成章的成为了队头元素,就能够进行pop操作将其删除了;
并且在删除后队列1变成了空队列,如果接下来还要进行删除操作,就将上面的操作反过来,将队列2中的数据push进队列1中,再把对列2中仅剩的数据4pop掉就可以了,
当我们要进行入栈操作时,只需要正常的入队列就可以了;
在我们写代码时一定要区分清楚空队列和非空队列要实现的功能;
2.2代码实现
跟用栈实现队列一样,我们要先把队列的数据结构写出来,并定义两个队列;
//#include<stdio.h>
//#include<stdbool.h>
//#include<stdlib.h>
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(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 fail");
return;
}
newnode->next = NULL;
newnode->val = x;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
// 队头删除
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->size != 0);
/*QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
if (pq->phead == NULL)
pq->ptail = NULL;*/
// 一个节点
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else // 多个节点
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
typedef struct {
Queue q1;
Queue q2;
} MyStack;
创建一个函数来获取指向MyStack的指针,并对队列初始化;
MyStack* myStackCreate() {
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
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* empty = &(obj->q1);
Queue* nonEmpty = &(obj->q2);
if (!QueueEmpty(&(obj->q1)))
{
nonEmpty = &(obj->q1);
empty = &(obj->q2);
}
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->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);
}
完整代码
typedef struct {
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate() {
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
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* empty = &(obj->q1);
Queue* nonEmpty = &(obj->q2);
if (!QueueEmpty(&(obj->q1)))
{
nonEmpty = &(obj->q1);
empty = &(obj->q2);
}
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->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);
}
/**
* Your MyStack struct will be instantiated and called as such:
* MyStack* obj = myStackCreate();
* myStackPush(obj, x);
* int param_2 = myStackPop(obj);
* int param_3 = myStackTop(obj);
* bool param_4 = myStackEmpty(obj);
* myStackFree(obj);
*/
3.设计循环队列
3.1代码逻辑
循环队列的定义:
循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
在设计时我们要实现以下几个功能:
在设计时有一个非常严重的问题:假溢出问题;
系统作为队列用的存储区还没有满,但队列却发生了溢出,我们把这种现象称为"假溢出"。我们可以假设分配了大小为6的数组,用作队列。那我们在添加完a4元素后,就无法再添加元素了,因为队列已经“满了”。但是这时0-2号数组的槽位是空的,理论上是可以添加新元素的,这时候的队列元素的溢出就是“假溢出”。
一种方法是增加一个size每次队首的元素被取出时候,让剩余所有的元素都往前平移一个单位。但是这种方式太过于耗费性能,数组最不擅长删除元素,与其用这种方式实现顺序队列,还不如直接用链式队列。
另一种就是额外多开一块空间,为什么要多开一块空间,当我们只开k块空间时,
–head(指向队头);
–rear(指向队尾);
–初始化时都为0
我们无法区分判空和判满;
则要多增加一块空间,当head==rear时为空,当rear+1 == head时为满
当我们判满时还有一个坑点,因为我们使用的是数组,当rear在数组末端+1时,会越界,这时候要用到取余数的方法,(rear+1)%(k+1),让rear达到了循环的功能;
我们用第二种方法来实现;
3.2代码实现
初始化
在初始化之前我们要先定义好结构体变量,我们需要定义一个数组空间,来存储循环队列
定义head,rear分别指向队头和队尾,定义k表示队列的容量;
初始化时head,rear都指向队头;
由于我们这里使用的是数组,所以我们在初始化时,进行扩容操作;
typedef struct {
int* a;
int k;//队列的容量
int head;//头
int rear;//尾的下一个
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* queue = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
queue->a = (int*)malloc(sizeof(int) * (k + 1));
queue->rear = queue->head = 0;
queue->k=k;
return queue;
}
插入元素
先判满,若未满就将新数据添加到rear所指向的位置;rear别忘了要自增
如果rear在数组的末端 ,还要取余数==(rear = rear%(k+1))==;
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if (myCircularQueueIsFull(obj)) {
return false;
} else {
obj->a[obj->rear++] = value;
obj->rear %= obj->k + 1;
return true;
}
}
删除元素
删除时也可能会有越界的情况,当head在末端时,+1会越界,这是需要取余数操作;head = head%(k+1);
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj)) {
return false;
} else {
obj->head++;
obj->head = (obj->head + obj->k + 1) % (obj->k + 1);
return true;
}
}
判空和判满
根据我们前面说的:
当head==rear时为空,当rear+1 == head时为满
当我们判满时还有一个坑点,因为我们使用的是数组,当rear在数组末端+1时,会越界,这时候要用到取余数的方法,(rear+1)%(k+1),让rear达到了循环的功能;
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->head == obj->rear;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear + 1) % (obj->k + 1) == obj->head;
}
获取队头数据
还是要先判空,
1.如果为空,返回-1;
2.不为空,返回head指向的数据;
int myCircularQueueFront(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj)) {
return -1;
} else {
return obj->a[obj->head];
}
}
获取队尾数据
还是先判空
1.为空,返回-1;
2.不为空,需要返回rear的前一个数据;但这里有一个坑就是当rear在队头时,rear-1就成了-1会造成越界,所以需要取余数操作:((rear+k)%(k+1));
下图为特殊情况:
int myCircularQueueRear(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj)) {
return -1;
} else {
return obj->a[(obj->rear+obj->k)%(obj->k+1)];
//这里可能有点难理解,代入写特殊情况就明白了为什么这样写了
//及rear可能等于0,此时rear-1可就等于-1了
}
}
销毁
由内向外销毁;
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
完整代码
typedef struct {
int* a;
int k;//队列的容量
int head;//头
int rear;//尾的下一个
} MyCircularQueue;
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->head == obj->rear;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear + 1) % (obj->k + 1) == obj->head;
}
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* queue = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
queue->a = (int*)malloc(sizeof(int) * (k + 1));
queue->rear = queue->head = 0;
queue->k=k;
return queue;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if (myCircularQueueIsFull(obj)) {
return false;
} else {
obj->a[obj->rear++] = value;
obj->rear %= obj->k + 1;
return true;
}
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj)) {
return false;
} else {
obj->head++;
obj->head = (obj->head + obj->k + 1) % (obj->k + 1);
return true;
}
}
int myCircularQueueFront(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj)) {
return -1;
} else {
return obj->a[obj->head];
}
}
int myCircularQueueRear(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj)) {
return -1;
} else {
return obj->a[(obj->rear+obj->k)%(obj->k+1)];
//这里可能有点难理解,代入写特殊情况就明白了为什么这样写了
//及rear可能等于0,此时rear-1可就等于-1了
}
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
/**
* Your MyCircularQueue struct will be instantiated and called as such:
* MyCircularQueue* obj = myCircularQueueCreate(k);
* bool param_1 = myCircularQueueEnQueue(obj, value);
* bool param_2 = myCircularQueueDeQueue(obj);
* int param_3 = myCircularQueueFront(obj);
* int param_4 = myCircularQueueRear(obj);
* bool param_5 = myCircularQueueIsEmpty(obj);
* bool param_6 = myCircularQueueIsFull(obj);
* myCircularQueueFree(obj);
*/