第三章 栈和队列
3.1 栈和队列的定义和特点
- 栈和队列是两种常用的,重要的数据结构
- 他们两个不过是限定了插入和删除的位置,只能在表的"端点"进行的线性表
栈的添加和删除只能在表尾:我们又把这个表尾称为栈顶,压栈叫push,弹栈叫pop
- 生活中我们的手电筒,子弹弹匣等都是栈的结构
- 许多问题:数制转换,表达式求值,括号匹配的校验,八皇后问题,行编辑程序,函数调用,迷宫求解,递归函数调用的实现,都会利用到栈
队列只能在表尾添加,删除只能在表头:
- 对于程序设计中解决排队问题很有帮助:
- 脱机打印输出:按申请顺序的先后顺序一次输出
- 多用户系统中,多个用户排成队,分时的循环使用CPU和主存
- 按用户的优先级排成多个队,每个优先级一个队列
- 实时控制系统中,信号按接收的先后顺序依次处理
- 网络电文传输,按到达的时间先后顺序依次进行
3.1.1 栈的定义和特点
栈是一个特殊的线性表,是限定仅在一端进行插入和删除的线性表
-
又称为**后进先出(Last In First Out)**的线性表,简称LIFO结构
-
表尾(an)称为栈顶Top,表头(a1)称为栈底Base
-
插入叫做入栈,压栈,删除叫做压栈,出栈,弹栈
**存储结构:**两种结构·都行
运算规则: LIFO规则
实现方式: 关键是编写入栈和出栈函数,顺序栈和链栈实现不太一样
3.1.2 队列的定义和特点
队列是一种先进先出(First In First Out FIFO)的线性表。在表一段插入(表尾),在另一端(表头)删除
逻辑结构: 同线性表相同,一对一关系
存储结构:顺序队或链队,以循环顺序队更加常见
运算规则: 先进先出(FIFO)
实现方式: 关键是掌握入队和出队操作,具体实现依顺序队或链队不同而不同
3.2 案例引入
3.2.1 进制转换
- 十进制整数N向其他进制数d(二、八、十六)的转换是计算机实现计算的基本问题
转换法则: 除以d倒取余
算法原理: n = (n div d) * d + n mod d (div为整除运算,mod为求余运算)
例如: 把十进制数159转换成八进制数
3.2.2 括号匹配校验
例如: 检验(( )])
是否匹配
3.2.3 表达式求值
-
表达式求值是程序涉及语言编译中一个最基本的问题,他的实现也要运用到栈.
-
这里介绍的算法是由运算符优先级确定运算顺序的对表达式求值算法—算符优先算法
表达式的组成
-
操作数:常量,变量
-
**运算符:**算数运算符,关系运算符,和逻辑运算符
-
界限符: 左右括弧和表达式结束符
-
任何一个表达式都是由这三部分组成的
例如:# 3 * (7 - 2) #(#是界限运算符)
我们需要设置两个栈
- 一个是算符栈OPTR,用于寄存运算符
- 另一个称为操作数栈OPND,用于寄存运算数和运算结果
3.2.4 舞伴问题(只有这个是队列问题)
-
假设在舞会上,男士和女士各自排成一一队.舞会开始的时候,依次从男队和女队的队头各出一人配成舞伴.如果两队初始人数不同,那么较长的那一队中未配对的人等待下一轮舞曲.
-
判断关键条件: 某队变为空
3.3 栈的表示和操作的实现
栈的表示
常用操作
- InitStack(&S) 初始化操作,创建一个空栈S
- DestroyStack(&S) 销毁一个栈
- StackEmpty(S) 判定栈是否为空,返回TRUE或FALSE
- StackLength(S) 返回栈的长度
- GetTop(S, &e) 取栈顶元素,用e返回
- ClearStack(&S) 将栈置空
- Push(&S,e) 压栈操作,插入e元素
- Pop(&S,&e) 出栈操作 用e返回弹出的元素
3.3.1 顺序栈
**存储方式:**同一般的顺序表存储结构完全相同
- 利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素.栈底一般在低地址端.
- 附设top指针,指示栈顶元素在顺序栈的位置
- 为了方便操作,top指针其实指向栈顶元素的上一个下标地址
- 附设base指针,指示栈低元素在顺序栈的位置
- 另外,用stacksize表示栈可使用的最大容量(因为c语言中数组索引不能越界,为了防止越界,我们就设定这个)
- 附设top指针,指示栈顶元素在顺序栈的位置
base == top 是栈空的标志
top - base == stacksize 是栈满的标志
栈满时候的处理方法
- 报错,返回操作系统
- 分配更大的空间,作为栈的储存空间,将原栈的内容移入新栈
栈溢出有两种
- 上溢:栈已经满了,但是还要继续插入的时候
- 一般认为是一种错误
- 下溢: 栈已经没有元素了,但是还要执行出栈
- 一般认为是一种结束条件,即问题处理结束
顺序栈的表示
#define MAXSIZE 100;
typedef struct{
SElemType *base;//指向元素的指针
SElemType *top;
int stacksize;//栈可用最大容量
}SqStack;
顺序栈的初始化
Status InitStack(SqStack &S)
Status InitStack(SqStack &S){
S.base = new SElemType[MAXSIZE];//S.base = (SElemType*)malloc(MAXSIZE*sizeof(SElemType))
if(!S.base) exit(OVERFLOW);//内存分配失败
S.top = S.base;
S.stacksize = MAXSIZE;
}
判断顺序栈是否为空
Status StackEmpty(SqStack S){
if(S.top == S.base){
return TRUE;
}else{
return FALSE;
}
}
求栈的长度
int StackLength(SqStack S){
return S.top - S. base;
}
清空顺序栈
- 不需要把里面的元素清空,直接将top指针指向base就可以了
Status ClearStack(SqStack &S){
if(S.base)//简单判断一下是否有base指针存在
S.top = S.base;
return OK;
}
销毁顺序栈
Status DestroyStack(&S){
if(S.base){
delete S.base;//删除这个指针意思就是把其指向的堆区的数据也一起删除了,这里删除的就是我们创建的栈
S.stacksize = 0;
S.base = S.top = NULL;
}
return OK;
}
顺序栈的入栈(重要)
- 检测栈是否是满的,存进去元素之后top指针后移
Status Push(SqStack &S,SElemType e){
if(S.top - S.base == S.stacksize ) return ERROR;
*S.top = e;
S.top++;//指针的++其实就是 top = top + sizeof(SElemType *) ;
//其实上面两步可以简化为一步
//*S.top++= e;
return OK;
}
顺序栈的出栈
- 检测栈是否是空的,不是空的那么我们将top指针–,然后将元素取出,;
Status Pop(SqStack &S,SElemType &e){
if(S.top == S.base ) return ERROR;
S.top--;
e = *S.top;
//当然可以化为一步
//e = *--S.top;
return OK;
}
3.3.2 链栈
- 链栈是运算受限的单链表,只能在链表头部进行操作
定义
注意:
- 链栈中的指针的方向是an指向a1(为了操作方便)
- 链表的头指针就是栈顶
- 不需要头结点
- 基本不存在栈满的情况
- 空栈相当于头指针为空
- 插入和删除仅在栈顶处执行
链栈的初始化
void InitStack(LInkStack &S){
//令其头指针为空就可以了
S =NULL;
return OK;
}
链栈是否为空
Status StackEmpty(LinkStack S){
if(S ==NULL) return TRUE;
else return FALSE;
}
链栈的入栈
- 直接不多bb,直接插入
Status Push(LinkStack &S, SElemType e){
StackNode *p;//生成新结点
p->data = e;//关键操作,自己体会
p->next = S;
S = p;
return OK;
}
链栈的出栈
- 先看看头指针是不是空的,不是空的就大胆的丢掉那个
Status Pop(LinkStack &S,SElemType &e){
if(S == NULL) return ERROR;
e = S->data;
p = new StackNode;
p = S;
S = S->next;
delete p;
return OK;
}
获取栈顶元素
SElemType GetTop(LinkStack S){
if(S!= NULL){
return S -> data;
}
}
3.4 栈和递归
递归的定义
- 若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的.
- 我们可以发现,单链表也是递归的
- 若一个进程直接的或间接的调用自己,则这个调用的过程就是递归的过程
- 例如递归求n的阶乘
以下三种情况常常用到递归的方法
- 递归定义的数学函数
- 斐波那契数列
- 具有递归特性的数据结构
- 二叉树
- 单链表
- 广义表
- 可递归求解的问题
- 迷宫问题
- Hanoi塔
递归问题—用分治法来求解
- 分治法: 对于一个比较复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解
必备的三个条件
- 能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是变化有规律的
- 可以通过上述转化而使问题简化
- 必须有一个明确的递归出口,或称递归的边界
一般形式
void p(参数表){
if(递归条件) 可直接求解步骤 //基本项
else p(较小的参数)//归纳项
}
-
递归函数调用的实现
- “递归工作栈” ——递归程序运行期间使用的数据存储区
- 储存了实在参数,局部变量,返回地址
- “递归工作栈” ——递归程序运行期间使用的数据存储区
-
递归的优缺点
- **优点:**结构清晰,程序易读
- 缺点: 每次调用都要生成工作记录,保存状态信息,入栈;返回是要出栈,回复状态信息。时间开销比较大
-
有时候我们要递归变成非递归
-
方法1: 尾递归、单项递归转换成循环结构
-
//伪递归 long Fact(long n){ if(n ==0) return 1; else return n *Fact(n-1); } //变成 long Fact(long n){ t=1; for(i =1;i<=n;i++){ t*=i; } return t; }
-
//单项递归 long Fib(long n){ if(n==1 ||n==2) return 1; else return Fib(n-1) + Fib(n-2); } //变成 long Fib(long n){ if(n==1 ||n==2) return 1; else{ for(i=3;i<=n;i++){ t3 = t1+t2; t1 = t2; t2 = t3; } return t3; } }
-
-
方法2: 自用栈模拟系统的运行时栈
-
-
改写后可能会结构不清晰等问题,可能还要经过优化
3.5 队列的表示和操作的实现
3.5.1 队列的顺序表示和实现
-
队列的顺序表示:用一维数组base[MAXQSIZE]
-
#define MAXQSIZE 100 Typedef struct{ QElemType *base; int font; int rear; }SqQueue
真溢出与假溢出
- 当
rear == MAXQSIZE 但是font != 0
时,是假溢出
解决上溢的方法
- 将队中元素依次向对头方向移动(这个方法浪费时间,效率低下)
- 将空间想想成为一个循环的表,当发生假溢出的时候,将front指针指向数组的第一个元素(使用该方法)
- base[0]接在base[MAXSQSIZE - 1]后面,当rear + 1 ==MAXQSIZE时,令rear =0;
- 实现方法:**利用 模运算
- 可是这样会出现一个问题,队空和队满时我们判断依据都是front == rear,怎么解决?
- 另设一个标志以区别队空和堆满
- 另设一个变量,记录元素个数
- 少用一个元素空间
这里,我们使用少用一个元素空间来解决,也就是说,rear指向base[MAXSQSIZE - 1]这个地方的时候,就表示队列已经满了,即通过(rear +1)%6 来判断队是否已经满了
循环队列的定义和普通顺序队列定义一样
队列初始化
Status InitQueue(SqQueue &Q){
Q.base = new QElemType[MAXQSIZE];
if(!Q.base) exit(OVERFLOW);
Q.front = 0;
Q. rear = 0;
return OK;
}
队列长度计算
int QueueLength(SqQueue Q){
return (Q.rear - Q.front + MAXQSIZE)%MAXQSIZE;
}
队列出队
Status DeQueue(SqQueue &Q,QElemTyppe &e){
if(Q.front == Q. rear) return ERROR;
e = Q.base[Q.front];
Q.front = (Q.front+1)%MAXQSIZE;
return OK;
}
队列元素入队
Status EnQueue(SqQueue &Q,QElemTyppe e){
if((Q.rear + 1)%MAXQSIZE == Q.front) return ERROR;
Q.base[Q.rear] = e;
Q.rear = (Q.rear+1)%MAXQSIZE;
return OK;
}
取队头元素
SElemType GetHead(SqQueue Q){
//先判断是否为空
if(Q.front == Q. rear) return ERROR;
return Q.base[Q.front];
}
3.5.1 队列的链式表示和实现
- 若用户无法估计所用队列的长度,就可以采用链队
链队列的类型定义
-
结点的定义
#define MAXQSIZE 100 typedef struct Qnode{ QElemType data; struct Qnode *next; }QNode, *QueuePtr
-
链队的指针的定义(使用这个创建链队,相当于头指针)
typedef struct{ QuenePtr front; QuenePtr rear; }LinkQueue;
链队列运算指针变化状况
链队列的初始化
Status InitQuene(LinkQueue &Q){
Q.front = Q.rear = (QueuePtr) malloc(sizeof(QNode));
if(!Q.front) exit(OverFlLOW);
Q.front->next = NULL;
return OK;
}
链队的销毁
Status DestroyQueue(LinkQueue &Q){
while(Q.front){
p = new Qnode;
p = Q.front ->next;
free(Q.front);
Q.front = p;
}
return OK;
}
链队的入队
元素的入队在队尾入
Status EnQueue(LinkQueue &Q, QElemType e){
p = new Qnode;
if(!p) exit(OVERFLOW)
p.data = e;
p->next =NULL;
Q.rear->next = p;
Q.rear = p;
return OK;
}
链队的出队
Status DeQueue(LInkQueue &Q, QElemType &e){
if(Q.rear == Q.front) return ERROR;
p = new Qnode;
p = Q.front->next;
Q.front ->next = p->next;
if(Q.rear ==p) Q.rear=Q.front;//如果删除的是最后一个元素,那么就让尾指针指回头结点
delete p;
return OK;
}
链队的销毁
- 从对头开始删除
Status DestroyQueue(LinkQueue &Q){
while(Q.front){
p = Q.front->next;
free(Q.front);
Q.front = p;
}
return OK;
}
获取链队头元素
Status GetHead(LinkQueue Q, QElemType &e){
if(Q.front == Q.rear) return ERROR;
e = Q.front->next->data;
reurn OK;
}
next =NULL;
Q.rear->next = p;
Q.rear = p;
return OK;
}
#### 链队的出队
```c++
Status DeQueue(LInkQueue &Q, QElemType &e){
if(Q.rear == Q.front) return ERROR;
p = new Qnode;
p = Q.front->next;
Q.front ->next = p->next;
if(Q.rear ==p) Q.rear=Q.front;//如果删除的是最后一个元素,那么就让尾指针指回头结点
delete p;
return OK;
}
链队的销毁
- 从对头开始删除
Status DestroyQueue(LinkQueue &Q){
while(Q.front){
p = Q.front->next;
free(Q.front);
Q.front = p;
}
return OK;
}
获取链队头元素
Status GetHead(LinkQueue Q, QElemType &e){
if(Q.front == Q.rear) return ERROR;
e = Q.front->next->data;
reurn OK;
}