这三道题都是比较经典的一道题,主要想要考察我们对于栈、队列的性质的应用,也是笔试题的常客!!!接下来就让我们一起来手撕它!!!
题目描述:
➡️挑战链接⬅️
分析:
由于我们当前用的是C语言来刷的题,而C语言刷题最大的缺点就是得自己造轮子!!!因此我们在这里得把我们之前写的栈copy过来,不然没办法做;
进入主题:
我们的目的是实现栈,而且是利用队列去实现栈:
首先我们想到的是栈的性质是先进后出,队列的性质是先进先出;
我们先按照一样的顺序给数据入栈和入队列,那么现在我们要实现的是栈,栈的基本擦做就是出栈入栈,那么我们想要出栈的话,那么对应上图我们就像出掉4,那么栈最后所剩的数据还剩下1、2、3,但是我们对应队列的操作的话,队列是不能之间删除队尾的数据的,只能删除队头的数据,为此我们就要想办法既要保留队尾前面的数据,又要删除队尾的数据!!题目给了我们一点提示,题目允许我们使用两个队列:
那么我们就可以利用另一个队列用来先存储前一个队列队尾的数据,也就是将q1的队尾以前的数据全部放入q2这个队列里面去,最后只将最后一个队尾数据留在q1中:
那么这样的话是不是q1中原来队尾的数据就变到了队头的位置,我们是不是就能对其进行出队操作(删除该数据),同时我们也还是保留了队尾以前的数据,是不是就相当于完成了一次出栈操作!!!
那么好,既然完成了一次出栈操作,那么q1的队列是不是就变为了一个空队列;那么我们接着来完成入栈操作:由于刚才我们完成了出栈操作,所以现在我们的栈里面只剩下1、2、3这3个有效数据:
现在我想要入栈的话,是不是就是在原来的数据后面插入一个数据,比如我想插入一个数据5:那么原来的数据在哪里?是不是在q2里面,为此我们只需对q2队列完成一次入队操作就相当于完成了一次入栈操作;
那么现在我们如果再来一次出栈操作,那么按照上面的意思:我们是不是要把q2里面除了队尾的元素全部放入q1里面去啊!然后在队q2里面的最后一个元素完成一次出队操作,就相当于完成了一次出栈操作:
这是q2又变为空队了,然后我们又在原有的基础上再插入数据我们就会在q1的后面插入,不会再q2里面插入;
不知道读者们又没又注意到在整个出栈入栈的过程中q1、q2两个队列是不可能同时为空的,一定是有一个队列为空零一个队列不为空的时候我们才能完成一次出栈操作,当两个队列都为空时,就表示这个栈也就为空了!!!为此我们在这里总结一下上面的出栈操作:
1、 将不为空的队列中的元素(除了队尾元素)全部倒转到为空的队列中去;
2、 那么现在这个原来不为空的队列就只剩下一个元素了,以栈的方式来看的话,就是栈顶元素,我们就只需对其完成出栈操作就可以了;
再来总结一下上面的入栈操作:
1、 我们对不为空的队列进行掺入数据,也就相当于完成了一次入栈,不要对空的那个队列插入数据,否则会打乱我们出栈操作!!一定要保证一个队列为空!!!;
2、 如果两个队列都为空的话,那么就表示这个栈是个空栈,那么我们只需要随便找个队列入数据就可以了,保证另一个数据始终为空!!!
那么 如何获得栈顶元素?
我们的队列有一个操作叫做获取队尾元素,我们只需要对不为空的队列执行获取队尾的操作就能完成一次获取栈顶元素的操作了!!!
好了现在我们已经解决了栈的基本操作,下面就是模拟实现了:
// 链式结构:表示队列
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* next;
QDataType val;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* front;
QNode* rear;
int size;
}Queue;
//初始化队列
void QueueInit(Queue*q);
//销毁队列
void QueueDestroy(Queue*q);
//入队列
void QueuePush(Queue*q,QDataType x);
//出队列
void QueuePop(Queue*q);
//获取队头元素
QDataType QueueFront(Queue*q);
//获取队尾元素
QDataType QueueBack(Queue* q);
//获取队列元素个数
size_t QueueSize(Queue*q);
//队列判空
bool QueueEmpty(Queue*q);
//初始化队列
void QueueInit(Queue* q)
{
assert(q);
q->front = NULL;
q->rear = NULL;
q->size=0;
}
//销毁队列
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->front;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
q->front = NULL;
q->rear = NULL;
q->size=0;
}
//开辟新节点
static QNode* BuyQNode(QDataType x)
{
QNode* NewNode = (QNode*)malloc(sizeof(QNode));
if (!NewNode)
{
printf("malloc fail!\n");
exit(EXIT_FAILURE);
}
NewNode->next = NULL;
NewNode->val = x;
return NewNode;
}
//入队列
void QueuePush(Queue* q,QDataType x)
{
assert(q);
QNode* NewNode = BuyQNode(x);
if (q->rear == NULL)
{
q->front = q->rear = NewNode;
}
else
{
q->rear->next = NewNode;
q->rear = NewNode;
}
q->size++;
}
//出队列
void QueuePop(Queue* q)
{
assert(q);
assert(QueueEmpty(q)==false);//判空
QNode* next = q->front->next;
free(q->front);
q->front = next;
if (q->front == NULL)//删除节点过后front指针指向NULL,表示无节点可删
q->rear = q->front;
q->size--;
}
//队列判空
bool QueueEmpty(Queue* q)
{
assert(q);
return (q->front == q->rear && q->front == NULL);
}
//获取队头元素
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->front->val;
}
//获取队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->rear->val;
}
//获取队列元素个数
size_t QueueSize(Queue* q)
{
assert(q);
QNode* cur = q->front;
return q->size;
}
typedef struct {
Queue q1;
Queue q2;//我们得需两个队列才能完成模拟操作
} MyStack;
bool myStackEmpty(MyStack* obj);
MyStack* myStackCreate() {
//该结构体用于控制两个队列,并且完成结构体的创建和初始化工作
MyStack*NewStack=(MyStack*)malloc(sizeof(MyStack));
QueueInit(&NewStack->q1);
QueueInit(&NewStack->q2);//完成对于两个队列的初始化工作
//也就相当于完成了对于这个结构体的初始化工作
return NewStack;//由于该结构体是在堆上开辟的不会随着改函数栈帧的销毁而销毁
//只有当我们手动去销毁时,才会被销毁;
}
void myStackPush(MyStack* obj, int x) {
//完成入栈操作
assert(obj);//养成一个好的代码习惯,反之用户乱穿null指针
Queue *empty=&obj->q1;
Queue *nonempty=&obj->q2;//我们需要对不为空的队列完成插入操作,为此我们得需要知道那个是非空队;
//假设q2是非空队,但是假设是会出错的啊,这就需要我们纠正
if(QueueEmpty(&obj->q2))//如果q2是空队,则纠正错误
{
empty=&obj->q2;
nonempty=&obj->q1;
}
//到这里我们只需完成队非空队列的插入操作
QueuePush(nonempty,x);
}
int myStackPop(MyStack* obj) {
//出栈操作
assert(obj);//好习惯
assert(myStackEmpty(obj)==false);//当栈为空的时候就不需要在删除了
//老样子先判断那个队列是非空
Queue *empty=&obj->q1;
Queue *nonempty=&obj->q2;
if(QueueEmpty(&obj->q2))
{
empty=&obj->q2;
nonempty=&obj->q1;
}
//将非空队列的元素(除队尾外)全部搬运到空队;
while(QueueSize(nonempty)>1)
{
QueuePush(empty,QueueFront(nonempty));
QueuePop(nonempty);
}
int top=QueueFront(nonempty);
QueuePop(nonempty);//完成出栈操作
return top;
}
int myStackTop(MyStack* obj) {
//获取栈顶元素
assert(obj);
assert(myStackEmpty(obj)==false);//当栈为空的时候就不要在进行此操作了
//老样子先判断那个队列是非空
Queue *empty=&obj->q1;
Queue *nonempty=&obj->q2;
if(QueueEmpty(&obj->q2))
{
empty=&obj->q2;
nonempty=&obj->q1;
}
//非空队列的队尾就相当于栈顶
int top=QueueBack(nonempty);
return top;
}
bool myStackEmpty(MyStack* obj) {
assert(obj);
return (QueueEmpty(&obj->q1)&&(QueueEmpty(&obj->q2)));//只有两个队列都为空,整个栈才为空;
}
void myStackFree(MyStack* obj) {
assert(obj);
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);//注意这里不要一上来就释放obj,这样会造成内存泄漏,因为obj之开辟存放q1和q2的空间没有开辟具体的队列空间,对于具体的队列空间是通过q1、q2去完成的,为此我们需要先释放队列,在释放存储队列的结构体(obj);
}
最后还是画图解释一下最后的销毁操作吧:
题目描述:
➡️挑战链接⬅️
分析:
大致思路呢,其实和上面一道题是一模一样的:
对于队列来说的话我们想要完成出队操作只需删掉队头元素就可以了,这里的队头元素相当于栈底元素,但是现在我们的数据是放在栈里面的,无法直接去删除,我们只有将栈底以前的元素全部全部抽离出来(并不是删除)才能直接接触到栈底的元素,至此我们只需要再对其进行出栈操作就算是完成了一次出队操作:当然根据上面的思路,这些栈底以前的数据我们可以抽离出来放在另一个栈里面:
进行完删除操作后,那么我们是不是还得将s2里面的数据倒回s1里面去,不然的在插入数据的时候就会照成顺序错误!!当然如果我们真的这么做的话效率可就太低了,你有没有注意到我们s1在完成出队的操作之后,就变为了空栈,我们直接在s1里面插入数据,在连接着s2一起看,你又没又注意到s2栈似乎扮演者队头的身份,s1扮演者队尾的身份,你看哈我们如果需要再次进行出队操作的话,是不是就只对s2进行出栈操作就好了,我们如果想要在删除数据的话是不是就直接在s1里面插入数据:也就是说原来在s1里面是栈底(以队列眼光来看就是队头),经过倒转到s2以后,原本在s1是栈底的数据就在s2中变为了栈顶的数据(以队列的眼光来看,原本是属于队头的数据处于栈底,无法完成出队操作,但是现在变为了s2的栈顶数据,相当于我们打通了封闭队头数据出去的大门,在s2中阻止队头数据出去的大门就不在了,我们就可以顺利的完成出队操作);
总结上面的言论:
1、我们将一个栈当作“入栈”数据的插入就放在这个栈里面;
2、我们将另一个栈当作“出栈”数据的删除就在这个栈里面进行;
3、当“出栈”里面没有数据时,就需要我们将“入栈”里面的原素全部倒进“出栈”;
就有点类似于黑洞和白洞的关系,一个只进东西,一个只出东西:
理论分析完了,下面就是代码实现:
typedef int DataType;
typedef struct Stack
{
DataType* nums;
int capcity;
int top;
}ST;
//初始化栈
void InitStack(ST* ps);
//销毁栈
void DestroyStack(ST* ps);
//入栈
void StackPush(ST* ps,DataType x);
//出栈
void StackPop(ST*ps);
//判断栈是否为NULL
bool StackEmpty(ST* ps);
//统计栈的元素
size_t StackSize(ST* ps);
//获取栈顶元素个数
DataType StackTop(ST*ps);
//初始化栈
void InitStack(ST* ps)
{
assert(ps);//防止乱传
ps->capcity = 0;
ps->top = 0;
ps->nums = NULL;
}
//销毁栈
void DestroyStack(ST*ps)
{
assert(ps);
ps->capcity = 0;
ps->top = 0;
free(ps->nums);
}
//检查扩容,不提供给用户,由程序自己完成
static void Check_Capcity(ST* ps)
{
assert(ps);
if (ps->capcity == ps->top)//需要扩容
{
int len = (ps->capcity == 0) ? 4 : ps->capcity * 2;
DataType* tmp = (DataType*)realloc(ps->nums,len*sizeof(DataType));
if (!tmp)
{
printf("realloc fail!\n");
exit(EXIT_FAILURE);
}
ps->nums = tmp;
ps->capcity = len;
}
}
//入栈
void StackPush(ST* ps,DataType x)
{
assert(ps);
Check_Capcity(ps);
ps->nums[ps->top] = x;
ps->top++;
}
//出栈
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));//判空
ps->top--;
}
//判断栈是否为NULL
bool StackEmpty(ST* ps)
{
assert(ps);
return !ps->top;
}
//统计栈的元素
size_t StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
//获取栈顶元素
DataType StackTop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));//栈不为空,我们才有元素获取;
return ps->nums[ps->top - 1];
}
typedef struct {
ST PushSt;//这个栈专门用来插入数据(入栈)
ST PopSt;//这个栈专门用来删除数据 (出栈)
} MyQueue;
bool myQueueEmpty(MyQueue* obj);
MyQueue* myQueueCreate() {
//创建结构体
MyQueue*q=(MyQueue*)malloc(sizeof(MyQueue));
InitStack(&q->PushSt);
InitStack(&q->PopSt);//初始化两个栈
return q;
}
void myQueuePush(MyQueue* obj, int x) {
//入队
assert(obj);//好习惯
//我们只需要在PushSt栈里面插入数据
StackPush(&obj->PushSt,x);
}
int myQueuePop(MyQueue* obj) {
//出队
assert(obj);
assert(myQueueEmpty(obj)==false);//判空
//只需要对PopSt栈进行操作即可
//当然如果PopSt栈里面没有元素了,我们就需要将PushSt里面的元素全部倒过来
/*if(StackEmpty(&obj->PopSt))
{
while(StackEmpty(&obj->PushSt)==false)
{
StackPush(&obj->PopSt,StackTop(&obj->PushSt));
StackPop(&obj->PushSt);
}
}
int top=StackTop(&obj->PopSt);
StackPop(&obj->PopSt);
return top;*/
//代码复用
int top= myQueuePeek(obj);
StackPop(&obj->PopSt);
return top;
}
int myQueuePeek(MyQueue* obj) {
assert(obj);
assert(myQueueEmpty(obj)==false);
//当然如果PopSt栈里面没有元素了,我们就需要将PushSt里面的元素全部倒过来
if(StackEmpty(&obj->PopSt))
{
while(StackEmpty(&obj->PushSt)==false)
{
StackPush(&obj->PopSt,StackTop(&obj->PushSt));
StackPop(&obj->PushSt);
}
}
//PopSt的栈顶元素就是队头元素
int top=StackTop(&obj->PopSt);
return top;
}
bool myQueueEmpty(MyQueue* obj) {
assert(obj);
return ((StackEmpty(&obj->PopSt))&&(StackEmpty(&obj->PushSt)));//两个栈同时为空队列才为空
}
void myQueueFree(MyQueue* obj) {
assert(obj);
DestroyStack(&obj->PopSt);
DestroyStack(&obj->PushSt);
free(obj);//与前一道题一样,必选先释放所开辟的栈,在释放当前结构体,否则会照成内存泄漏;
}
题目描述:
➡️挑战链接⬅️
分析:
嗯,循环队列,很好!我可以用循环链表来实现,嗯想法是不错的,下面我们来画图分析一下:
由于环形链表是固定大小的,所以链表的大小是开好的;
首先队列为空的条件是front==rear,然后呢rear指向的是待插入位置,这没问题吧,好那我现在不断插入数据,将这个环形链表插满:
我们可以发现此时队列满了的条件也是:front==rear, 这似乎就与我们判断队列为空的条件就似乎有点冲突了,我front==rear到底是表示队列已满嘞,还是队列此时为空嘞?这会引发歧义!!
那如何解决嘞?
1、第一种办法:我们在定义队列结构的时候,多定义一个size,专门用来记录队列长度的,入锅size==k那就说明队列满了,如果size==0,就说明时空队;这种解决办法比较简单,读者可以自行下去写一下相关代码;
2、第二种办法:
有大佬就提出我们在生成环形链表的时候在多开一个节点,需要k个节点,实际上我们开了k+1个节点;
此时队列为空的条件还是:front==rear;
而队列已满的条件就是rear->next==front
那么我们再来看看这样的结构是否满足其他操作的要求;
1、 出队:只要不为空队,front往后走就行,不需要释放节点,可以满足;
2、 入队:只要不为满队,在rear所指位置处填上数据,rear往后走就行,可以满足;
3、 获取队头元素:只要不为空队,直接返回front->val;即可,可以满足;
4、 获取队尾元素:我的rear指针指向的不是尾节点,而是尾节点的下一个节点,又是单链表结构,我们无法直接获取尾节点的元素,如果去遍历寻找的话,效率就很低,那该怎么办?我们可以在定义环形队列的结构时,多定义一个指针专门用来记录rear指针的前一个节点;
这样的话我们就把获取尾节点的元素的时间复杂度降到了O(1);这种办法呢?读者也可以自行实现一下:在初始化的时候可能代码会多一点(其他还好);!!!
上面链表给我们的最大麻烦就是无法直接获取尾元素地址,虽然上面的解决方法将获取尾元素
的时间复杂度降到了O(1),但是呢我认为还不够好!;
我们可不可以不加那个前指针直接就找到了尾指针!!
当然可以我们既然可以用链表构成循环队列,我们为什么不能用数组去构成循环队列呢?
当然我们的数组也需要开辟K+1个长度,不然判空和判满的条件会冲突!!
那么很自然的我队空的条件就是front==rear;
那么队满嘞?
通过大量的尝试和代入得出队满的条件:
(rear+1)%(k+1)==front;
现在我们来看一看是否能满足队列的基本操作:
1、出队:只要队不为空,front++;但是想这副图这样需要我们注意:
很明显我们向这种情况在出队的话,front明显越界了,理论上我们是希望front指向0号下标的:
因此我们在每一次front加加过后都应该判断一下front与k+1的关系,如果front==\k+1,front指针越界,需要我们修正,以此让其循环起来;若不等的话,则不需要修正;
2、入队,只要队未满,那么我们就只需在rear位置插入就行了,同时我们的rear也需要注意向上面的front一样的越界问题:
为此在rear加加结束过后我们也需要对其进行判断是否越界,以便及时修正rear,让队列循环起来;
3、获取队头元素:只要队不为空,直接返回front所指的元素;
4、获取队尾元素,只要队不为空,直接返回rear-1所指元素,应为我的rear始终指向的是我队尾元素的下一个:但是我们同样的rear-1越界的问题,比如下图:
很明显我们在返回队尾元素之前,需要对rear做一番检查,如果rear==0,则返回下标为k的元素,如果不等于,则就返回rear-1所指的元素!!;
好了以上是对循环队列的所有分析了,下面我们就利用代码来实现一下:
typedef struct {
int *nums;
int front;
int rear;
int k;//必须把这个带着,不然后期无法判满和循环
} MyCircularQueue;
void myCircularQueueFree(MyCircularQueue* obj);
MyCircularQueue* myCircularQueueCreate(int k);
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value);
bool myCircularQueueDeQueue(MyCircularQueue* obj);
int myCircularQueueFront(MyCircularQueue* obj);
int myCircularQueueRear(MyCircularQueue* obj);
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue*q=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
q->nums=(int*)malloc(sizeof(int)*(k+1));//实际开k+1个空间
q->front=0;
q->rear=0;
q->k=k;
return q;
}
bool myCircularQueueEnQueue(MyCircularQueue* q, int value) {
assert(q);//好习惯
if(myCircularQueueIsFull(q))//如果队列已满,则返回false,表示插入失败;
return false;
else
{
q->nums[q->rear]=value;
q->rear++;
q->rear=(q->rear==q->k+1)?0:q->rear;//判断rear指针是否越界
}
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* q) {
assert(q);
if(myCircularQueueIsEmpty(q))//如果队列为空就会删除失败
return false;
else
{
q->front++;
q->front=(q->front==q->k+1)?0:q->front;//判断一下front是否越界
}
return true;
}
int myCircularQueueFront(MyCircularQueue* q) {
assert(q);
if(myCircularQueueIsEmpty(q))//判空
return -1;
return q->nums[q->front];
}
int myCircularQueueRear(MyCircularQueue* q) {
assert(q);
if(myCircularQueueIsEmpty(q))//判空
return -1;
int index=(q->rear)?q->rear-1:q->k;
return q->nums[index];
}
bool myCircularQueueIsEmpty(MyCircularQueue* q) {
assert(q);
return ((q->front==q->rear));
}
bool myCircularQueueIsFull(MyCircularQueue* q) {
assert(q);
return ((q->rear+1)%(q->k+1)==q->front);
}
void myCircularQueueFree(MyCircularQueue* q) {
assert(q);
free(q->nums);
free(q);
}
以上便是博主对于三个经典例题的解释和说明了,感谢大家阅读!!