用队列实现栈,点我开始做题
注意,文章使用的队列,及提到的队列函数,需要自己实现,详情看之前的文章队列的实现,
点击我开始做题
点我开始学习队列
这就是考察了我们队栈和队列的认识
- 队列:先进先出
- 栈:后进先出
思路讲解
我们用2个队列怎么实现出栈?
- 假设我们队列里,存放的1 2 3 4
- 按照队列的性质,先出的数据是1
- 目标是模拟栈的性质,那我们就要先让4出
为了实现这个,我们可以利用另一个队列来回倒数据,要先出4,可以先将1 2 3放到另一个队列中
这里就实现了栈的后进先出的性质
我们用2个队列怎么实现入栈?
- 还是这张图,我们已经将4出栈了
- 如果要入栈,数据肯定要放入有数据的队列中
- 如果2队列都没有数据,就随便放一个队列中
定义结构体
题目这样给的,意思是让你将2个队列放到MyStack结构体中,方便定义
typedef struct { Queue q1; Queue q2; } MyStack;
值得注意的是,这个是匿名结构体的定义方法
初始化
- 题目给的模板的意思是,创造一个MyStack变量,这样你就拥有2个队列了
- 也就是我们的的初始化结构体
- 我们能不能这样创建初始化?
- 不能,我们定义的obj是在函数里的,出了作用域就销毁了,但是我们仍然返回obj的地址,这不是野指针吗?
- 为了解决这个,我们可以malloc指针来接收,malloc出来的空间是在堆上了出作用域不会销毁
MyStack* myStackCreate() { MyStack* obj = (MyStack*)malloc(sizeof(MyStack)); if (!obj) { return NULL; } queueinit(&obj->q1); queueinit(&obj->q2); return obj; }
- 注意,我们创建obj指针的时候,也就是创建了2个Queue结构体,其结构体中有首尾指针,我们也要给这2指针初始化,不然就是野指针
- 我们传的实参是要取地址的,因为你创建MyStack的时候,是定义了变量,不是指针
注意,如果为了在传参的时候不取地址,我们创建MyStack的时候定义指针行不行?
即
可以是可以,但这样要考虑的问题太多了
- 在结构体里定义Queue指针,那q1和q2就是没有初始化的野指针
- 将q1和q2传递给QueueInit函数初始化,不就是对野指针解引用访问吗
- 也就是说,我们只有结构体指针,没有结构体
- 为了解决这个问题,我们必须在初始化前创建结构体出来
> 这样就要free3次,malloc几次释放几次
这就是2种方法的区别,显然第一种好太多
入栈
入栈的逻辑很简单,哪个不为空,往哪个入栈,都为空指定一个队列进行入栈
void myStackPush(MyStack* obj, int x) {
if (!queueempty(&obj->q1))
{
queuepush(&obj->q1, x);
}
else
{
queuepush(&obj->q2, x);
}
}
出栈并返回栈顶元素
题目要求是出栈的同时返回数据
代码逻辑:
-
出不为空的队列中的数据,放到另一个队列中,直到剩一个
我们不需要关心是哪个队列不为空,可以用假设法来定义变量即可
-
这一个就是我们要出栈的数据
出到还剩一个,这个就是要返回并删除的数据
int myStackPop(MyStack* obj) {
Queue* noempty = &obj->q1;
Queue* empty = &obj->q2;
if (queueempty(&obj->q1))
{
noempty = &obj->q2;
empty = &obj->q1;
}
while (queuesize(noempty) > 1)
{
queuepush(empty, queuefront(noempty));
queuepop(noempty);
}
int top = queuefront(noempty);
queuepop(noempty);
return top;
}
返回栈顶元素
我们想一想,队列的尾是不是就是栈顶元素,这个时候我们的back函数是不是有用了
int myStackTop(MyStack* obj) {
Queue* noempty = &obj->q1;
Queue* empty = &obj->q2;
if (queueempty(&obj->q1))
{
noempty = &obj->q2;
empty = &obj->q1;
}
return queueback(noempty);
}
判空
bool myStackEmpty(MyStack* obj) {
return queueempty(&obj->q1)
&& queueempty(&obj->q2);
}
free函数
void myStackFree(MyStack* obj) {
queuedestroy(&obj->q1);
queuedestroy(&obj->q2);
free(obj);
}
- 我们只释放obj行不行?
- 不行,如果只释放obj,相对于释放了q1和q2,那q1和q2里面的首尾指针怎么办?
用栈实现队列,
这就是考察了我们队栈和队列的认识
- 队列:先进先出
- 栈:后进先出
思路讲解
我们用2个栈怎么实现出队列?
- 假设我们栈a里面,存放数据1 2 3 4(1 2 3 4顺序进栈),怎么实现队列一样的先进先出?
- 可以将数据全部倒到栈b里面
-
在栈b中依次出顺序即可
-
这样我们就做到了在栈b出数据,是先进先出了的
-
值得注意的是,我们再将栈b的数据倒到栈a里面顺序会变的,不是跟以前队列模拟栈一样了
我们用2个栈怎么实现入队列?
- 还是这个图为例,这个时候假设我们要再入数据 5 6,怎么入?
- 肯定不能入栈b里面去,这样出队列的顺序就改变了
- 只能将数据入到栈a里面
- 来回几次就会发现,出数据和入数据是在2个不同的栈里实现的
- 我们不妨定义专门的出栈,专门的入栈
- 当然,我们还可以将数据再倒回栈a里面,再倒到栈b里面,这样不就很麻烦了吗
要出队列就专门将数据倒到popst里面,入数据就放入pushst里面
定义结构体,初始化
这里和我们的用2队列实现栈的代码一样,并且注意点都一致,这里不再叙述,详情可以看之前的文章
typedef struct {
st pushst;
st popst;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
if (!obj)
{
return NULL;
}
stinit(&obj->pushst);
stinit(&obj->popst);
return obj;
}
入队列
入队列的逻辑非常简单,我们只需要往pushst里面无脑入即可
void myQueuePush(MyQueue* obj, int x) {
stpush(&obj->pushst, x);
}
peek函数,返回队列头元素
题目提供了这2个函数
- pop函数是删除队列头元素,并且返回改元素
- peek函数是单纯返回队列头元素
为了,代码的复用性,我们是不是可以先定义peek函数,到时候直接在pop函数复用即可
代码逻辑也很简单,当popst为空的时候,就在pushst里面倒数据,不为空的时候就直接返回队列头元素
- 注意,在往popst里面倒数据的时候,要在pushst里面删除数据
- 对于pushst来说就是出数据然后删除
int myQueuePeek(MyQueue* obj) {
if (stempty(&obj->popst))
{
while (!stempty(&obj->pushst))
{
stpush(&obj->popst, sttop(&obj->pushst));
stpop(&obj->pushst);
}
}
return sttop(&obj->popst);
}
出队列
int myQueuePop(MyQueue* obj) {
int top = myQueuePeek(obj);
stpop(&obj->popst);
return top;
}
直接复用我们的peek函数
- 当popst为null的时候,不需要我们手动倒数据,在peek函数里面就完成了
- 当popst不为null的时候,可以直接拿到队列元素,只需要删除这个元素即可
判空函数
bool myQueueEmpty(MyQueue* obj) {
return stempty(&obj->popst) && stempty(&obj->pushst);
}
free函数
void myQueueFree(MyQueue* obj) {
stdestroy(&obj->popst);
stdestroy(&obj->pushst);
free(obj);
}
设计循环队列,点我开始做题
思路讲解
我们先不能确定设计循环队列用数组还是用链表实现,可以先画图来理解
- 我们的rear指针是指向元素还是指向元素的下一个位置?
- 指向元素的下一个位置,插入后,使rear++
- 那什么情况下是代表队列满了?什么情况下代表队列是空的?
- 当最后一个元素位置被插入后,rear和front指向同样的位置
- 倘若我们用rear == front来表示,是不能确定队列是满的还是空的
解决方案:
- 空置一个位置,不存放数据,判满条件就是,rear下一个位置等于front。判空就是rear=front
- 添加计数器size,判满就是size=目标元素个数,判空就是size=0
- 我们一般默认使用空置一个的方法
实现方案:
用数组还是用链表?
- 当用链表实现的时候
上图是连续5次的插入,并且rear->next = front代表队列满了
下图为删除数据,当我们要删除2次数据的时候,只需要将front往前移动2个
如果要再添加数据,覆盖掉前面的数据即可,添加6 7
这里有一个难点,题目要求返回队列头尾的元素数据,头的数据好说,但是返回尾数据(这个时候rear指向的是元素的下一个位置)有点麻烦,当然也可以实现
- 当用数组实现的时候
假设要存储k=5个数据,要开k+1=6个空间,我们的判空条件很简单
按理来说,当rear到第6个空间的时候,我们就要盘满了,(rear+1)==front,但是当rear是空间的最后一个位置怎么办?+1就是越界了,这个时候我们可以%空间大小即可
这可以用(rear+1) %(k+1)== front来判满
当我们要删除数据的时候,让front++即可,一旦rear超出空间的时候就让其回到0的位置
明显数组来实现会更加简单一点
定义结构体
typedef struct {
int front;
int rear;
int k;
int* arr;
} MyCircularQueue;
这里没有难度
MyCircularQueue(k)
: 构造器,设置队列长度为 k
简单来说就是初始化结构体,创造出一个MyCircularQueue变量
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if (!obj)
return NULL;
int* p = (int*)malloc(sizeof(int) * (k + 1));
if (!p)
return NULL;
obj->front = obj->rear = 0;
obj->k = k;
obj->arr = p;
return obj;
}
先创建结构体变量,这样我们才能使用结构体,再创造数组,注意,我们采用置空的方法,就要多创造一个空间
isEmpty()
: 检查循环队列是否为空。
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->rear == obj->front;
}
isFull()
: 检查循环队列是否已满。
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear + 1) % (obj->k + 1) == obj->front;
}
这些都是在思路讲解里面提到过
enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if (myCircularQueueIsFull(obj))
return false;
obj->arr[obj->rear] = value;
/* if(obj->rear+1 >= obj->k+1)
{
obj->rear = 0;
}
else
{
obj->rear++;
} */
obj->rear++;
obj->rear %= (obj->k + 1);
return true;
}
这样就是先插入数据,再让rear++即可
我们要注意的是,当rear超过空间的时候让其回到0,我们可以用if来判断,这里我们学习一个新东西,%
(rear+1)%(k+1)当rear+1在空间里面的时候,等于本身的值,当大于等于k+1的时候,就会从0开始重新排
deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return false;
obj->front++;
obj->front %= (obj->k + 1);
return true;
}
让front++即可
同样要注意我们的越界循环问题
Front
: 从队首获取元素。如果队列为空,返回 -1 。
int myCircularQueueFront(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return -1;
return obj->arr[obj->front];
}
Rear
: 获取队尾元素。如果队列为空,返回 -1 。
int myCircularQueueRear(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return -1;
return obj->arr[(obj->rear - 1 + obj->k + 1) % (obj->k + 1)];
}
注意,我们的rear是在元素的下一个位置,在k+1空间里面,rear-1即可访问到
如下图,当rear=0的时候,队尾数据下标其实是5,同样的用%即可
释放空间
malloc什么就释放什么即可,这里写简单一点
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->arr);
free(obj);
}
括号匹配问题
这里用栈是天然的合适,遇到左括号我们就入栈,遇到右括号就出栈,一个一个开始匹配即可
- 当没有左括号的时候,即栈在遇到第一个右括号的时候,仍然为空,直接flase
- 当遇到右括号,则取出一个左括号与之匹配,一旦不匹配就flase
- 当全部匹配完成了,栈应该是空的,刚好与我们的empty函数一致
//括号匹配问题 用栈实现
bool isValid(char* s)
{
st st;
stinit(&st);
while (*s) //左括号入栈
{
if (*s == '('
|| *s == '{'
|| *s == '[')
{
stpush(&st, *s);
}
else //遇到右括号
{
if (stempty(&st)) //栈为空,是没有左括号的情况
{
stdestroy(&st);
return false;
}
char new = sttop(&st); //取左括号匹配
stpop(&st);
if ((*s == ')' && new != '(')
|| (*s == '}' && new != '{')
|| (*s == ']' && new != '['))
{
stdestroy(&st);
return false;
}
}
s++; //迭代
}
bool ret = stempty(&st);
stdestroy(&st);
return ret;
}