目录
1、栈的定义
2、栈的抽象数据类型
3、栈的顺序存储结构及实现
1、栈的顺序存储结构
2、栈的顺序存储结构——进栈操作
3、栈的顺序存储结构——出栈操作
4、两栈共享空间
5、栈的链式存储结构及实现
1、栈的链式存储结构(简称链栈)
2、栈的链式存储结构——进栈操作
3、栈的链式存储结构——出栈操作
6、栈的应用——递归
1、斐波那契数列实现
7、栈的应用——四则运算表达式求值
1、后缀(逆波兰)表示法定义
2、后缀表达式规则
3、中缀表达式转后缀表达式
8、队列的定义
9、队列的抽象数据类型
10、循环队列
11、队列的链式存储结构及实现
1、队列的链式存储结构——入队操作
2、队列的链式存储结构——出队操作
总结
限定仅在表尾进行插入和删除的线性表,
又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈顶:
允许插入和删除的一端称为栈顶(top)
栈底:
相对于栈顶的另外一端称为栈顶(bottom)
空栈:
不含任何数据元素的栈
栈的插入操作(push)
叫做进栈,也称压栈、入栈
栈的删除操作(pop)
叫做出栈,也有的叫做弹栈
进栈出栈变化形式
最先进栈的元素,不一定最后出栈
比如,如果有3个整型数字元素1、2、3依次进栈,会有如下的出栈次序:
- 第一种:1、2、3进,再3、2、1出。出栈次序321
- 第二种:1进,1出,2进,2出,3进,3出。出栈次序123
- 第三种:1进,2进,2出,1出,3进,3出。出栈次序213
- 第四种:1进,1出,2进,3进,3出,2出。出栈次序132
- 第五种:1进,2进,2出,3进,3出,1出。出栈次序231
ADT 栈(stack)
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
(定义一个top变量指示栈顶元素在数组中的位置,通常把空栈的判定条件定位top等于-1)
typedef int SElemType;/*SElemType类型根据实际情况而定,这里假设为int*/
typedef struct{
SElemType data[MAXSIZE];
int top;/*用于栈顶指针*/
}SqStack;
/*插入元素e为新的栈顶元素*/
Status Push(SqStack *S, SElemType e){
if(S->top == MAXSIZE - 1){/*栈满*/
return ERROR;
}
S->top++;/*栈顶指针增加一*/
S->data[S->top] = e;/*将新插入元素赋值给栈顶空间*/
return OK;
}
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(SqStack *S, SElemType *e){
if(S->top == - 1){
return ERROR;
}
*e = S->data[S->top];/*将要删除的栈顶元素赋值给e*/
S->top--;/*栈顶指针减一*/
return OK;
}
将一个栈的栈底为数组的始端,即下标为0处;
另一个栈为数组的末端,即下标为数组长度n-1处
两个栈如果增加元素,就是两端点向中间延伸。
关键思路:
新增的元素在数组两端开始,向中间靠拢
栈1为空:
当top1等于-1时,栈1为空
栈2为空:
当top2等于n时,栈2为空
栈1为满:
若栈2为空栈,栈1的top1等于n-1,栈1满
栈2为满:
若栈1为空栈,栈2的top2等于0,栈2满
栈满:
两个栈见面之时,即两个指针之间相差1时,top1 + 1 == top2为栈满
两栈共享空间的结构的代码如下:
/*两栈共享空间结构*/
typedef struct{
SElemType data[MAXSIZE];
int top1;/*栈1栈顶指针*/
int top2;/*栈2栈顶指针*/
}SqDoubleStack;
插入元素的代码如下:
(stackNumber用于判断是栈1还是栈2)
/*插入元素e为新的栈顶元素*/
Status Push(SqDoubleStack *S, SElemType e, int stackNumber){
if(S->top1 + 1 == S->top2){/*栈已满,不能再push新元素*/
return ERROR;
}
if(stackNumber == 1){/*栈1有元素进栈*/
S->data[++S->top1] = e;/*若栈1则先top1+1后给数组元素赋值*/
}else if(stackNumber == 2){/*栈2有元素进栈*/
S->data[--S->top2] = e;/*若栈2则先top2-1后给数组元素赋值*/
}
return OK;
}
删除元素的代码如下:
(stackNumber用于判断是栈1还是栈2)
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber){
if(stackNumber == 1){
if(S->top1 == -1){
return ERROR;/*说明栈1已经是空栈,溢出*/
}
*e = S->data[S->top1--];/*将栈1的栈顶元素出栈*/
}else if(stackNumber == 2){
if(S->top2 == MAXSIZE){
return ERROR;/*说明栈2已经是空栈,溢出*/
}
*e = S->data[S->top2++];/*将栈2的栈顶元素出栈*/
}
return OK;
}
使用这种数据结构的情况通常分为两种
结构代码如下:
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack{
LinkStackPtr top;
int count;
}LinkStack;
假设元素值为e的新结点是s,top为栈顶指针,示意图如下
/*插入元素e为新的栈顶元素*/
Status Push(LinkStack *S, SElemType e){
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = S->top;/*把当前的栈顶元素赋值给新结点的直接后继,如图中①*/
S->top = s;/*将新的结点s赋值给栈顶指针,如图中②*/
S->count++;
return OK;
}
假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可,示意图如下
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(LinkStack *S, SElemType *e){
LinkStackPtr p;
if(StackEmpty(*S)){
return ERROR;
}
*e = S->top->data;
p = S-top;/*将栈顶结点赋值给p,如图③*/
S->top = S->top->next;/*使得栈顶指针下移一位,指向后一结点,如图④*/
free(p);/*释放结点p*/
S->count--;
return OK;
}
如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。
使用常规的迭代方法来实现前40位的斐波那契数列,代码如下:
int main(){
int i;
int a[40];
a[0] = 0;
a[1] = 1;
printf("%d", a[0]);
printf("%d", a[1]);
for(i = 2; i < 40; i++){
a[i] = a[i-1] + a[i-2];
printf("%d", a[i]);
}
return 0;
}
递归
把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数
每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出
使用递归方法来实现代码如下:
/*斐波那契数列的递归函数*/
int Fbi(int i){
if(i < 2){
return i == 0 ? 0 : 1;
}
return Fbi(i - 1) + Fbi(i - 2);/*这里Fbi就是函数自己,它在调用自己*/
}
int main(){
int i;
for(i = 0; i < 40; i++){
printf("%d", Fbi(i));
}
return 0;
}
对于“9 + (3 - 1) * 3 + 10 / 2”,可以用后续表示法表示为“9 3 1 - 3 * + 10 2 / +”
一种不需要括号且所有的符号都是在要运算数字的后面出现,即为后缀表示法
从左到右遍历表达式的每一个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
中缀表达式(平时所用的标准四则运算表达式,比如“9 + (3 - 1) * 3 + 10 / 2”)
转换规则
从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
只允许在一端进行插入操作,而在另外一端进行删除操作的线性表
是一种先进先出(First In First Out)的线性表,简称FIFO。
队尾
允许插入的一端称为队尾
队头
允许删除的一端称为队头
ADT 队列(queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q): 初始化操作.建立一个空队列Q。
DestroyQueue(*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
队列的这种头尾相接的顺序存储结构称为循环队列
循环队列队列满的条件(QueueSize为队列的最大尺寸)
(rear + 1) % QueueSize == front
循环队列长度公式计算(QueueSize为队列的最大尺寸)
(rear - front + QueueSize) % QueueSize
循环队列的顺序存储结构代码如下
typedef int QElemType;/*QElemType类型根据实际情况而定,这里假设为int*/
/*循环队列的顺序存储结构*/
typedef struct{
QElemType data[MAXSIZE];
int front;/*头指针*/
int rear;/*尾指针,若队列不空,指向队列尾元素的下一个位置*/
}SqQueue;
循环队列的初始化代码如下
/*初始化一个空队列Q*/
Status InitQueue(SqQueue *Q){
Q->front = 0;
Q->rear = 0;
return OK;
}
循环队列求队列长度代码如下
/*返回Q的元素个数,也就是队列的当前长度*/
int QueueLength(SqQueue Q){
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
循环队列的入队列操作代码如下
/*若队列未满,则插入元素e为Q新的队尾元素*/
Status EnQueue(SqQueue *Q, QElemType 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(SqQueue *Q, QElemType *e){
if(Q->front == Q->rear){/*队列空的判断*/
return ERROR;
}
*e = Q->data[Q->front];/*将队头元素赋值给e*/
Q->front = (Q->front + 1) % MAXSIZE;/*front指针向后移一位置*/
/*若到最后则转到数组头部*/
return OK;
}
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列
链队列的结构代码如下
typedef int QElemType;/*QElemType类型根据实际情况而定,这里假设为int*/
typedef struct QNode{/*结点结构*/
QElemType data;
struct QNode *next;
}QNode, *QueuePtr;
typedef struct{/*队列的链表结构*/
QueuePtr front, rear;/*队头、队尾指针*/
}LinkQueue;
入队操作时,其实就是在链表尾部插入结点,如下图所示:
其代码如下
/*插入元素e为Q的新的队尾元素*/
Status EnQueue(LinkQueue *Q, QElemType e){
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if(!s){/*存储分配失败*/
exit(OVERFLOW);
}
s->data = e;
s->next = NULL;
Q->rear->next = s;/*把拥有元素e新结点s赋值给原队尾结点的后继,*/
/*见上图中①*/
Q->rear = s;/*把当前的s设置为队尾结点,rear指向s,见上图中②*/
return OK;
}
入队操作时,其实就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点,如下图所示:
其代码如下
/*若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR*/
Status DeQueue(LinkQueue *Q, QElemType *e){
QueuePtr p;
if(Q->front == Q->rear){
exit(OVERFLOW);
}
p = Q->front->next;/*将欲删除的队头结点暂存给p,见上图中①*/
*e = p->data;/*将欲删除的队头结点的值赋值给e*/
Q->front->next = p->next;/*将原队头结点后继p->next赋值给头结点后继,*/
/*见上图中②*/
if(Q->rear == p){/*若队头是队尾,则删除后将rear指向头结点,见上图中③*/
Q->rear = Q->front;
}
free(p);
return OK;
}
对于栈来说,如果是两个相同数据类型的栈,则可以使用数组的两端作栈底的方法来让两个栈共享数据,可以最大化地利用数组空间。
对于队列来说,为了避免数组插入和删除时需要移动数据,于是引入循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得原本插入和删除的时间复杂度O(n)变成了O(1)。
如果突然想不明白书上的知识点,那就冷静一会,调整心态,反复阅读反复查找资料,用心学肯定能懂,共勉。