基本思路:队列的基本特点:先进先出。栈的基本特点:后进先出。下面阐述这两种数据类型的具体不同点所在。
队列与栈之间的具体区别
1.首先阐述一下栈这种数据结构,其基本特点为后进先出,栈这种结构体一般创作如下:
typedef struct
{
int topIn;
int* data;
int size;
}MyStack;
其中的topIn指向栈的头部,topOut指向栈的尾部。栈能够进行的基本数据操作包括以下几点
MyStack* mystackcreat(MyStack* stack,int size)//创建一个新的栈
{
stack->topIn=0;//一般而言,栈的栈顶索引指向的是当前存在元素的下一个元素
stack->data=(int*)malloc(sizeof(int)*size);
stack->size=0;
return stack;
}
void mystackpush(MyStack* obj,int x)//负责插入新的元素到栈顶
{
obj->data[obj->topIn++]=x;
obj->size++;
}
int mystackpop(MyStack* obj)//返回并移除栈顶元素
{ obj->size--;
return obj->data[--obj->topIn];//符号说明:由于topIn指向的是下一个元素,因此应该先做自减
}
int mystackpeel(MyStack* obj)//返回栈顶元素但不移除
{
obj->size--;
return obj->data[obj->topIn-1];
}
bool mystackempty(MyStack* obj)//确定栈是否为空
{
return obj->topIn==0;
}
以上就是栈的基本操作,接下来介绍队列这种数据类型
typedef struct{
int front;//指向队首
int rear;//指向队尾
int* data;
int size;
}Queue;
这就完成了一个队列结构体的创建,这里有个题外话,那就是我们为什么在做数据结构时要创建一个结构体,这个结构体到底有什么作用?我从以下几个方面进行阐释:1.我们每次在创建一个数据结构类型的时候,其实质都是在创作一个新的数据类型。类比我们熟知的int型,这种类型的数据能够进行加,减,乘,除的一系列操作,我们其实是在创作一个新的类型,它可以进行push,pop,empty,peek这些函数对应的操作2.而结构体中对应的各个数据则是起到辅助,提示的作用,让我们在C语言原有基本操作的基础上完成更高端的一系列操作。
下面介绍队列对应的一系列操作函数,在这一方面,队列与栈类似
Queue* myqueuecreat(Queue* obj,int size)
{
obj->front=obj->rear=0;
obj->data=(int*)malloc(sizeof(int)*size);
obj->size=0;
return obj;
}
void myqueuepush(Queue* obj,int x)
{
obj->data[obj->rear++]=x;
obj->size++;
}
int myqueuepop(Queue* obj)
{
obj->size--;
return obj->data[obj->front++];
}
int myqueuepeek(Queue* obj)
{
obj->size--;
return obj->data[obj->front+1];
}
bool myqueueempty(Queue* obj)
{
return obj->front==obj->rear;
}
好的,我们已经基本了解了栈和队列这两种基本结构,接下来我们将会将这两种数据操作抽象ADT作为接口,直接进行运用,加深我们对数据类型的理解。
如何用队列实现栈
要想做到用队列实现栈,首先阐明一个基本的思路:构造辅助队列,作为主队列的备份。当我们进行pop操作时,将主队列除最后一个元素外的所有元素全部弹入辅助队列内,然后返回主队列的最后一个元素。最后仍然将主队列和辅助队列调换后实现初始化(辅助队列仍然为空,主队列只失去了队尾元素)。
下面请看源代码:
typedef struct{
int* data;
int front;
int rear;
int size;
}Queue;
void Queueinit(Queue** queue,int size)
{
(*queue)=(Queue*)malloc(sizeof(Queue));
(*queue)->data=(int*)malloc(sizeof(int)*size);
(*queue)->front=0;
(*queue)->rear=0;
(*queue)->size=size;
}
void Queuepush(Queue* queue,int x)
{
queue->data[queue->rear++]=x;
}
int Queuepop(Queue* queue)
{
return queue->data[queue->front++];
}
bool Queueempty(Queue* queue)
{
return queue->front==queue->rear;
}
typedef struct {
Queue* q1;
Queue* q2;
} MyStack;
MyStack* myStackCreate() {
MyStack* mystack=(MyStack*)malloc(sizeof(MyStack));
Queueinit(&(mystack->q1),9);
Queueinit(&(mystack->q2),9);
return mystack;
}
void myStackPush(MyStack* obj, int x) {
Queuepush(obj->q1,x);
}
int myStackPop(MyStack* obj) {
int last;
while(!Queueempty(obj->q1))
{
last=Queuepop(obj->q1);
if(!Queueempty(obj->q1))
{
Queuepush(obj->q2,last);
}
}
Queue* temp;
temp=obj->q1;
obj->q1=obj->q2;
obj->q2=temp;
return last;
}
int myStackTop(MyStack* obj) {
int x=myStackPop(obj);
myStackPush(obj,x);
return x;
}
bool myStackEmpty(MyStack* obj) {
return Queueempty(obj->q1)&&Queueempty(obj->q2);
}
void myStackFree(MyStack* obj) {
free(obj->q1->data);
free(obj->q2->data);
free(obj->q1);
free(obj->q2);
free(obj);
}
/**
* Your MyStack struct will be instantiated and called as such:
* MyStack* obj = myStackCreate();
* myStackPush(obj, x);
* int param_2 = myStackPop(obj);
* int param_3 = myStackTop(obj);
* bool param_4 = myStackEmpty(obj);
* myStackFree(obj);
*/
下面我将对这些代码的一部分具体细节进行详述:
void Queueinit(Queue** queue,int size)
{
(*queue)=(Queue*)malloc(sizeof(Queue));
(*queue)->data=(int*)malloc(sizeof(int)*size);
(*queue)->front=0;
(*queue)->rear=0;
(*queue)->size=size;
}
问题:为什么在这个函数中我的参数传递为一个指向指针的指针?
解答:这个函数的目的是对结构体指针queue进行初始化,如果直接传递queue,由于函数中操作的是形参变量的备份,因此在申请内存时,内存并没有申请到我想要的地址,而是申请到了另一个同名备份地址上。而采用双指针的传递就可以忽视这个问题。
总结:在通过一个函数来对指针进行性内存申请时,一定要传递指向指针的指针,否则你想要分配内存的指针将永远是野指针。
注意:由于运算符号的优先级关系,指针解引时加上括号会是一个好习惯。
MyStack* myStackCreate() {
MyStack* mystack=(MyStack*)malloc(sizeof(MyStack));
Queueinit(&(mystack->q1),9);
Queueinit(&(mystack->q2),9);
return mystack;
}
问题1:在这个函数中,为什么没有参数的传递,如果有参数的传递,这个函数的内容会和现在有什么区别?
答案:想象一下在主函数上对该函数的调用,如果没有参数的传递,说明在主函数中并没有声明一个栈类型的结构体变量,这会造成怎样的问题呢?
接下来我会详细阐释在主函数中没有声明变量的后果以及一些取代方法:
1.在主函数中声明变量,本质上是在栈上进行内存的分配,同样的道理,在子函数上直接声明变量都是在栈上进行内存的分配,这种分配方式分配内存的存活时期只在函数内部,在这个函数中,由于没有在栈上申请内存,那么我们需要另一种方式申请内存。
2.在堆上申请内存,也就是利用malloc,calloc等动态申请内存函数申请,其特点为存活周期长,但需要手动释放内存(free函数)。
问题2:为什么在Queueinit函数中,我要对指针进行取址呢?
答案:这里和我们的上面说过的对指针进行内存申请联系到了一起,由于我们在对指向数组,结构体等这类指针进行内存申请时,如果要调用函数,要传递指向指针的指针,因此要对要申请内存的指针作取址操作。
int myStackPop(MyStack* obj) {
int last;
while(!Queueempty(obj->q1))//循环条件:当q1非空时进入循环
{
last=Queuepop(obj->q1);//将q1的队首元素进行出队操作
if(!Queueempty(obj->q1))//再次判断q1是否为空
{
Queuepush(obj->q2,last);//如果q1还没有为空,则将q1的队首元素入队到q2中qv
}
}
Queue* temp;
temp=obj->q1;
obj->q1=obj->q2;
obj->q2=temp;
return last;
}
这个函数是整个操作实现的难点,接下来我们具体说明:
这段代码的操作并不复杂,我们主要想实现的目标是:将主队列除最后一个元素外全部传递到辅助队列中去,难点在于:怎样除去最后一个元素?实现方法是,在进行pop操作后,进行if条件的判断。也就是说,在最后一步时我们移除了p1的最后一个元素,但还没来的及将它赋值给q2,就被if语句pass掉了。
这种思想可以进行更广泛的使用,每当我们想要“留一手时”,我们可以在某个函数的判断后面增加一个if语句,达到“留一手”的效果。