数据结构算法刷题笔记——四、栈与队列
4.1 栈与队列
栈:限定仅在表尾进行插入和删除操作的线性表
队列:只允许在一端进行插入操作,在另一端进行删除操作的线性表
4.2 栈的定义
4.2.1栈的定义
栈
(stack):是限定仅在表尾进行插入和删除操作的线性表
- 栈顶:允许插入和删除的一端
- 栈底:栈顶的另一端
- 空栈:不含任何数据元素的栈
- LIFO:别名,后进先出(Last InFirst Out)的线性表
- 栈是线性表
- 栈元素具有线性关系(前驱后继关系)
- 表尾——栈顶
- 表头——栈底
- 插入操作:进栈、压栈、入栈
- 删除操作:出栈、弹栈
4.2.2 进栈出栈变换形式
保证栈顶元素先出栈就可以
4.3 栈的抽象数据类型
- 理论上:线性表的操作特性它都具备
- 针对它的特殊性:一些操作有一些变化
- 插入和删除操作:改为 进栈push 和 出栈pop
4.4 栈的顺序存储结构及实现
4.4.1 栈的顺序存储结构
栈的顺序存储
:顺序栈:线性表顺序存储的简化
- 栈底:下标为0
- 栈顶:top变量,指示栈顶元素在数组中的位置
- top 小于等于存储站长度StackSize
- 空栈:top == -1
栈的结构定义
typedef int SElemType; /* SElemType类型根据实际情况而定,这里加黑色为int */
/* 顺序栈结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top; /* 用于栈顶指针 */
}SqStack;
4.4.2 栈的顺序存储结构——进栈操作 push
进栈操作 push
- 时间复杂度O(1)
/* 插入元素e为新的栈顶元素 */
Status Push(SqStack *S, SElemType e)
{
if(S->top == Maxize-1) { /* 判断栈满 */
return ERROR;
}
S->top++; /* 栈顶指针增加一 */
S->data[s->top] = e; /* 将新插入元素赋值给栈顶空间 */
return OK;
}
4.4.3 栈的顺序存储结构——出栈操作 pop
出栈操作 pop
- 时间复杂度O(1)
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqStack *S, SElemType *e)
{
if(S->top == -1) /* 栈为空,无可删除元素 */
*e = S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return OK;
}
4.5 两栈共享空间
出发点:必须事先确定数组存储空间大小,万一不够用了,就需要用编程手段来扩展数组的容量
解决方案:两个相同类型的栈,用一个数组来存储这两个栈
- 数组两个端点,作为两个栈的栈底
- 下表为0,栈一的栈底
- 下表为n-1,栈二的栈底
- 两个栈的栈顶在数组中间,由两端向中间靠拢
- top1 = -1,栈一为空栈
- top2 = n,栈二为空栈
- top1 + 1 == top2 :栈满
- 使用范围:当两个栈的空间需求有相反关系,一个栈增长时另一个栈缩短的情况
两栈共享空间结构:
/* 两栈共享空间结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top1; /* 栈1栈顶指针 */
int top2; /* 栈2栈顶指针 */
}SqDoubleStack;
两栈共享空间——插入元素 push
- 函数参数需要一个判断是栈一或栈二的栈号参数stackNumber
- 注意:++S->top1, --S->top2(插入元素,先自增长度,再赋值)
/* 插入元素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)
S->data[--S->top2] == e;
return OK;
}
两栈共享空间——删除元素 pop
- 注意:S->top1–, S->top2++(删除元素,先赋值,再自减长度)
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
if(stackNumber == 1){
if(S->top1 == -1) /* 判断栈1已经是空栈,则删除操作会溢出 */
return ERROR;
*e = S->data[S->top1--]; /* 将栈1的栈顶元素出栈 */
}
else if(stackNumber == 2){
if(S->top2 == MAXSIZE)
return ERROR;
*e = S->data[S->top2++];
}
}
4.6 栈的链式存储结构及实现
4.6.1 栈的链式存储结构
栈的链式存储结构:简称链栈
- 只有栈顶进行插入和删除操作
- 栈顶:放在单链表的头部
- 有了栈顶在头部,单链表中的头结点失去了意义,链栈基本没有头结点
- 栈满:链栈基本不会栈满,除非计算机内存没空间
- 空栈:top == NULL
- 其他操作和单链表类似,在插入和删除上有一些特殊
/* 链栈结构 */
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode, * LinkStackPtr;
typedef struct
{
LinkStackPtr top; /* 栈顶指针 */
int count;
}LinkStack;
4.6.2 栈的链式存储结构——进栈操作 push
进栈 push 操作:元素值为e的新节点s,top为栈的栈顶指针
- 时间复杂度O(1)
/* 插入元素e为新的栈顶元素 */
Status Push(LinkStack *S, SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = S->top;
S->top = s;
S->count++;
return OK;
}
4.6.3 栈的链式存储结构——出栈操作 pop
链栈的出栈 pop 操作:变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可
- 时间复杂度O(1)
/* 若栈不空,则删除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;
}
链栈和顺序栈选择
- 用链栈:栈的使用过程中元素变化不可预料,有时很小,有时很大,最好用链栈
- 用顺序栈:栈的变化在可控范围内,使用顺序栈更好
4.7 栈的作用
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心
4.8 栈的应用——递归
4.8.1 斐波那契数列的实现
斐波那契数列:前面相邻两项之和,构成了后一项
打印前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;
printf("递归显示斐波那契数列:\n");
for(int i = 0; i < 40; i++){
printf("%d",Fbii());
}
return 0;
}
4.8.2 递归的定义
递归函数:直接调用自己或通过一系列的调用语句间接地调用自己的函数
- 每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值推出
迭代和递归的区别:
- 迭代:使用循环结构
- 优点:不需要反复调用函数和占用内存
- 递归:使用选择结构
- 优点:是程序结构更简洁,更容易理解
- 缺点:递归调用,会建立函数的副本,耗费大量的时间和内存
递归过程就是操作系统通过栈的形式实现的
- 前行阶段:对每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中
- 退回阶段:位于栈顶的局部变量、参数值和返回值被弹出,用于返回调用层次中执行代码的其余部分,恢复了调用的状态
4.9 栈的应用——四则运算表达式求值
4.9.1 后缀(逆波兰)表示法的定义
栈的应用:比较常见的——数学表达式的求值
后缀表达法(逆波兰):一种不需要括号的后缀表达法
- 所有符号都要在运算数字的后面出现
- 规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
4.9.2 后缀表达式的计算结果
4.9.3 中缀表达式转后缀表达式
标准四则运算表达式:中缀表达式 :9+(3-1)*3+10/2
中缀表达式——转换为——后缀表达式:
- 规则:从左到右遍历终追你表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号,则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
4.10 队列的定义
队列:只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
- FIFO:一种先进先出的线性表,简称FIFO
- 队尾:允许插入的一端
- 队头:允许删除的一端
- 排在第一个的优先出列,最后的在队伍最后
4.11 队列的抽象数据类型
队列有类似于线性表的各种操作
- 不同点:
- 插入数据只能在队尾
- 删除数据只能在队头
4.12 循环队列
队列也存在顺序存储和链式存储两种结构
4.12.1 队列顺序存储的不足
队列的顺序存储结构:按照最基础的数组存储形式
-
插入操作:在队尾插入一个元素,不需要移动任何元素
- 时间复杂度O(1)
- 时间复杂度O(1)
-
删除操作:在队头出列,即下标为0的位置
- 时间复杂度O(n)
- 时间复杂度O(n)
存在不足:每次出队(删除)都在队头下标为0的位置,性能大大增加
改进方式:队头不需要一定在下标为0的位置,循环队列的使用
- 引入两个指针:
- front指针:指向队头元素
- rear指针:指向队尾元素的下一个位置
4.12.2 循环队列的定义
出发点:解决假溢出,后面满了,再从头开始,头尾相接的循环。
循环队列
:队列的这种头尾相接的顺序存储结构成为循环队列
-
front指针:指向队头元素
-
rear指针:指向队尾元素的下一个位置
-
队列空的条件:front == rear
-
队列满时,保留一个元素空间
-
队列满的条件:(rear+1)%QueueSize == front
-
队列长度:(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 DeQieie(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;
}
4.13 队列的链式存储结构及实现
循环队列的算法时间性能并不高,但是循环队列面临着数组可能会溢出的问题
需要研究一下不需要担心队列长度的链式存储结构
链队列:队列的链式存储结构,就是线性表的单链表,只不过它只能尾进头出
- 队头指针:指向链队列的头结点
- 队尾指针:指向终端结点
- 空队列:front和rear都指向头结点
链队列的结构
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */
typedef struct QNode /* 结点结构 */
{
QElemType data;
struct QNode *next;
}QNode, *QueuePtr;
typedef struct /* 队列的链表结构 */
{
QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;
4.13.1 队列的链式存储接哦古——入队操作
入队操作
- 在链表尾部插入结点
/* 插入元素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;
}
4.13.2 队列的链式存储结构——出队操作
出队操作
- 头结点的后继结点出队
- 将头结点的额后继改为它后面的结点
- 若链表除头结点外只剩下一个元素,则需将rear指向头结点
出队操作
/* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if(Q->front == Q->rear)
return ERROR;
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;
}