栈与队列常见习题分析
1.括号匹配问题
接下来我们分析一下这道题存在的几种情况:
1.字符串中只存在左括号或者是右括号:
此时,肯定是不匹配的。
2.字符串中既有左括号,也有右括号,但是不一定全部的左括号都有对应的右括号:
此时左括号与右括号也是不匹配的。
3.字符串中左括号右括号的数量相对应,但是不是以正确的顺序闭合:
此时也是不匹配的。
那么这道题思路应该是怎样的呢?其实这儿我们就可以用到栈先入后出的思想了,如果此时是左括号,我们就入栈,字符串就右移一位,如果遇见了右括号,就出栈,然后比较此时左右括号是否匹配就可以了:
代码实现:
bool isValid(char * s){
Stack st;
StackInit(&st);
while(*s)
{
//如果是左括号,就入栈
if(*s == '(' || *s == '[' || *s == '{')
{
StackPush(&st, *s);
s++;
}
//如果不是左括号,就开始判断
else
{
//如果此时栈里面为空,说明没有左括号存在,就返回false;
if(StackEmpty(&st))
{
return false;
}
else
{
//栈顶元素出栈,判断是否匹配
char top = StackTop(&st);
if(top == '(' && *s != ')'
|| top == '{' && *s != '}'
|| top == '[' && *s !=']')
{
StackPop(&st);
return false;
}
else
{
StackPop(&st);
s++;
}
}
}
}
//如果此时栈里面不为空,就不匹配
if(!StackEmpty(&st))
{
return false;
}
//为空就匹配
else{
return true;
}
//最后销毁
StackDestory(&st);
}
2.用队列实现栈
我们通过学习可以知道栈是一种先入后出的结构,队列是队头出,队尾入的结构,那么如何用队列来实现栈呢?
首先我们创建两个队列,然后用这两个队列来模拟栈的实现:
typedef struct {
Queue q1;
Queue q2;
} MyStack;
//栈的创造
MyStack* myStackCreate() {
MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&obj->q1);
QueueInit(&obj->q2);
return obj;
}
2.1 元素的入栈
此时我们模拟实现的是栈,插入就需要从栈顶入,而队列入元素是从队尾入,他们性质都是一样的,但是此时有两个队列,我们该入那个呢?在这儿就必须保证有一个队列是空的,如果两个队列里都有元素,我们无法进行下一步了。
如果q1为空,就从q2入,如果q2为空,就从q1入,如果俩都为空,从谁入都无所谓:
代码实现:
void myStackPush(MyStack* obj, int x) {
if(!QueueEmpty(&obj->q1))
{
QueuePush(&obj->q1, x);
}
else
{
QueuePush(&obj->q2, x);
}
}
2.2 元素的移除并返回栈顶元素
因为栈每次出栈都是栈顶元素先出,我们此时就可以将q2的前n-1个元素全部插入q1中,此时q2就只剩队尾的元素了,再将队尾的元素拿出来,就相当于是栈顶出,此时q2就为空了,如果还需要继续出,依然重复刚才操作,只不过是q1,q2的位置换了而已:
代码实现:
int myStackPop(MyStack* obj) {
Queue* empty = &obj->q1;
Queue* noempty = &obj->q2;
//判断是空队列
if(!QueueEmpty(&obj->q1))
{
empty = &obj->q2;
noempty = &obj->q1;
}
while(QueueSize(noempty) > 1)
{
QueuePush(empty,QueueFront(noempty));
QueuePop(noempty);
}
int top = QueueFront(noempty);
QueuePop(noempty);
return top;
}
2.3 返回栈顶元素
其实就是将队尾的元素返回,q1为空时返回q2的队尾元素,q2为空时返回q1的队尾元素:
代码实现:
int myStackTop(MyStack* obj) {
if(!QueueEmpty(&obj->q1))
{
return QueueBack(&obj->q1);
}
else
{
return QueueBack(&obj->q2);
}
}
2.4 判断栈是否为空
也就是判断两个队列是否为空,两个队列都为空了,就为空:
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
2.5 销毁栈
也就是将两个队列销毁,在将我们的栈销毁
void myStackFree(MyStack* obj) {
QueueDestory(&obj->q1);
QueueDestory(&obj->q2);
free(obj);
}
3.用栈实现队列
栈是先入后出,队列是队头出,队尾入,又怎么样用栈来实现队列呢?
3.1 元素的插入
其实入很简单,我们只要保证从非空的那个栈里面入就可以了,这样就能保证入的元素肯定是处于栈顶,所以我们两个栈一个可以设计为专门入栈,另一个专门出栈:
typedef struct {
Stack pushst;//专门入栈
Stack popst;//专门出栈
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue)) ;
StackInit(&obj->pushst);
StackInit(&obj->popst);
return obj;
}
void myQueuePush(MyQueue* obj, int x) {
StackPush(&obj->pushst, x);
}
3.2 从开头移除队列元素并返回
因为栈是先入后出,如果要模拟出队列性质,就需要最先将栈底的元素拿出来,我们就可以考虑将pushst里的元素挨个入进popst里面,这样就在pushst里面最底的元素在popst里面就成为了最顶元素,如果最开始popst不为空,我们就直接移除就可以了,就可以模拟出先入先出的性质:
代码实现:
int myQueuePop(MyQueue* obj) {
//判断popst是否为空
if(StackEmpty(&obj->popst))
{
while(!StackEmpty(&obj->pushst))
{
//将pushst的元素挨个放入popst里面
StackPush(&obj->popst,StackTop(&obj->pushst));
//将pushst里的元素移除
StackPop(&obj->pushst);
}
}
//获取popst里栈顶元素
int top = StackTop(&obj->popst);
StackPop(&obj->popst);
return top;
}
3.3 返回队列开头元素
其实就是返回popst里面最顶的元素:
代码实现:
int myQueuePeek(MyQueue* obj) {
//判断popst是否为空
if(StackEmpty(&obj->popst))
{
while(!StackEmpty(&obj->pushst))
{
//将pushst的元素挨个放入popst里面
StackPush(&obj->popst,StackTop(&obj->pushst));
//将pushst里的元素移除
StackPop(&obj->pushst);
}
}
return StackTop(&obj->popst);
}
3.4 判断队列是否为空
就是判断popst 与 pushst是否都为空:
代码实现:
bool myQueueEmpty(MyQueue* obj) {
return StackEmpty(&obj->pushst)
&& StackEmpty(&obj->popst);
}
3.5 销毁队列
也就是销毁掉popst与pushst,在销毁掉队列:
代码实现:
void myQueueFree(MyQueue* obj) {
StackDestory(&obj->pushst);
StackDestory(&obj->popst);
free(obj);
}
4.循环队列实现
首先我们得理解循环队列是什么?
循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
1.当head == tail 时,这就是一个空的循环队列。
2.当head == tail + 1时,这就是一个满的循环队列;
我们在开辟循环队列时,总会多开辟一个空间,为什么呢?
因为如果不多开辟一个空间,判空和判满容易混淆,比如说下面这种情况,在tail位置插入一个元素,tail就要向后移动,此时head == tail,但他却是满的:
4.1 初始化队列
循环队列既可以用数组实现,也可以用链表实现,但是链表实现的话不容易找到尾,所以在这儿我们使用数组来实现:
typedef int CTDataType;
typedef struct CQueue
{
CTDataType* data;//存储元素队列
int head;//循环队列头
int tail;//循环队列尾
int size;//存储数据个数
} CQueue;
//假设队列长度为k
void CQueueInit(CQueue* ps, int k)
{
assert(ps);
ps->data = (CTDataType*)malloc(sizeof(CTDataType) * (k + 1));
ps->head = ps->tail = 0;
ps->size = k;
}
4.2 向循环队列中插入一个元素
插入过程中如果没满,就很容易,插入一个,tail向后挪动就可以,但是会出现两种情况:
1.此时tail并不是在下标为k的位置,继续插入的话tail向后挪动就可以:
2.当tail == k时,在插入时tail就会向后挪动,此时tail == k+1就会越界:
此时我们修正一下tail的位置,让tail == 0,就又可以继续插入了:
代码实现:
//队列插入x,如果插入成功,就返回真
bool CQueuePush(CQueue* ps, int x)
{
assert(ps);
//判断队列是否满了,没满,就插入
if (!CQueueFull(ps))
{
//值赋给tail位置
ps->data[ps->tail] = x;
//tail指向下一个位置
ps->tail++;
//如果此时的tail == k+1
if (ps->tail == ps->size + 1)
{
//修正一下tail的位置
ps->tail = 0;
}
//插入成功
return true;
}
//如果队列满了,就插入失败
else
{
return false;
}
}
当然,当tail == k时我们修正也是可以的:
//判断队列是否满了,没满,就插入
if (!CQueueFull(ps))
{
//值赋给tail位置
ps->data[ps->tail] = x;
//如果此时的tail == k
if (ps->tail == ps->size)
{
//修正一下tail的位置
ps->tail = 0;
}
else//如果tail != k,tail就直接++
{
ps->tail++;
}
//插入成功
return true;
}
//若果队列满了,就插入失败
else
{
return false;
}
4.3 从循环队列中删除一个元素
删除元素也会出现两种情况:
1.head并没有在下标为k的位置,head只需要向后挪动就可以了:
2.head此时位置在下标为k的位置,head往后挪动一位,就越界了:
我们修正一下head的位置,令head == 0,就可以了:
代码实现:
//队列删除一个元素,如果删除成功,就返回真
bool CQueuePush(CQueue* ps, int x)
{
assert(ps);
//判断是否为空,不为空进行删除
if (!CQueueEmpty(ps))
{
//head向后挪动位置
ps->head++;
//如果此时head == k+1
if (ps->head == ps->size + 1)
{
//修正一下head的值
ps->head = 0;
}
return true;
}
//为空,就返回假
else
{
return false;
}
}
同样也可以写成:
//判断是否为空,不为空进行删除
if (!CQueueEmpty(ps))
{
//如果此时head == k
if (ps->head == ps->size + 1)
{
//修正一下head的值
ps->head = 0;
}
//此时head != k,就直接++
else
{
ps->head++;
}
return true;
}
//为空,就返回假
else
{
return false;
}
4.4 获取队顶元素
获取队顶元素就简单了,还接返回head位置的值就可以了:
代码实现:
//获取队顶元素
bool CQueueFront(CQueue* ps)
{
assert(ps);
//如果不为空,就返回head位置的值
if (!CQueueEmpty(ps))
{
return ps->data[ps->head];
}
//为空,直接返回-1;
else
{
return -1;
}
}
4.5 获取队尾元素
获取队尾元素就直接返回tail - 1位置的值,此时就会出现两种情况了:
1.tail并不处于下标为0的位置,此时直接返回tail - 1位置的值:
2.tail处于下标为0的位置,此时tail - 1位置是下标为k的位置,返回的是下标为k的位置的值:
//获取队尾元素
bool CQueueRear(CQueue* ps)
{
assert(ps);
//如果不为空,就返回tail-1位置的值
if (!CQueueEmpty(ps))
{
//tail == 0,就返回k位置的值;
if (ps->tail == 0)
{
return ps->data[ps->size];
}
//tail != 0,返回tail-1位置的值
else
{
return ps->data[ps->tail - 1];
}
}
//为空,直接返回-1;
else
{
return -1;
}
}
还可以写成:
//如果不为空,就返回tail-1位置的值
if (!CQueueEmpty(ps))
{
int tailprev = ps->tail - 1;
if (tailprev == -1)
{
tailprev = ps->size;
}
return ps->data[tailprev];
}
//为空,直接返回-1;
else
{
return -1;
}
4.6 检查循环队列是否已满
循环队列满的条件是tail + 1 == head ,我们只需要将tail+1 == k+1这个条件考虑进去,然后直接返回就可以了:
//检查循环队列是否已满
bool CQueueFull(CQueue* ps)
{
assert(ps);
int tailnext = ps->tail + 1;
if (tailnext == ps->size + 1)
{
tailnext = 0;
}
return tailnext == ps->head;
}
4.7 检查循环队列是否为空
队列为空的条件就是head == tail,直接返回就行了:
//检查循环队列是否已满
bool CQueueEmpty(CQueue* ps)
{
assert(ps);
return ps->head == ps->tail;
}
4.8 销毁队列
void CQueueDestory(CQueue* ps)
{
assert(ps);
free(ps->data);
ps->head = ps->tail = 0;
ps->size = 0;
}