目录
一、栈的定义
二、栈的抽象数据类型
三、栈的顺序存储结构及实现
1、栈的顺序存储结构
2、进栈操作
3、出栈操作
四、两栈共享空间
五、栈的链式存储结构及实现
1、栈的链式存储结构
2、栈的链式存储结构——进栈操作
3、栈的链式存储结构——出栈操作
六、栈的应用——递归
七、栈的应用——四则运算表达式求值
1、后缀(逆波兰)表示法定义
2、后缀表达式计算结果
3、中缀表达式转后缀表达式
八、队列
1、队列的定义
2、队列的抽象数据类型
3、循环队列
1) 什么是循环队列?
2) 循环队列的代码实现
4、队列的链式存储结构及实现
栈与队列:
栈是限定仅在表尾进行插入和删除操作的线性表。
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。
栈是限定仅在表尾进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表,简称LIFO结构。
定义中说是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。栈底是固定的,最先进栈的只能在栈底。栈的插入操作,叫作压栈,也称压栈、入栈。栈的删除操作,叫作出栈,也称作弹栈。
ADT 栈
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitStack(*S):初始化操作,建立一个空栈S。
DestroyStack(*S):若栈存在,则销毁它。
ClearStack(*S):将栈清空。
StackEmpty(S):若栈为空,则返回true,否则返回false。
GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
Push(*S,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
StackLength(S):返回栈S的元素个数。
endADT
由于栈是线性表的特例,所以栈的顺序存储其实也是线性表顺序存储的简化,我们简称为顺序栈。线性表是用数组来实现的,下标为0的一端可以作为顺序栈的栈底,另一端则作为顺序栈的栈顶。我们可以定义一个top变量来指示栈顶元素在数组中的位置,这top就如同游标卡尺的游标,它可以来回移动,意味着栈顶的top可以变大变小,但无论如何游标不能超出尺的长度。因此,top必须小于StackSize,当栈中存在一个元素时,top为0;如果为空栈,则top为-1。
栈的结构定义如下:
#include
using namespace std;
#define MAXSIZE 20
#define OK 1
#define ERROR 0
typedef int Status; //函数结果状态码,如OK等
typedef int ElemType;
typedef struct{
ElemType data[MAXSIZE];
int top; //栈顶指针
}Stack;
进入操作代码如下:
//插入元素e为新的栈顶元素
Status Push(Stack *s, ElemType e){
if(s->top == MAXSIZE-1){ //栈满
return ERROR;
}
s->top++; //栈顶指针加1
s->data[s->top]=e; //将插入元素赋值给栈顶空间
return OK;
}
出栈操作代码如下:
//若格非空,则删除栈顶元素,用e返回删除的元素
Status Pop(Stack *s, ElemType *e){
if(s->top == -1){
return ERROR;
}
*e=s->data[s->top];
s->top--;
return OK;
}
两栈共享空间是利用同一个数组来存储两个栈,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0处;另一个栈的栈底为数组的末端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是从两端点向中间处延伸。当栈1为空时,则top1为-1;当栈2为空时,则top2为n。栈满的情况有三种如下:
① 当栈1是空栈,top2为0时,就是栈2满;
② 当栈2是空栈,top1为n-1时,就是栈1满;
③ 栈1、栈2都不是空栈,且两个指针相差1时,即top1+1=top2时。
两栈共享空间的结构实现代码如下:
//两栈共享空间的结构实现
typedef struct{
ElemType data[MAXSIZE];
int top1; //栈1的栈顶指针
int top2; //栈2的栈顶指针
}DoubleStack;
两栈共享空间插入元素的实现代码如下:
//两栈共享空间插入元素的实现代码
Status Push(DoubleStack *s, ElemType e, int stackNumber){ //statckNumber用于判断是栈1还是栈2
if(s->top1 + 1 == s->top2){ //判断栈是否已满
return ERROR;
}
if(stackNumber == 1){ //栈1有元素进栈
s->data[s->top1+1]=e; //赋值
}else if(stackNumber == 2){ //栈2有元素进栈
s->data[--s->top2]=e; //赋值
}
return OK;
}
两栈共享空间删除元素的实现代码如下:
//两栈共享空间删除元素
Status Pop(DoubleStack *s, ElemType *e, int stackNumber){
if(stackNumber == 1){
if(s->top1 == -1){ //栈1空栈
return ERROR;
}
*e = s->data[s->top1--];
}else if(stackNumber == 2){
if(s->top2 == MAXSIZE){ //栈2空栈
return ERROR;
}
*e = s->data[s->top2++];
}
return OK;
}
事实上,使用两栈共享的数据结构,通常是用于对两个栈的空间需求有相反关系时,也就是一个栈在增长而另一个栈在缩短的情况。就像买卖股票一样,有人买入,就有人卖出。这样使用两栈共享空间存储方法才有比较大的意义,否则两个栈都在不停的增长,那很快就会因栈满而溢出了。
栈的链式存储结构,简称链栈。由于单链表有头指针,而栈顶指针也是必须的,所以可以让栈顶指针当作单链表的头指针。通常对于链栈来说,是不需要头结点的。
链栈的代码定义如下:
//定义链栈
typedef struct StackNode{
ElemType data; //节点元素
struct StackNode *next; //下一个节点指针
}StackNode,*LinkStackPtr;
typedef struct LinkStack{
LinkStackPtr top; //栈顶指针
int count; //栈元素数量
}LinkStack;
进栈操作的代码如下:
//进栈操作,添加新元素e
Status Push(LinkStack *S, ElemType e){
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode)); //创建一个LinkStackPtr类型的新结点
s->data = e; //将元素e赋值给新结点
s->next = S->top; //把当前的栈顶元素赋值给新结点的直接后继
S->top = s; //把当前的栈顶元素指针指向新结点
S->count++; //栈元素+1
return OK;
}
出栈操作的代码如下:
//出栈操作,用于e返回出栈的元素
Status Pop(LinkStack *S, ElemType *e){
LinkStackPtr p;
if(S->top == NULL){
return ERROR;
}
*e = S->top->data; //将要删除的元素赋值给e
p = S->top; //将栈顶结点赋值给p
S->top = S->top->next; //将栈顶结点赋值为栈顶结点的后继节点,即指针下移一位
free(p); //释放要删除的结点
S->count--; //栈中元素-1
return OK;
}
我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。每个递归定义至少有一个条件,满足时递归就不再进行,即不再引用自身而是返回值退出。
斐波那契的递归函数如下:
//递归函数
int Fbi(int n){
if(n < 2){
return n = 0 ? 0 : 1;
}
return Fbi(n-1) + Fbi(n-2);
}
迭代和递归的区别:迭代使用的是循环结构,递归使用的是选择结构。递归能使用程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。
递归和栈有什么关系呢?在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
一种不需要括号的后缀表达法,我们把它称为逆波兰表示。比如:9+(3-1)*3+10/2 用后缀表达法表示为:931-3*+102/+,之所以叫后缀表达式,原因在于所有的符号都是在要运算数字的后面出现。
后缀表达式:931-3*+102/+
计算规则:从左到右遍历表达式中的每个数字和符号,遇到是数字就进栈,遇到是符号就将处于栈顶的两个数字出栈进行运算,然后将运算结果进栈,最终获取到结果。
下面是具体的进栈、出栈运算过程:
① 9进栈 3进栈 1进栈; 栈中值9 3 1
② 下面是"-",将1 3依次出栈并进行运算3-1得到2进栈;栈中值9 2
③ 3进栈;栈中值9 2 3
④ 下面是"*",将3 2依次出栈并进行运算2*3得到6进栈;栈中值9 6
⑤ 下面是"+",将6 9依次出栈并进行运算9+6得到15进栈;栈中值15
⑥ 10进栈 2进栈;栈中值15 10 2
⑦ 下面是"/",将2 10依次出栈并进行运算10/2得到5进栈,栈中值15 5
⑧ 下面是"+",将5 15依次出栈并进行运算15+5得到20
转化规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号,则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
中缀表达式9+(3-1)*3+10/2 转化为后缀表达式:931-3*+102/+
下面是具体的转化过程:
① 9输出;后缀表达式为9
② 下面是"+"入栈、"("入栈;栈中值 + (
③ 3输出;后缀表达式为93
④ 下面是"-"入栈;栈中值+ ( -
⑤ 1输出;后缀表达式为931
⑥ 下面是")",需要进行配对,栈中元素依次出栈并输出,直到"("出栈为止;后缀表达式为931-,栈中值+
⑦ 下面是"*",优先级比栈中"+"高,所以入栈;栈中值+ *
⑧ 3输出;后缀表达式为931-3
⑨ 下面是"+",低于栈顶符号的优先级,所以* +依次出栈输出,然后将当前"+"入栈;后缀表达式为931-3*+,栈中值+
⑩ 10输出;后缀表达式为931-3*+10
⑪ 下面是"/" ,优先级比"+"高,入栈;栈中值+ /
⑫ 2输出,此时已经到了最后,将栈中元素依次出栈并输出;后缀表达式为931-3*+102/+
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的一端被称为队尾,允许删除的一端被称为队头。假设队列是q=(a1,a2,a3,...,an),那么a1就是队头元素,而an就是队尾元素。这样我们删除时就从a1开始,而插入时,就列在最后。
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q
DestoryQueue(*Q):若队列Q存在,则销毁它
ClearQueue(*Q):将队列Q清空
QueueEmpty(Q):若队列Q为空,返回true,否则返回false
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值
QueueLength(Q):返回队列Q的元素个数
endADT
假设一个队列有n个元素,则顺存储的队列需要建立一个大于n的数组,并把队列的所有元素存储在数组的前n个单元中,数组下标为0的一端即是队头。所谓的入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1)。出列时由于是在队头,即下标为0的位置,也就意味着队列中的所有元素都得向前移动,以保证队列的队头,也就是下标为0的位置不为空,此时时间复杂度为O(n)。但如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队的性能就会大大增加,也就是说,队头不需要一定设置在下标为0的位置。
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。
这时有一个问题,如果数组末尾元素已经添加,再往后加就会产生数组越界的错误。而由于队头不一定设置在下标为0的位置,那么就会出现队头前面还有空闲的空间,这种现象被称为“假溢出”。
解决假溢出的办法就是后面满了,从前面开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。在循环队列中,当数组满了之后,可以将rear指针指向下标为0的位置。为了解决front==rear时无法判断队列是空还是满的问题,需要在队列满时,数组中还保留一个空闲单元。
由于rear可能比front大,也可能比front小,若队列的最大尺寸为QueueSize,那么队列满的条件就是(rear+1)%QueueSize==front ;另外,当rear>front时,队列长度为rear-front,但当rear 循环队列的定义代码如下: 循环队列的初始化代码如下: 循环队列的长度获取代码如下: 循环队列的入队操作代码如下: 循环队列的出队操作代码如下: 队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作方便,我们将队头指针指向链队列的头结点,而队尾指向终端结点。 链队列的结构定义代码如下: 链队列的入队操作代码如下: 链队列的出队操作代码如下: 2) 循环队列的代码实现
//循环队列的顺序存储结构
typedef struct{
ElemType data[MAXSIZE];
int front; //队头指针
int rear; //队尾指针
}Queue;
//初始化一个空队列
Status InitQueue(Queue *Q){
Q->front = 0;
Q->rear = 0;
return OK;
}
//获取循环队列的队列长度
int QueueLength(Queue Q){
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
//循环队列的入队操作,如果队列未满,则插入元素e为Q的队尾元素
Status EnQueue(Queue *Q, ElemType e){
if((Q->rear + 1) % MAXSIZE == Q->front){ //队满的判断
return ERROR;
}
Q->data[Q->rear] = e; //将元素e赋值给队尾
Q->rear = (Q->rear + 1) % MAXSIZE; //将rear指针向后移动一位
return OK;
}
//循环队列的出队操作,如果队列不为空,则删除Q的队头元素,用e返回其值
Status DeQueue(Queue *Q, ElemType *e){
if(Q->front == Q->rear){
return ERROR;
}
*e = Q->data[Q->front];//将队头元素赋值给e
Q->front = (Q->front - 1) % MAXSIZE; //将front指针后移一位
return OK;
}
4、队列的链式存储结构及实现
//链队列的结构定义
typedef struct Node{ //队列的结点
ElemType data;
struct Node *next;
}Node,*QueuePtr;
typedef struct{ //队列的链表结构
QueuePtr front,rear; //队头和队尾指针
}LinkQueue;
//链队列的入队操作
Status EnQueue(LinkQueue *Q, ElemType e){
QueuePtr s = (QueuePtr)malloc(sizeof(Node));
if(!s){ //存储分配失败
exit(0);
}
s->data = e;
s->next = NULL;
Q->rear->next = s; //将当前队尾节点的next指向s节点
Q->rear = s; //将队尾指针指向s节点
return OK;
}
//链队列的出队操作,若队列不空,删除Q的队头元素,用e返回其值
Status DeQueue(LinkQueue *Q, ElemType *e){
QueuePtr p;
if(Q->front == Q->rear){ //判断队列为空
return ERROR;
}
p = Q->front->next; //将要删除的队头结点赋给p
*e = p->data; //将队头结点的值赋给e
Q->front->next = p->next; //改变头结点的后继为原队头结点的后继
if(Q->rear == p){ //如果rear指针指向的是要删除的节点
Q->rear = Q->front;
}
free(p); //删除节点
return OK;
}