第4章 栈与队列
4.2 栈的定义
栈(stack) 是限定仅在表层进行插入和删除操作的线性表
允许插入和删除的一端称为栈顶(top)
另一端称为栈底(bottom)
后进先出(Last In First Out) 线性表, 简称 LIFO
栈的插入操作: 进栈, 压栈, 入栈
栈的删除操作: 出栈, 弹栈
栈本身是一个线性表, 之前讨论的有关线性表的顺序存储和链式存储对于栈同样适用
4.4 栈的顺序存储结构及实现
4.5 两栈共享空间
4.6 栈的链式存储结构及实现
4.6.2 栈的链式结构-----进栈操作
栈是由栈元素构成的线性链表, 其中有一个栈元素为栈顶元素.
不存在栈溢出情况. 除非整个内存都用完了
空栈: 当 top = NULL 时候
栈元素:
typedef struct StackNode
{
sElemType data;// 元素内容
struct StackNode *next; //指向下一个元素地址
} StackNode, *LinkStackPtr;
栈顶元素
typedef struct LinkStack
{
LinkStackPtr top;// 指向栈顶元素位置
int count;// 栈元素的数量
}LinkStack;
进栈操作 (栈为链式存储结构时, 不用考虑栈溢出)
过程:
创建新的栈元素 s , s 的数据data 装我们的新数据, s 的 next 指针 指向栈顶top元素
把栈顶元素top指向这个新的栈元素 s, 让s 称为新的栈顶, 栈顶元素的cout属性加一
代码如下:
// 先配置 新的栈元素
// 再配置 栈顶元素
Status push(LinkStack *S, sElemType e)
{
// 大写 S 是整个栈, e 是单纯的数据
// 小写 s 是栈元素
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
// 栈元素的数据 为 e
s->data = e;
// 栈元素的 next 指向之前的栈顶元素.
s->>next = S->top;
// 栈顶元素现在是 s
S->top = s;
// 栈元素数量加1
S->count++;
return OK;
}
4.6.3 栈的链式存储结构-----出栈结构
当栈顶 top = NULL 时 为空栈, 不能执行出栈操作(Pop)
过程:
拿到栈顶数据
取出栈顶元素
栈顶元素更新为下一个栈元素
释放取出的栈顶元素
代码如下:
Status Pop(LinkStack *S, sElemType *e)
{
// 传入参数 S: 整个栈
// 传入参数 *e : 用来存栈顶数据的存储位置
//p : 栈顶元素
LinkStackPtr p;
// 先判断是否是空栈
if(StackEmpty( *s )){
return ERROR;
}
// *e 获取栈顶的数据
*e = S->top-data;
// 给 p 赋值为 栈顶元素
p = S->top;
// 栈顶元素指向下一个元素(出栈)
S->top = S->top->next;
// 释放被出栈的栈顶元素p
free(p);
// 跟新栈元素数量
S->count--;
// 出栈操作结束
return OK;
}
总结: 如果栈的使用过程中元素变化不可预料, 有时很小, 有时非常大, 那么最好用链栈,
反之, 如果它的变化在可控范围内, 建议使用顺序栈会更好一些
4.8 栈的应用----递归
4.8.1 斐波那契数列实现
F(n) = 0, 当n=0
F(n) = 1, 当n=1
F(n) = F(n-1) + F(n-2), 当n>1
打印前40位的斐波那契数列
简单的实现:
int main()
{
// 从斐波那契数列这个名字上, 我们就可以看到, 可以用数组来实现.
// 根据公式的规律把数列和数组的每个元素对应存起来.
// 直接给 a[0] 设值 为 0
// 直接给 a[1] 设值 为 1
// a[i] = a[i-1] + a[i-2] 和公式 F(n) = F(n-1) + F(n-2) 对应起来.
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;
}
递归实现方法:
/* 上一个实现方法的思路是 从头到尾 的推断方式
先考虑 i=0 再考虑 i=1 一直到 i = 40
*/
/* 这里的递归采用的是 从尾到头 的推断方式
先是 i = 40, i = 39, i = 38, 一直到 i = 1, i = 0
*/
// 斐波那契的递归函数
int Fbi( int i )
{
if (i<2)
{
// i=0 时 Fbi(0) = 0
// i=1 时 Fbi(1) = 1
return i==0 ? 0 : 1;
}
// i >= 2时: Fib(i) = Fib(i-1) + Fib(i-2)
return Fbi(i - 1) + Fbi(i - 2);// 这里Fbi 就是函数自己, 它在调用自己
}
int main ()
{
int i;
for (int i = 0; i < 40; i++){
// 输出 每一个斐波那契数列元素
printf("%d", Fbi(i));
}
return 0;
}
4.8.2 递归定义
我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数, 称作递归函数
每个递归定义, 必须至少有一个条件, 满足时递归不再进行, 即不再引用自身而是返回值退出
-
递归和迭代:
- 迭代: 使用循环结构, 不需要反复调用函数和占用额外的内存,
- 递归: 使用选择结构, 使程序的结构更清晰, 更简洁, 更容易让人理解, 从而减少读懂代码的时间. 但是大量的递归调用会建立函数的副本, 会耗费大量的时间和内存.
递归是用栈这种数据结构实现的. 简单的说, 在执行过程中, 对于每一层递归, 函数的局部变量, 参数以及返回地址都被压入栈中, 在退回阶段, 位于栈顶的局部变量, 参数值和返回地址被弹出, 用于返回调用层次中执行代码的其余部分, 也就是恢复了调用的状态.
4.9 栈的应用 -- 四则运算表达式求值
4.9.1 后缀 (逆波兰)表示法定义
4.9.2 后缀表达式计算结构
4.9.3 中缀表达式转后缀表达式
4.10 队列的定义
定义: 队列(queue) 是只允许在一端进行插入操作. 而在另一端进行删除操作的线性表.
队列是一种先进先出(First In First Out)线性表, 简称FIFO. 允许插入的一端称为队尾, 允许删除的一端称为队头. 假设队列是 q = (a1, a2, ......, an) , 那么 a1 就是队头元素, 而 an 是队尾元素, 这样我们就可以删除时, 总是从 a1 开始, 插入时, 列在最后.
队头 队尾
出队列 <--- a1, a2, a3, ...... an <---入队列
4.12 循环队列
顺序队列的不足: 造成假溢出
循环队列: 后面满了, 就再从头开始, 也就是头尾相接的循环. 我们把队列的这种头尾相接的顺序存储结构称为循环队列
c 语言中 区域运算符%
比如 5%4 = 1
4%5 = 4
循环队列的顺序存储结构代码
//队列结构体
typedef int QElemType; // QElemType 为 int 类型(类型按照实际情况来定, 这里假设为 int)
typedef struct
{
QElemType data[MAXSIZE];// int 类型数组
int front; // 头指针
int rear; // 尾指针
}sqQueue
循环队列的初始化代码
Status InitQueue(sqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
循环队列求队列长度代码如下:
当 rear > front 时, 队列长度为: QueueLength = rear - front
当 rear < front 时, 队列长度为: QueueLength = (QueueSize - front) + (0 + rear)
把上面两个条件结合在一起得到公式: QueueLength = (rear - front + QueueSize) % QueueSize
int QueueLength(sqQueue Q)
{
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
循环队列的入队列操作代码如下:
Status EnQueue(SqQueue *Q, QElemType d)
{
// 判断是否队列已经满了
if((Q->rear + 1) % MAXSIZE == Q->front)
{
return ERROR;
}
// 队列中插入值
Q->data[Q->rear] = e;
// 后移队尾指针
Q->rear = (Q->rear+1)%MAXSIZE;
return OK;
}
循环队列的出队列操作代码如下:
Status DeQueue(sqQueue *Q, QElemType *e)
{
// 判断队列是否为空
if(Q->front == Q->rear)
{
return ERROR;
}
// 取出队首数据
*e = Q->data[Q->front];
// 队首 指针后移
Q->front = (Q->front+!)%MAXSIZE;
return OK;
}
4.13 队列的链式存储结构及实现
队列的链式存储结构, 其实就是线性表的单链表, 只不过它只能尾进头出而已, 我们把它简称为链队列
空队列时, front 和 rear 都指向头结点
链队列的结构:
- 头指针指向链队列的头结点.
- 队尾指针指向终点结点
typedef int QElemType; // QElemType 是int 类型, (可以根据实际情况自己设定类型)
typedef struct QNode // 结点结构
{
QElemType data; // 结点数据
struct QNode *next;//存放下一个结点的指针
}
typedef struct
{
QueuePtr front, rear; // 队头, 队尾 指针
} LinkQueue;
队列的链式存储结构-----入队操作
Status EnQueue(LinkQueue *Q, QElemType e)
{
// 给新的队列元素分配内存
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if(!s)
{
exit(OVERFLOW); // 退出程序, 并返回 OVERFLOW 的值给主程序.(当运算溢出时)
}
s->data = e; // 新的队列元素赋值
s->next = Null; // 新的队列元素的下一个元素为 Null
Q->rear->next = s; // 队尾指针的下一个元素 为 s
Q->rear = s; // 队尾指针指向 s
return OK;
}
队列的链式存储结构---出队操作
// 若队列不空, 删除Q的队头元素, 用e返回其值, 并返回OK, 否则返回ERROR
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
// 判断队列是否为空
if(Q->front == Q->rear)
{
return ERROR;
}
// p 为队首
p = Q->front->next;
// 取出队首的值
*e = p->data;
// 队头指针后移
Q->front->next = p->next
// 如果队列被清空
if(Q->rear == p)
{
Q->rear = Q -> front;
}
// 释放p
free(p);
return OK
}
- 循环队列和链队列比较:
- 时间上: 没有什么大的区别
- 空间上: 循环队列必须有一个固定的长度, 就有了存储元素个数和空间浪费的问题.空间上,链队列会更加灵活.
可以确定队列长度最大值时,使用循环队列
不能预估队列长度时, 使用链队列