目录
一、有效的括号
二、用栈实现队列
三、用队列实现栈
四、设计循环队列
①数组的方式:
②链表的方式
声明:因为本次oj详解是用纯C来写的,所以说我们本期的题目需要手写栈和队列,为了代码的可读性增强也为了减少冗余,博主直接把本期oj题需要导入的栈和队列源码放在下面,在写题目的时候就不导入了,感谢谅解。
1、栈
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void StackInit(ST* ps);
void StackDestory(ST* ps);
void StackPush(ST* ps,STDataType x);
void StackPop(ST* ps);
STDataType StackTop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);
void StackInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//只能尾插
if (ps->top==ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newCapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity = newCapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
--ps->top;
}
STDataType StackTop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
2、队列
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 QueueDestory(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
void QueueDestory(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* del = cur;
cur = cur->next;
free(del);
}
pq->head = pq->tail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
else
{
newnode->next = NULL;
newnode->data = x;
}
if (pq->tail == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
//head为null,tail为野指针
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* del = pq->head;
pq->head = pq->head->next;
free(del);
del = NULL;
}
pq->size--;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL && pq->tail == NULL;
}
int QueueSize(Queue* pq)
{
assert(pq);
//求size
/*QNode* cur = pq->head;
int size = 0;
while (cur)
{
++size;
cur = cur->next;
}
return size;*/
return pq->size;
}
注意:博主放的栈和队列的源码所存储的数据类型都是int类型,对于不同的题目有不同的数据类型要求需要更改注意一下。
来源:leetcode:20、有效的括号
1、题目概述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例:
输入:s = "()" 输出:true输入:s = "()[]{}" 输出:true输入:s = "(]" 输出:false
2、题目分析
总共有三对括号:"()" "[]" "{}"。
如果刚开始入的是右括号,显然不可能凑成一对括号,如果是左括号我们需要去存下来,如果下一个还是左括号显然不可能凑成一对括号,如果是右括号则要判断是否配对。
总结:
1、我们需要一个容器去存左括号,然后当遇到右括号我们需要去拿这个容器的最后一个元素去和它配对。
2、比较适合的就是用栈去存左括号,遇到右括号就取出栈顶元素去配对。
3、代码编译
bool isValid(char * s)
{
ST STMatch;
StackInit(&STMatch);
while(*s)
{
if(StackEmpty(&STMatch))//空的时候右括号绝对不能遇到
{
if(*s!='('&&*s!='{'&&*s!='[')
{
return false;
}
else
{
StackPush(&STMatch,*s);
}
}
//非空
else
{
if(*s==']'||*s==')'||*s=='}')
{
//右括号就要取栈顶去配对,然后出栈
if(*s==']'&&StackTop(&STMatch)=='['
||*s=='}'&&StackTop(&STMatch)=='{'
||*s==')'&&StackTop(&STMatch)=='(')
{
StackPop(&STMatch);
}
else
{
return false;
}
}
else
StackPush(&STMatch,*s);
}
s++;
}
bool flag=StackEmpty(&STMatch);
StackDestory(&STMatch);
return flag;
}
用C来写的话,比较麻烦的是我们需要去导入一个栈,理清思路这道题目并不困难。还有一点注意这里的数据类型是字符而不是整型,所以如果用博主上面的栈的话注意更改数据类型。
来源:leetcode:232、用栈实现队列
1、题目概述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false这道题目就是让我们用两个后入先出的栈实现一个先入先出的队列。
2、题目分析
拿到这道题目不要慌,我们破解这道题目的关键在于明白栈的基本功能和接口有哪些,以及要我们实现的队列的接口有哪些。
题目要求我们用两个栈去实现队列的接口:
1、void myQueuePush(MyQueue* obj, int x) :实现插入的功能,也就是尾插
2、int myQueuePop(MyQueue* obj) :先入先出,pop出头。
3、int myQueuePeek(MyQueue* obj) :返回队列开头的元素,也就是找到队列的头
4、bool myQueueEmpty(MyQueue* obj) :判空
图示解析:
1、 左边是博主列出的栈的基本功能,其他功能的实现都好说,主要是如何实现队列的先入先出,这是一个问题,因为栈是后入先出的,只能出尾,所以我们需要借助另一个栈去倒数据,即把一个栈的数据Pop出,然后再push入另一个栈,这时原来的头就变成了尾,再pop出栈顶元素,就实现了先入先出。
2、那么解决了先入先出的问题,我们还有一个问题需要解决,就是在push的时候入哪个栈,以及每次Pop是否都需要去倒数据?
简单分析一下,我们就会发现,我们可以设定一个栈,栈1用于push,一个栈,栈2用于pop最优,只要push就直接入栈1,当栈2没有空的时候,我们是不需要去把栈1的数据倒入栈2的,当栈2空的时候,再去倒栈1入栈2.
3、代码编译
typedef struct
{
ST st1;
ST st2;
} MyQueue;
MyQueue* myQueueCreate()
{
MyQueue* obj=(MyQueue*)malloc(sizeof(MyQueue));
StackInit(&obj->st1);
StackInit(&obj->st2);
return obj;
}
void myQueuePush(MyQueue* obj, int x)
{
//因为它出栈时并不是找不空的出栈,所以这里指定一个栈用于push,一个栈用于pop
StackPush(&obj->st1,x);
}
int myQueuePop(MyQueue* obj)
{
if(StackEmpty(&obj->st2))
{
while(!StackEmpty(&obj->st1))
{
StackPush(&obj->st2,StackTop(&obj->st1));
StackPop(&obj->st1);
}
}
int front=StackTop(&obj->st2);
StackPop(&obj->st2);
return front;
}
int myQueuePeek(MyQueue* obj)
{
if(StackEmpty(&obj->st2))
{
while(!StackEmpty(&obj->st1))
{
StackPush(&obj->st2,StackTop(&obj->st1));
StackPop(&obj->st1);
}
}
return StackTop(&obj->st2);
}
bool myQueueEmpty(MyQueue* obj)
{
return StackEmpty(&obj->st1)&&StackEmpty(&obj->st2);
}
void myQueueFree(MyQueue* obj)
{
StackDestory(&obj->st1);
StackDestory(&obj->st2);
free(obj);
obj=NULL;
}
来源:leetcode:225、用队列实现栈
1、题目概述
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
2、题目分析
这道题目和上面的用栈实现队列是姊妹题。也比较类似,我们需要注意的是Pop的实现,即先入先出的队列如何实现后入先出的问题。
因为队列先入先出,也就是尾入头出,我们要想让尾先出,就要把尾调整到头,怎么调整呢?我们借助一个队列把它的元素一个一个pop出,再push到另一个队列中去,直到只剩一个元素,这时尾来到了头,拿到这个元素再pop就实现了后入先出。
需要注意的是,另一个队列的元素仍是原本的元素,有序的,只不过少了尾,而另一个队列已经空了。
所以每次push的时候我们需要去找非空的队列,每次pop的时候我们需要把非空的队列倒到另一个队列中。
3、代码编译
typedef struct
{
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate()
{
MyStack* obj=(MyStack*)malloc(sizeof(MyStack));
QueueInit(&obj->q1);
QueueInit(&obj->q2);
return obj;
}
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))//如果q1不空
{
Empty=&obj->q2;
NonEmpty=&obj->q1;
}
while(QueueSize(NonEmpty)>1)
{
QueuePush(Empty,QueueFront(NonEmpty));
//往空里面压
QueuePop(NonEmpty);
}
int tail=QueueFront(NonEmpty);//后入先出
QueuePop(NonEmpty);
return tail;
}
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)
{
QueueDestory(&obj->q1);
QueueDestory(&obj->q2);
free(obj);
obj=NULL;
}
1、因为用栈实现队列,和用队列实现栈很类似,它们在判空的时候都需要判断两个栈或者两个队列是否为空。
2、用队列实现栈这道题目在实现Pop这个接口的时候,博主利用了一点小技巧,因为这个接口需要先判断哪个队列非空,对于非空的队列倒数据,如果是队列1非空,需要倒数据,如果队列2非空,需要倒数据,而这么编写代码是比较冗余重复的,博主这里就假设队列1为空,队列2非空,如果实际相反再进行调换,这样转换成非空和空队列操作,代码量大大减少。
来源:leetcode:622、设计循环队列
1、题目描述
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
2、题目分析
循环队列,本质还是实现一个队列,实现这个队列的基本几个接口,只不过这个队列是有限长度的,并且这个队列是循环的。
这个循环在我们的理解大概是个环状,这里要怎么实现呢?用数组还是链表来实现呢?乍一看,这个循环队列和我们循环链表的概念很相似,是否链表就优于数组呢?别着急,我们再来细品一下:
我们先图解看一下使用链表和数组的图解:
结合这个图解,博主把每个接口需要的功能先大致分析一下:
1、Front(队首) 和Rear(队尾)这两个接口比较简单,就是直接使用两个指针指向队首和队尾,然后返回就可以了。队首两种方式都可以,队尾的话如果是数组,直接找下标就能找到队尾的下标然后返回,(不过这一点还有一点细节需要注意,如果没发现还是会出错,博主后面还会说)但是链表,因为链表的尾指针指向的位置是没有填数据的,也就是队尾的next,所以需要遍历找到尾指针的上一个位置,比较麻烦。
2、enQueue(value): 向循环队列插入一个元素,这个函数也比较简单,就是尾插,我们设有一个尾指针,插入然后++就可以了。数组唯一需要注意的就是
3、deQueue(value):从循环队列删除一个元素,队列先入先出的功能,就是头删,头指针++。
4、isEmpty(): 检查循环队列是否为空。isFull():判断循环队列是否满了,这两个问题比较有趣
分析一下我们就会发现,判空和判满无法区分,怎么解决呢?
我们创造性的提出了一种方式,类似于链表中增加一个哨兵位,我们多加一个位置,但是不填数据。比如:
我们发现,当我们加了一个位置的时候,这时候为空和为满的情况是不相同的,这时候是可以区分开两种情况的,说明增加一个位置确实是可行的,而且这个位置是不断变化的。
上面的图是数组的情况,这里需要处理的细节就是back如何和front建立联系,以及当back在尾的时候怎么回到头,这一点也和博主上面说的插入的数组需要考虑的情况相符合。这些都是数组需要考虑的。我们再来设想链表的话,似乎就不需要考虑这些问题,只需要back的next指针是否为front即可。
3、代码编译
博主用数组和链表都实现了一遍,都可以,各有利弊吧。
//数组去写
typedef struct
{
int*a;
int front;
int back;
int capacity;
} MyCircularQueue;
bool myCircularQueueIsFull(MyCircularQueue* obj);
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
int* tmp=(int*)malloc(sizeof(int)*(k+1));
if(tmp==NULL)
{
perror("malloc fail");
return NULL;
}
else
{
obj->a=tmp;
obj->front=obj->back=0;
obj->capacity=k+1;
}
return obj;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
//插入元素
if(!myCircularQueueIsFull(obj))
{
//如果没满
obj->a[obj->back]=value;
obj->back++;
obj->back%=obj->capacity;//主要是要考虑到到了最后一个位置
return true;
}
else
{
return false;
}
}
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
if(!myCircularQueueIsEmpty(obj))
{
//没空才能删
obj->a[obj->front]=0;//虽然没啥意义
obj->front++;
obj->front%=obj->capacity;
return true;
}
else
{
return false;
}
}
int myCircularQueueFront(MyCircularQueue* obj)
{
if(!myCircularQueueIsEmpty(obj))
return obj->a[obj->front];
else
return -1;
}
int myCircularQueueRear(MyCircularQueue* obj)
{
if(!myCircularQueueIsEmpty(obj))
return obj->a[(obj->back-1+obj->capacity)%obj->capacity];//要考虑到back在第一个的情况
else
return -1;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
return obj->front==obj->back;
}
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
return (obj->back+1)%obj->capacity==obj->front;
}
void myCircularQueueFree(MyCircularQueue* obj)
{
free(obj->a);
obj->front=obj->back=0;
obj->capacity=0;
free(obj);
obj=NULL;
}
理解了上面的图解,用数组的方式来实现代码还是比较简单的,不过需要注意颇多的细节,
博主认为这些地方都是不易想到,需要注意的,而且可以说是点睛之笔。
1、第一个%主要是,当最后一个位置被填之后,如何回到队首,这个%可以说十分巧妙,直接回到队首。
3、而第三个找队尾元素,这样设计的原因是要考虑到,如果back在队首,那么队尾肯定是最后一个位置的元素,怎么找到呢?这种方式可以说很好的解决了这个问题。
4、而第四个则是为了解决判满的问题,当满的时候。back的下一个位置就是front,这样设计是为了解决back在最后一个位置,而front在第一个位置的情况。
这样操作不仅解决特殊情况,而且具有普适性,十分实用,但也不易被想到。
typedef struct MCQueue
{
int data;
struct MCQueue* next;
} MCQueue;
typedef struct
{
MCQueue* front;
MCQueue* back;
}MyCircularQueue;
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);
MCQueue* BuyNewnode()
{
MCQueue* newnode=(MCQueue*)malloc(sizeof(MCQueue));
if(newnode==NULL)
{
perror("malloc fail");
exit(-1);
}
else
{
newnode->next=NULL;
}
return newnode;
}
MyCircularQueue* myCircularQueueCreate(int k)
{
//初始化
MCQueue* CQueue=NULL;
MCQueue* tail=NULL;
int N=k+1;//多加一个位置
//构建链表
while(N--)
{
MCQueue* newnode=BuyNewnode();
if(CQueue==NULL)
{
CQueue=newnode;
//方便找尾
tail=CQueue;
}
else
{
//头插的一种方式
newnode->next=CQueue;
CQueue=newnode;
}
tail->next=CQueue;
//链接起来
}
MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->front=CQueue;
obj->back=CQueue;
return obj;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
if(!myCircularQueueIsFull(obj))
{
obj->back->data=value;
//指向下一个
obj->back=obj->back->next;
return true;
}
else
return false;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
//删的前提是没空
if(!myCircularQueueIsEmpty(obj))
{
obj->front=obj->front->next;
return true;
}
else
return false;
}
int myCircularQueueFront(MyCircularQueue* obj)
{
if(!myCircularQueueIsEmpty(obj))
{
return obj->front->data;
}
else
{
return -1;
}
}
int myCircularQueueRear(MyCircularQueue* obj)
{
//这个比较麻烦,需要找到back的前一个
if(!myCircularQueueIsEmpty(obj))
{
MCQueue* cur=obj->front;
while(cur->next!=obj->back)
{
cur=cur->next;
}
//找到前一个
return cur->data;
}
else
return -1;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
return obj->front==obj->back;
}
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
//如何判满?
return obj->back->next==obj->front;
}
void myCircularQueueFree(MyCircularQueue* obj)
{
MCQueue* cur=obj->front->next;
while(cur!=obj->front)
{
MCQueue* next=cur->next;
free(cur);
cur=next;
}
free(cur);
free(obj);
}
从逻辑上来讲,链表更符合循环链表的逻辑,不过用链表比较怪怪的,但是构建就比较麻烦,需要一个一个链表去开创,还要去链接起来,更古怪的是这些链表什么数据也不放,不太合习惯,而且用链表来实现比较恶心的有两点:
1、链表的初始化和链接,以及开创之后,因为这是一个环形链表,而且没有下标,那么哪个是头,哪个是尾需要你自己去定义。
2、在找尾的时候,需要再次遍历一下,时间复杂度是0(N)。
优点就是:
不需要去特殊处理数组需要处理的那些情况,初始化之后写起来很顺。
好了,本期的oj分享就到这里了,希望大家可以养成良好的编程习惯,多画图分析,争取一次过,而不是修修补补。