弹夹——栈
线性结构栈,十分神似弹夹。众所周知,弹夹上弹是从入口处压入,而使用的时候也是从顶部取出。所以先压入的子弹会被后使用。栈就是如此,数据进入的时候从栈顶进入;取出的时候也是从栈顶取出。
栈是顺序表的变种,她只能从顶部插入或删除。让我们回忆一下顺序表的定义方式。顺序表的结构体内有两个成员,一个是用于存放数据的数组、另一个是指示结尾的Last。让我们把顺序表竖起来,就是一个栈了。我们在栈中放入一个新的成员变量,用于指示栈的最大容量。所以栈的定义就跃然纸上了。
typedef int ElementType;
typedef int Position;
typedef struct SNode * PtrToSNode;
struct SNode{
ElementType * Data;
// 定义了一个数组
Position Top;
// 栈顶指针
int MaxSize;
// 栈的最大容量
};
typedef PtrToSNode Stack;
顺便,我将创建栈空间的代码附上。其实跟创建顺序表的非常相似。
Stack CreateStack(int MaxSize){
Stack s = (Stack)malloc(sizeof(struct SNode));
s->Data=(ElementType *)malloc(MaxSize*sizeof(ElementType));
s->MaxSize=MaxSize;
s->Top=-1;
return s;
}
根据刚刚的定义,栈最主要的两个操作就是出栈和入栈。
入栈的时候,我们只要先判断栈空间有没有满,然后将Top+1并且把元素放入数组的Top下标的位置即可。接下来我们瞅瞅入栈的函数Push。
bool Push(Stack S, ElementType x){
if(S->Top==S->MaxSize-1){
printf("栈空间已满");
}else{
S->Top+=1;
S->Data[S->Top]=x;
// 也可以写成S->Data[++(S->Top)]=x;
return true;
}
}
出栈跟入栈差不多,只需要先判断栈是否为空,然后获取数组Top下标的元素,最后Top--即可。
ElementType Pop(Stack S){
if(S->Top==-1){
printf("栈为空");
return -1;
}else{
return S->Data[(S->Top)--];
}
}
这样一个简单的栈就完美实现了。但是用数组实现的话,会导致空间有一丢丢的浪费,所以有人就想出了个法子,一个数组塞两个栈。
聪明的大家一定已经想到了,我们只需要搞两个栈顶指针,一个指向数组下标为-1的地方,另一个指向Maxsize即可,这样从头开始是一个栈,从脚开始也是一个栈。让我们来看看定义。
typedef int ElementType;
typedef int Position;
typedef struct SNode * PtrToSNode;
struct SNode{
ElementType * Data;
// 定义了一个数组
Position Top1;
Position Top2;
int MaxSize;
// 栈的最大容量
};
typedef PtrToSNode Stack;
就是如此简单,只是将前文的定义加上了一个新的指针而已。但是在判空和判断栈满方面略有不同。判空相对简单,所以我只展示一下判断栈是否满了。
bool IsFull(Stack S){
return(S->Top2-S->Top1==1);
}
肥肠简单,对吧。
那么肯定又有聪明的小可爱要问了,为啥要用数组实现呢,用链表实现岂不是不会导致空间的浪费?
太对了,所以接下来我们一起瞅瞅用链表实现的栈。
温故而知新,再看一次链表节点的定义:
typedef int ElementType;
typedef struct LNode * PtrToLNode;
struct LNode{
ElementType Data;
PtrToLNode Next;
};
typedef PtrToLNode Stack;
接下来我们可以定义一个头节点,为了方便操作,我们将头节点作为栈的顶部。
Stack CreateStack(){
Stack s;
s=(Stack)malloc(sizeof(struct LNode));
s->Next=NULL;
return s;
}
这个栈很明显是无上限的,因为入栈的时候我们只要加一个链表节点进去即可。所以我们来研究一下如何判空。显然当头节点的指针域为空时,栈即为空。
bool IsEmpty(Stack s){
return(s->Next==NULL);
}
入栈操作就比较耐人寻味了。跟链表的头插法很相似,我们只需要将新分配一个节点空间,然后填充数据域,再填充指针域为头节点的next,最后将头节点的next指向这个新节点即可。
ElementType Pop(Stack s){
PtrToSNode FirstCell;
// 保存当前的第一个节点
ElementType TopElem;
// 保存要返回的元素
if(IsEmtpy(s)){
printf("空栈");
return -1;
}else{
FirstCell = s->Next;
TopElem = FirstCell->Data;
s->Next=FirstCell->Next;
free(FirstCell);
return TopElem;
}
}
排排列,吃果果——队列
队列,顾名思义就是一个队列(笑),如果说栈是先进的后出,那队列就是先进的先出。同样,队列也可以用老朋友数组来实现。但是因为进入队列要从尾部进去,而出队要从头部出,所以我们需要两个指针,一个指向头一个指向尾。所以咱们就能获得一个这样的结构:
在插入元素的时候,我们只要将rear+1,然后插入到rear+1的位置即可。取出元素的时候我们只要将front+1即可,肥肠简单。但是这样会带来一个小问题。
随着不断取出和加入,当出现图示情况的时候,我们认为队列已经满了,但是事实上并没有。 对此,有大佬提出了一种解决方案——循环队列。循环队列就是把一个数组首尾相连,变成环状态,然后就可以避免上述问题啦。
(画图技术太差,所以不画了,大家可以自己想象一下)
所以我们来看一看循环队列的实现。我们肯定需要一个数组,两个指针,还有一个最大容量。
typedef int ElementType;
typedef int Position;
typedef struct QNode * PtrToQNode;
struct QNode{
ElementType * Data;
Position Front, Rear;
int MaxSize;
};
typedef PtrToQNode Queue;
创建队列的函数跟创建栈的函数很像:
Queue CreateQueue(int MaxSize){
Queue q;
q=(Queue)malloc(sizeof(struct QNode));
q->Data=(ElementType *)malloc(MaxSize*sizeof(ElementType));
q->Front=q->Rear=0;
q->MaxSize=MaxSize;
return q;
}
对于一个长度为10的循环队列,如何判断是否已经满了或空了呢?
判空相对简单,我们只需要判断front和rear是否相等即可。
反观判满,就可能出现两种情况。第一种,假如front=0,rear=9,即为队列满了。我们可以归纳一个条件出来就是rear-front=maxsize-1.但是还有一种情况,假如front=2,rear=1。由于front是头结点不存储数据,所以也应当视为队列满了,但是这样就无法使用刚刚的条件了。我们来提取一下当前这种情况,会得到rear+1=front。我们发现,第一个式子经过变形可以得到(rear+1)-front=maxsize,第二个式子可以变成(rear+1)-front=0。通过不懈努力,两个式子只相差一个maxsize了,接下来我们就需要用一个特殊的运算将maxsize和0在逻辑上划上等号。通过让rear+1%maxsize的方式,我们就能消解“圈数”带来的影响。因为rear+1与front之间的差值要么为maxsize,要么为0,所以对rear+1取maxsize余数的结果必然等于front。综上所述,最终判满的式子就是:
bool IsFull(Queue q){
return((q->Rear+1)%q->MaxSize==q->Front);
}
接下来让我们补全加入队列和从队列中取出的过程即可。由于是循环队列,所以加入队列的时候不能光让rear++,万一超过了数组长度可就不好玩了。当达到末尾的时候,我们也可以用%maxsize的方式巧妙得让尾巴“兜”回来。
bool AddQ(Queue q, ElementType x){
if(IsFull(q)){
printf("队列已满");
return false;
}else{
q->Rear=(q->Rear+1)%q->MaxSize;
q->Data[q->Rear]=x;
return true;
}
}
同理,取出的时候我们也要让头“兜”回来。
ElementType DeleteQ(Queue q){
if(q->Rear==q->Front){
printf("队列已空");
return -1;
}else{
q->Front=(q->Front+1)%q->MaxSize;
return q->Data[q->Front];
}
}
与栈相同,队列也可以用链式存储结构实现。但是由于链式存储结构只能从头遍历到尾,所以队列的头部必须是链表的头部,尾部也必须是链表的尾部。进入队列的时候我们需要将尾部的节点指向入队节点;出队的时候我们只需要free掉头部的节点即可。相关代码比较简单,故按下不表。