数据结构与算法5:栈和队列

第三章 栈和队列

3.1、栈和队列的定义和特点

栈和队列是两种常用的、重要的数据结构
栈和队列是限定插入和删除只能在表的“端点”进行的线性表
栈和队列是线性表的子集(是插入和删除位置受限的线性表)

3.1.1、普通线性表的插入和删除操作

在这里插入图片描述

注解:
插入的位置可以在所有元素的之后,所有元素之后不需要移动元素,也可以在某个中间的位置插入,那么这个位置之后所有的元素都需要依此后移,等元素插入进去;也可以插入到第一个位置,如果插入到第一个位置,则所有的元素都需要后移动。
删除的位置如果在最后一个位置,则无需移动其他元素,只需要将最后一个元素删除即可;如果删除中间的元素,删除之后,则需要将后面的元素依此前移;如果删除第一个元素,则其后面的元素都需要前移。

在这里插入图片描述

注解:线性表呢?插入的时候可以在i个位置上来插入,插入的位置范围是(1,n+1)(1<= i <= n +1)。删除的时候可以在i个位置上删除,删除的位置范围是(1,n)(1<= i <= n)。栈和队列呢,就不能在任意的位置上删除了,我们规定它只能到表的端点,其中呢?插入的时候只能插入到表尾,也就是n+1位置,在删除的时候也只能删除最后一个第n元素。例子如下:

在这里插入图片描述

3.1.2、栈的应用——后进先出

由于栈的操作具有后进先出的固有特性,使得栈成为程序设计中有用工具。另外,如果问题求解的过程具有“后进先出”的天然特性的话,则求解的算法中也必然需要利用“栈”。
● 数据转换
● 表达式求值
● 括号匹配的检验
● 八皇后问题
● 行编辑程序
● 函数调用
● 迷宫求解
● 递归调用的实现

注解:那么队列呢?队列也是对表的端点操作,插入的时候限定插入在表尾,删除的的时候限定只能删除第一个元素。

在这里插入图片描述

3.1.3、队列常见应用

由于队列的操作具有先进先出的特性,使得队列成为程序设计中解决类似排队问题的有用工具。
● 脱机打印输出:按申请的先后顺序依此输出。
● 多用户系统中,多个用户排成队,分时地循环使用CPU和主存。
● 按用户的优先级排成多个队,每个优先级一个队列。
● 实时控制系统中,信号按接受的先后顺序依此处理。
● 网络电文传输,按到达的时间先后顺序依次进行。

3.1.4、栈的定义和特点

栈(stack)——是一个特殊的线性表,是限定仅在一端(通常是表尾)进行插入和删除操作的线性表。又称后进先出(Last In First Out)的线性表,简称LIDFO结构。
在这里插入图片描述

栈的相关概念

是仅在表尾进行插入、删除操作的线性表。
表尾(即an端)称为栈顶Top;表头(即a1端)称为栈底Base
在这里插入图片描述

插入元素到栈顶(即表尾)的操作,称为入栈
栈顶(即表尾)删除最后一个元素的操作,称为出栈
在这里插入图片描述

栈的示意图

在这里插入图片描述

入栈的示意图:
在这里插入图片描述

出栈的示意图
在这里插入图片描述

思考
在这里插入图片描述

总结:

栈的相关概念:
● 1、定义:限定只能在表的一端进行插入和删除运算的线性表(只能在栈顶操作)
● 2、逻辑结构:与同线性表相同,仍为一对一关系
● 3、存储结构:用顺序栈或链栈存储均可,但以顺序栈更常见
● 4、运算结构:只能在栈顶运算,且访问结点时依照后进先出(LIFO)的原则
● 5、实现方式:关键时编写入栈和出栈函数,具体实现依顺序栈或链栈的不同而不同。
栈与一般线性表有什么不同
● 栈与一般线性表的区别:仅在运算滚则不同。
在这里插入图片描述

3.1.5、队列的定义和特点

队列(queue)是一种先进先出(Frist In Frist Out----FIFO)的线性表。在表一端插入(表尾),在另一端(表头)删除
在这里插入图片描述

总结
队列的相关概念
● 1、定义:只能在表的一端进行插入运算,在表的另一端进行删除运算的线性表(头删尾插
● 2、逻辑结构:与线性表相同,仍为一对一关系。
● 3、存储结构顺序队或链队,以循环顺序队列更常见。
● 4、运算规则:只能在队首和队尾运算,且访问结点时依照先进先出(FIFO)的原则
● 5、实现方式:关键是掌握入队出队操作,具体是实现依顺序队或链队的不同而不同。

3.2、案例引入

在这里插入图片描述

3.2.1、案例3.1:进制转换

十进制整数N向其他进制数d(二,八,十六)的转换是计算机实现计算的基本问题
转换法则除以d倒取余
该转换法则对应于一个简单算法原理:
在这里插入图片描述

其中:div 为整除运算,mod为求余运算
例如:把十进制数159转换成八进制数
在这里插入图片描述

3.2.2、案例3.2:括号匹配

假设表达式中允许包含两种括号:圆括号和方括号
其嵌套的顺序随意,即:
● 1、( [ ] ( ) ) 或[ ( [ ] [ ] ) ]为正确格式
● 2、[ ( ] ) 为错误格式
● 3、( [ ( ) )或(()[ ] )为错误格式
在这里插入图片描述

注解:先将第一个圆括号左半部分入栈,接着是第二个圆括号左半部分,到第三个的时候是圆括号的右半部分,这时候第三个和第二个匹配完成便出栈,接着是第四个方括号右半部分,这时候又和第一个圆括号左半部分,发现匹配不成功。这个也是利用了栈的性质,先入栈的后匹配,后入栈的先匹配。这时候可以判断括号是否匹配,格式是否正确,有没有出现交叉的情况。

3.2.3、案例3.3:表达式求值

表达式求值是程序设计语言编译中一个最基本的问题,它的是实现也需要用栈。
这里介绍的算法是由运算符优先级运算顺序的对表达式求值算法。
————算符优先算法
表达式的组成
操作数(operand):常数、变量。
运算符(operator):算数运算符、关系运算符和逻辑运算符。
界限符(delimiter):左右括弧和表达式结束符。
任何一个算术表达式都由操作数(常量、变量)、算数运算符(+、-、*、/)和界限符(括号、表达式结束符‘#’、虚设的表达式起始符 ‘#’ )组成。后两者统称为算符。
在这里插入图片描述

为了实现表达式求值。需要设置两个栈:
一个是算符栈OPTR,用于寄存运算符。
另一个称为操作数栈OPND,用于寄存运算数和运算结果。
求值的处理过程是自左至右扫描表达式的每一个字符
● 当扫描的是运算数,则将其压入栈OPND,
● 当扫描到的是运算符时
若这个运算符比OPTR栈顶运算符的优先级高,则入OPTR,继续向后处理
若这个运算符比OPTR栈顶运算符优先级低,则从OPND栈中弹出两个运算数, OPTR中弹出栈顶运算符进行运算,并将运算结构压入栈OPND。
● 继续处理当前字符,直到遇到结束符为止。
——这个过程比较复杂,这里大概了解,后面会详细介绍。

3.2.4、案例3.4:舞伴问题

假设在舞会上,男士和女士各自排成一队。误舞会开始时,依此从男队和女队的队头开始各出一人配成舞伴。如果两队初始人数不相同,则较长的那一队中为配队者等待下一轮舞曲。现要求写一算法模拟上述舞伴配对问题。

显然,先入队的男士或女士先出对配成舞伴。因此该问题具有典型的先进先出特性,可以用队列作为算法的数据结构。

在这里插入图片描述

3.3、栈的表示和操作的实现

3.3.1、栈的抽象数据类型的类型定义

ADT Stack {
    数据对象:
             D = { ai | ai ∈ ElemSet, i = 1,2,...,n,n >= 0 }
    数据关系: 
             R1 = { < ai- 1, ai >| ai - 1, ai ∈ D,i =2,...,n } 
              约定an端为栈顶,a1端为栈底。  
    基本操作:初始化、进栈、出栈、取栈顶元素等
} ADT Stack

InitStack(&S) 初始化操作
操作结果:构造一个空栈S。
DestroyStack(&S) 销毁栈操作
初始条件:栈 S 已存在。
操作结果: 栈 S已被销毁。
StackEmpty(S) 判定 S 是否为空栈
初始条件: 栈 S 已存在。
操作结果: 若栈S为空栈,则返回TRUE,否则FALSE
StackLength(S) 求栈的长度
初始条件:栈 S 已存在。
操作结果: 返回 S 的元素个数, 即栈的长度。
GetTop(S,&e) 取栈顶元素
初始条件: 栈 S 已存在且非空。
操作结果: 用e 返回S 的栈顶元素。
ClearStack(&S) 栈置空操作
初始条件:栈 S 已存在。
操作结果: 栈 S 清为空栈。
Push(&S,e) 入栈操作
初始条件: 栈S已存在。
操作结果: 插入元素e为新的栈顶元素。
Pop(&S,&e) 出栈操作
初始条件: 栈 S 已存在且非空。
操作结果: 删除S的栈顶元素an,并用e返回其值。

3.3.2、顺序栈的表示

由于栈本身就是线性表,于是栈也有顺序存储和链式存储两种实现方式。
● 栈的顺序存储——顺序栈
● 栈的链式存储——链栈
存储方式:同一般线性表的顺序存储结构完全相同,利用一组地址连续的存储单元依此连续的存储单元依此存放栈底到栈顶的数据元素。栈底一般在低地址端
● 附设top指针,指示栈顶元素在顺序栈中的位置。
● 零设base指针,指示栈底元素在顺序栈中的位置。但是,为了操作方便,通常top指示真正的栈顶元素之上的下标地址
● 另外,用stacksize 表示栈可使用的最大容量
在这里插入图片描述
在这里插入图片描述

注解:这里认识一下栈的各种情况:这个栈最多能放4个元素,即stacksize = 4;空栈:最左边的是空栈,什么是空栈呢?空栈就是top指针和base执政都指向0,它两个的值是一样的,即base == top;入栈:什么是入栈呢?就本例而言就是往这个初始化为0,最大容量为4的栈里面放入放元素,当A元素扔进去后这个top指针就会向上走一步指向下标为1的位置,这就是说栈里面有一个元素了,同理,其他元素也是如此,直到放到第三个的图示情况,D放满这个栈的最大容量后,这个top指针指向下标为4的位置,这时候还能插入元素吗,是怎么判断的呢?这便是利用top与base的差值为最大容量判断的,只要 top - base等于最大容量stacksize了,就不能往里面插元素了,要是继续扔元素(上溢)该怎么处理呢?处理办法如下:
栈满时的处理办法:
1、报错,返回操作系统。
2、分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈。但是这个办法比较费时,尤其是元素比较多的时候,不到万不得已不要用。
出栈():有入栈就会出栈,情况和入栈差不多,就是把元素从这个栈顶部(栈顶)到栈底部(栈底)依此取出来,取一个,top指针向下移动一个,直到top再次等base就没有元素可以取了,要是还想取,这行为那就叫做下溢

使用数组作为顺序栈存储方式的特点
简单、方便、但容易产生溢出(数组大小固定)
上溢(overflow):栈已经满,又要压入元素
下溢(underflow):栈已经空,还要弹出元素
注:上溢是一种错误,使问题的处理无法处理进行;而下溢一般认为是一种结束条件,即问题处理结束。
在这里插入图片描述

注解:此处表示stacksize是用下标整数相减的,但是能用指针相减吗?可以,两个指针的相减实质是它们之间的差值,而且它们的差值与用整数相减的效果是一样的,则可以使用指针相减代替整数相减,而且这里的指针可以用定义成int类型,这也是没有问题的,但前提条件是这里的指针是同一个数组里面的。

3.3.3、顺序栈的实现——涉及栈的相关操作

【算法3.1】顺序栈的初始化
在这里插入图片描述

顺序栈判断是否为空
在这里插入图片描述

注解:top指针和base指针是否相等。

求顺序栈的长度
在这里插入图片描述

注解:两个指针之差,top指针减去base。

清空顺序栈
在这里插入图片描述

注解:清空顺序栈,是清空顺序栈的内容,如果栈中有元素,那就把base指针赋值给top指针,使得top指针和base指针相同,它们之间没有空间去存储元素,这样就清空顺序栈了。

销毁顺序栈
在这里插入图片描述

注解:实质是销毁空间,释放到内存池当中去了。这样顺序表里面不仅是里没有内容了,而且这栈本身也没有了。delete S.base是数组回归内存,和其下2句是把这个DestroyStack结构设置为空了。

【算法3.2】顺序栈的入栈
在这里插入图片描述

注解:入栈就是把元素存进去,每存进一个元素,栈里面就多了一个元素,top指针就向上移动一个位置,总结为第一存元素,其次top指针上移两个步骤。但是如果top指针已经满了呢?再往里面存就存不进去了,要想确保入栈这个操作正常进行,需要在存取元素前先要有个判断才行,满了就不再往里面存了。S.top,这里“”的意思是代表指针当前所指的那那块空间进行操作。的运算级比++高,所以先执行S.top=e;后执行S.top++这个可以合成一句:*S.top ++ = e;

【算法3.3】顺序栈的出栈
在这里插入图片描述

注解:关于出栈有2个问题,最核心的操作是当元素取出的是时候top指针会随着元素的减少会下移。另为一个问题是怎么判断什么时候停止的问题,在取出的过程一定会元素取完的时候,这时候top指针会和base指针同时指向0,即 S.top == S.base,所以依此为信号可以做一个判断操作。

3.3.4、链栈的表示

链栈运算受限的单链表,只能在链表头部进行操作。
在这里插入图片描述

注解:也是定义一个结构类型,这个结构类型呢,叫做栈的结点(struct StackNode),每个结点包含一个数据域(data)和一个指针域(next),其中数据域是存放栈中元素的,所以它是栈里头的类型;指针域是指向下一个元素的,所以它指向的类型也是这样(struct StackNod)的一个结构类型,所以仍然用结构类型定义这个指针,这种叫做自己定义自己的方式叫做嵌套类型。我们定义这样的类型struct StackNode{ }就叫做结点类型StackNode,然后再用这个类型定义一个指向这样结点的指针类型*LinkStack,然后用大写字母S代表这个栈。

注意:链栈中指针的方向。
● 链表的头指针就是栈顶
● 不需要头结点
● 基本不存在栈满的情况
● 空栈相当于头指针指向空
● 插入和删除仅在栈顶处执行

3.3.5、链栈的实现

【算法3.5】链栈的初始化
在这里插入图片描述

【补充算法】判断链栈是否为空
在这里插入图片描述

【算法3.6】链栈的入栈
在这里插入图片描述

【算法3.7】链栈的出栈
在这里插入图片描述

【算法3.8】取栈顶元素

在这里插入图片描述

3.4、栈与递归

3.4.1、递归

递归的定义
● 若一个对象部分地包含它自己,或用它自己给自己定义 ,则称这个对象是递归的;
● 若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。
在这里插入图片描述

以下三种情况常常用到递归方法
● 递归定义的数学函数
● 具有递归特性的数据结构
● 可递归求解的问题
举例
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

递归问题————用分治法求解
分治法:对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解
必备的三个条件:
● 1、能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是变化有规律的
● 2、可以通过上述转化而使问题简化
● 3、必须有一个明确的递归出口,或称递归的边界
分治法求解递归问题算法的一般形式:

Void p(参数表){
    if (递归结束条件) 可直接求解步骤; ----基本项
    else p(较小的参数); -----归纳项
}

例如:

Long Fact(long n){
    if(n == 0){
        return 1;//基本项
    }else {
        return n*Fact(n-1);//归纳项
    }
}
**函数调用过程**

调用前,系统完成:
(1)将实参,返回地址等传递给被调用函数
(2)为被调用函数的局部变量分配存储区
(3)将控制转移到被调用函数的入口
调用后,系统完成:
(1)保存被调用函数的计算结果
(2)释放被调用函数的数据区
(3)依照被调用函数保存的返回地址将控制转移到调用函数
当多个函数构成嵌套调用时:类似于栈
在这里插入图片描述

求解阶乘n!的过程:todo
在这里插入图片描述
在这里插入图片描述

递归函数调用的实现

“层次”主函数0层
第一次调用1层
第i次调用i层

”递归工作栈“——递归程序运行期间使用的数据存储区
”工作记录“——>实在参数、局部变量、返回地址
递归的优缺点:
● 优点:结构清晰,程序易读
● 缺点:每次调用的要成成工作记录,保存状态信息,入栈;返回时要出栈,恢复状态信息。时间开销大。
所以程序来说我们愿意用递归,从时间效率来说是比较差的,如果有时候对时间效率比较高的时候我们就要把递归变成非递归,方法如下(简单了解):
● 方法一:尾递归、单向递归——>循环结构
● 方法二:自用模拟系统运行时栈
尾递归——>循环结构
在这里插入图片描述

单向递归——>循环结构
虽然有一处以上的递归调用语句的参数只和主调函数有关,相互之间参数无关,并且这些递归调用语句处于算法的最后
在这里插入图片描述

借助栈改写递归
● 递归程序在执行时需要系统提供栈来实现
● 仿照递归算法执行过程中递归工作栈的状态变化可写出相应的非递归程序
● 改写后的非递归算法与原来的递归算法相比,结构不够清晰,可读性较差,有的还需要经过一系列优化

3.5、队列的表示和操作的实现

3.5.1、队列的表示和实现

队列示意图
在这里插入图片描述

相关术语
● 队列(Queue)是仅在表尾进行插入操作,在表头进行删除操作的线性表。
● 表为即an端,称为队尾;表头即a1端,称为队头。
● 插入元素称为入队;删除元素称为出队。
● 队列的存储结构为链式或顺序队(常用循环循序队)
● 它是一种先进先出(FIFO)的线性表。
在这里插入图片描述

队列的相关概念
在这里插入图片描述

队列的常见应用
在这里插入图片描述

队列的抽象数据类型定义
在这里插入图片描述

● 队列的物理存储可以用顺序结构,也可以用链式存储结构。相应地,队列的存储方式也分为两种,即顺序队列和链式队列
● 队列的顺序表示——用一组数组base[MAXQSIZE]
3.5.2、顺序队列表示和实现
在这里插入图片描述

#define MAXQSIZE 100   //最大队列长度
Typedef struct {
    QElemType *base;  //初始化的动态分配存储空间
    int front;  //头指针
    int rear;   //尾指针
}SqQueue;

对空、入队、出队、溢出

在这里插入图片描述

怎么解决假溢出问题呢?————引入循环队列模运算取余运算
● 1、将队中元素依次向对头方向移动。缺点:浪费时间。每移动一次,队中元素都要移动。
● 2、将队空间设想成一个循环的表,即分配给队列的m个存储单元,可以循环使用,当rear为maxqsize时,若向量的开始端空着,有可以从头使用空着的空间。当front 为maxqsize时,也是一样的。
循环队列:当base[0]接在base[MAXQSIZE -1]之后,若rear +1 == M,则令rear = 0;
● 实现方法:利用模(mod,C语言中:%)运算
● 插入元素:Q.base[Q.rear] = x; Q.rear = (Q.rear +1)% MAXQSIZE;
● 删除元素;x = Q.base[s.front] Q.front = (Q.front +1)% MAXQSIZE
● 循环队列:循环使用队列分配的存储空间。
怎么判断队空和队满的问题?
在这里插入图片描述

我们知道不管是队空还是队满的时候,其头指针front 和尾指针rear均指在一块区域,那么怎么区分队空和队满呢?
解决方案:
● 1、另外设一个标志以区别队空、队满
● 2、另设一个变量,记录元素个数
● 3、少用一个元素空间
循环队列解决队满时判断方法——少用一个元素空间
在这里插入图片描述

循环队列的操作
● 队列的初始化
在这里插入图片描述

Status InitQueue( SqQueue & Q){
    Q.base = new QEleType[MAXQSIZE]    //分配数组去空间,为什么这里是一个指针呢?因为数组元素的首地址就是一个指针
    //c语言: Q.base = (QElemType*)malloc(MAXQSIZE*sizeof(QElemType));
    if(!Q.base) exit(OVERFLOW);   //存储分配失败
    Q.front = Q.rear = 0;   //头指针尾指针为0,队列为空
    return OK; 
}

● 求队列的长度
在这里插入图片描述

int QueueLength (SqQueue Q){
    return ((Q.rear - Q.front + MAXQSIZE) % MAXQSIZE);
} 

● 循环队列入队
在这里插入图片描述

Status EnQueue(SqQueue & Q,QElemType e){
    if((Q.rear + 1)% MAXQSIZE == Q.front)return ERROR;  //队满
    Q.base[Q.rear] = e;   //新元素加入队尾
    Q.rear = (Q.rear + 1) % MAXQSIZE;    //队尾指针 +1
    return OK;  
}

● 循环队列的出队
在这里插入图片描述

Status DeQueue()SqQueue&Q,QEleType & e){
    if(Q.front == Q.rear) return ERROR;  //队空
    e = Q.base[Q.front];                 //保存队头元素
    Q.front = (Q.front + 1)%MAXQSIZE;    //队头指针+1
    return OK;
}

3.5.3、链式队列表示和实现

若用户无法估计所用队列的长度,则宜采用链式队列
在这里插入图片描述

链队列的类型定义
在这里插入图片描述

#define MAXQSIZE 100 //最大队列长度
typedef struct Qnode {
    QElemType data;
    stuct Qnode *next;
}QNode,*QuenePtr;

typedef struct{
    QuenePtr front;  //队头指针
    QuenePtr rear; //队尾指针
}LinkQueue;

链队列运算指针变化状况
在这里插入图片描述

链队列的操作——链队列初始化(算法3.16)
在这里插入图片描述

Status InitQueue (LinkQueue & Q){
    Q.front = Q.rear = (QueuePtr) malloc(sizeof(QNode));
    if(!Q.front)exit(OVERFLOW);
    Q.front->next = NULL;
    return OK;
}

链队列的操作——销毁链队列(算法3.16)
算法思想:从队头结点开始,依次释放所有结点
在这里插入图片描述

Status DestroyQueue (LinkQueue &Q){
    while(Q.front){
        p = Q.front -> next; free(Q.front); Q.front = p;
        //可以写成Q.rear = Q.front -> next;  free(Q.front);  Q.front = Q.rear;
    }
    return OK;
}

链队列的操作——将元素e入队(算法3.17)
在这里插入图片描述

Status EnQueue(LinkQueue & Q, QElemType e){
    p = (QueuePtr)malloc(sizeof(QNode));
    if(!p) exit(OVERFLOW);
    p -> data = e; p -> next = NULL;
    Q.rear -> next = p;
    Q.rear = p;
    return OK;
}

链队列的操作——链队列出队(算法3.18)
在这里插入图片描述
在这里插入图片描述

Status DeQueue( LinkQueue & Q, QElemType & e){
    if( Q.front == Q.rear ) return ERROR;
    p = Q.front -> next;
    e = p -> data;
    Q.front -> next = p -> next;
    if( Q.rear == p ) Q.rear = Q.front;
    delete p;
    return OK;
}

链队列的操作——求链队列的队头元素(算法3.19)
在这里插入图片描述

Status GetHead( LinkQueue Q, QElemType & e){
    if( Q.front == Q.rear ) return ERROR;
    e = Q.front -> next -> data;
    return OK;
}

3.6、案例分析与时实现

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值