栈和队列面试题目
前言
因为我们使用的C语言做答,所以只能使用自己实现的栈和队列进行操作。
- 栈的题目,为了便于书写,插入操作和删除操作都会用push、pop代替
push: 插入操作
pop: 删除操作- 队列的题目
push:直接采用入队操作。
top:直接采用取队尾元素。
empty:判断两个队列为空。
如果有需要,这是栈和队列实现的博客链接:栈和队列的基本操作实现实现
这里前面三题都需要把前面栈或者队列的函数接口内容搬过来。我这里只写了解答,就不把栈或者队列的实现搬过来了,要不然内容太过冗余。前面有栈和队列的链接,有想法的可以点开,看一下。
一、括号匹配问题
题目链接:有效括号
1. 解题思路
思路:遇到三种左括号的任意一种进行压栈操作。如果字符不是三种左括号的任意一种,则取栈顶元素和该字符进行匹配,并且进行出栈操作。(如果看到这里有到一定的想法,可以自己操作一番)。
2. 画图分析
①示例1
②示例2
3. 代码
bool isValid(char * s)
{
Stack st;
StackInit(&st);
while (*s)
{
//这一步操作:如果字符是左括号就进栈
if ((*s == '(') || (*s == '{') || (*s == '['))
{
StackPush(&st, *s);
}
else
{
//如果字符串第一个字符是右括号,直接返回false
if (StackEmpty(&st))
{
//这里不能忘了对栈进行销毁操作,以防出现内存泄漏,做题网站是检查不出来内存泄漏的。下面也是一样,在返回的前面要对栈销毁。
StackDestroy(&st);
return false;
}
//取栈顶元素,并出栈
STDataType top = StackTop(&st);
StackPop(&st);
//如果需要判断匹不匹配建议,反过来写,这样操作步骤更少。如果满足条件了,就会跳过这个语句,开始下一个字符。
if((*s == ')' && top != '(')
||(*s == ']' && top != '[')
||(*s == '}' && top != '{'))
{
StackDestroy(&st);
return false;
}
}
//这里不能忘了,s++,因为是字符串,所以要继续向后访问
s++;
}
//判断是否为空,为空满足条件 为真,不为空则为假
bool ret = StackEmpty(&st);
StackDestroy(&st);
return ret;
}
二、用队列实现栈
题目链接:用队列实现栈
1. 解题思路
思路:建立两个不为空的队列
主要问题还是pop:在push操作的时候,插入不为空的队列,如果都为空,随便入一个队列。在pop的时候需要把不为空的队列n-1个倒入为空的队列,然后在对剩下的一个数据进行删除操作即可。
2. 画图分析
这里主要pop分析,队列中需要先有元素,然后才能进行pop
3. 代码
建议:把判断为空的接口,放在前面,这样易对其他函数进行断言。当然也可以把判断为空的声明放在前面。
typedef struct
{
//建立两个队列
Queue q1;
Queue q2;
} MyStack;
//返回一个栈的指针
MyStack* myStackCreate()
{
//malloc一个栈的指针
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
if (NULL == pst)
{
perror("MyStack::malloc");
return NULL;
}
//对栈进行初始化,这里对两个队列初始化即可
QueueInit(&pst->q1);
QueueInit(&pst->q2);
//返回栈的指针
return pst;
}
bool myStackEmpty(MyStack* obj)
{
assert(obj);
//判断是否为空,对两个队列一起判断
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
void myStackPush(MyStack* obj, int x)
{
assert(obj);
//push数据,push到不为空的队列。如果都为空,我这里的写法,会push到第二个队列
if (!QueueEmpty(&obj->q1))
{
QueuePush(&obj->q1, x);
}
else
{
QueuePush(&obj->q2, x);
}
}
//重中之重,删除操作
int myStackPop(MyStack* obj)
{
assert(obj);
assert(!myQueueEmpty(obj));
//因为我们也不知道哪一个为空,所以我们假设第一个队列为空,第二个队列不为空
Queue* empty = &obj->q1;
Queue* nonempty = &obj->q2;
//这里判断我们的假设是否成立,如果不成立交换一下,成立直接跳过该if语句
if (!QueueEmpty(&obj->q1))
{
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);
//想象一下,不就是取队尾元素,这里也就显现出来在实现队列的时候,为什么还要取队尾元素了
if (!QueueEmpty(&obj->q1))
{
return QueueBack(&obj->q1);
}
else
{
return QueueBack(&obj->q2);
}
}
void myStackFree(MyStack* obj)
{
assert(obj);
//直接对两个队列销毁就可以
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
//obj也是malloc所以这里也要释放
free(obj);
}
三、用栈实现队列
题目链接:用栈实现队列
1. 解题思路
大致思路:建立一个专门插入的栈和一个专门删除的栈。
push:直接在专门插入的栈插入即可
empty:判断两个栈为空
pop和peek相似
主要问题完成peek
在进行删除操作的时候,如果专门删除的栈为空,那么把专门插入的栈中的数据倒入到专门删除的栈。这是专门删除的栈的栈顶就是队尾。注意:每次只能专门删除的栈为空的时候才能再次进行数据的倒入。
2. 画图分析
栈中需要先push元素,然后才能进行pop
3. 代码
建议:把判断为空的接口,放在前面,这样易对其他函数进行断言。当然把判断为空的声明放在前面也可以。
//这是匿名结构体类型
typedef struct
{
//建立两个栈,一个专门push,一个专门pop
Stack pushS;
Stack popS;
} MyQueue;
MyQueue* myQueueCreate()
{
MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
if(NULL == pq)
{
perror("Create::pq");
return NULL;
}
//对该队列初始化,只需要初始化两个栈即可
StackInit(&pq->pushS);
StackInit(&pq->popS);
return pq;
}
// 判断队列是否为空
bool myQueueEmpty(MyQueue* obj)
{
assert(obj);
//两个栈都为空,则队列为空
return StackEmpty(&obj->pushS) && StackEmpty(&obj->popS);
}
// 入队操作
void myQueuePush(MyQueue* obj, int x)
{
assert(obj);
//直接无脑的向专门push的栈push
StackPush(&obj->pushS, x);
}
// 取队头元素
int myQueuePeek(MyQueue* obj)
{
assert(obj);
assert(!myQueueEmpty(obj));
//这里需要判断popS是否为空,如果为空要从pushS倒入数据
if(StackEmpty(&obj->popS))
{
//倒入数据,在倒入数据的过程中,把pushS中所有的数据全部倒入到popS中,并且每向popS倒入一个数据pushS栈就要删除一个数据
while (!StackEmpty(&obj->pushS))
{
StackPush(&obj->popS,StackTop(&obj->pushS));
StackPop(&obj->pushS);
}
}
//对头数据就是popS的栈顶数据
return StackTop(&obj->popS);
}
// 将队列开头的元素移除,并返回该元素。因为pop和peek实现相似,所以这里借用一下
int myQueuePop(MyQueue* obj)
{
assert(obj);
assert(!myQueueEmpty(obj));
//保存之后,再进行释放
int front = myQueuePeek(obj);
StackPop(&obj->popS);
return front;
}
// 销毁队列
void myQueueFree(MyQueue* obj)
{
assert(obj);
//因为队列中就两个栈,所以销毁栈就可以了
StackDestroy(&obj->pushS);
StackDestroy(&obj->popS);
//因为obj是malloc的所以也要释放,不然存在内存泄漏
free(obj);
}
四、设计循环队列
因为是对循环队列(环形队列)的实现,所以这里的写法就和上面三个问题的形式有所区别
1. 循环队列介绍
循环队列就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,循环队列的大小也是固定的。
题目链接:设计循环队列
2. 思路
大致思路:使用数组实现(也可以使用链表),主要解决判断队列中为满的情况。使用数组实现循环队列更简单一些,取模就行。
解决为满的情况:
- 多开辟一个空间。例如题目要求队列长度为k,则开辟k+1个空间。
- 也可以在结构体中维护一个计数,来判断循环队列为空还是为满,这两种方式都可以
因为使用数组实现,虽然不需要注意野指针问题,但是要注意数组越界的问题。
解决办法:
1.如果出现rear+1或者front+1的情况,这种都属于下标可能大于数组容量,则直接模队列的容量。在下面接口中插入和删除操作有例子。
2.如果出现rear-1的情况,这种可能会属于下标小于0,则需要加个队列的容量大小,然后再模个队列的容量大小。下面接口中判断是否为满的接口涉及该步骤。
下面进行画图分析和接口实现
3. 接口实现 (数组实现)
本处对接口进行了拆分方便介绍。将接口合在一起就是整个循环队列的实现。
需要详细介绍的接口我会画图分析,因为循环队列的长度是固定的,所以假设循环队列的大小为k = 3(一个约定)。方便画图分析
- 循环队列的结构
typedef struct
{
int* a; //存放数据
//建立下标用来访问数组数据
int front; //队头的下标
int rear; //队尾的下标
//取模的时候需要使用循环队列的大小
int k; //循环队列的大小,假设为3
//第二种方式判空判满,如果count=k,那就是满,如果为0就是空,
//所以即使队头和队尾的下标指向同一个位置,也可以通过这种方式判断队列空满
//int count = 0;
} MyCircularQueue;
- 为循环队列开辟一块空间
MyCircularQueue* myCircularQueueCreate(int k)
{
//创建结构体指针
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if (NULL == obj)
{
perror("Create::malloc");
return NULL;
}
//开辟数组大小,要比k大1,方便判断为满的情况
obj->a = (int*)malloc(sizeof(int) * (k+1));
if (NULL == obj->a)
{
perror("obj->a::malloc");
return NULL;
}
//初始化结构体成员
obj->front = obj->rear = 0;
obj->k = k;
//返回开辟的结构体指针
return obj;
}
下面都会使用这个图进行画图分析:(可以给它理解成连续的数组,使用链表更容易分析)
- 判断循环队列是否为空
建议:尽可能把下面函数需要的接口写在前面,例如下面写pop (DeQueue)接口的时候,需要判断循环队列是否为空,直接调用这个为空的接口就可以。
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
//断言,如果obj为空,就没意义了,下面断言处一样的道理
assert(obj);
//当队头和队尾的下标一样,就代表循环队列为空。这里需要返回一个bool值,为空返回true,不为空返回false
return obj->rear == obj->front;
}
- 判断循环队列是否为满
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
assert(obj);
//如下图所示两种为满的情况,结合在一起
return (obj->rear+1) % (obj->k+1) == obj->front;
}
- 循环队列插入操作,插入成功返回真
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
assert(obj);
//如果为满就无法插入,返回假
if (myCircularQueueIsFull(obj))
return false;
//正常插入操作
obj->a[obj->rear] = value;
obj->rear++;
//这里也需要取模,以防越界, 如下图所示:
obj->rear %= obj->k+1;
return true;
}
- 循环队列删除操作,删除成功,则返回真
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
assert(obj);
//为空,就无法删除,直接返回假
if (myCircularQueueIsEmpty(obj))
return false;
//删除操作,直接让队头的下标++,把原来的队头的空间还给操作系统就好。
obj->front++;
//这里也要取模,如下图所示:
obj->front %= obj->k+1;
return true;
}
7. 从队首获取元素,如果队列为空返回 -1
int myCircularQueueFront(MyCircularQueue* obj)
{
assert(obj);
//这一步比较简单,使用队列为空的接口判断,为空就返回-1。不为空,直接返回队头元素即可
if (myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[obj->front];
}
- 从队尾获取元素,如果队列为空返回 -1
这里需要注意一下,这里存在越界的风险,因为返回队尾元素实际是: obj->a[obj->rear-1],而rear-1就会存在越界的风险。所以这里需要加个 (k+1),然后再 %(k+1)。如下图所示:
int myCircularQueueRear(MyCircularQueue* obj)
{
assert(obj);
if (myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[(obj->rear + obj->k) % (obj->k+1)];
}
- 销毁循环队列
void myCircularQueueFree(MyCircularQueue* obj)
{
assert(obj);
//因为malloc了两次,所以释放两次,对数组的操作可以置空,对obj的操作不能在这里置空,可以在外部置空。因为传的是obj,除非传的是obj的地址,
free(obj->a);
obj->a = NULL;
free(obj);
}
总结
前面三题可以看出是对栈和队列的基础应用,而最后一题,是一种特殊的队列——循环队列。可以理解成是对前面栈和队列知识的一种拓展。