【考纲内容】
(一)栈和队列的基本概念
(二)栈和队列的顺序存储结构
(三)栈和队列的链式存储结构
(四)栈和队列的应用
(五)特殊矩阵的压缩存储
【知识框架】
栈
栈的基本概念
栈(Stack)
:只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但是限定只能在一端进行插入和删除操作。
栈顶(Top)
:线性表允许插入的那一端。
栈底(Bottom)
:固定的、不允许进行插入和删除的那一端。
空栈
:不含任何元素的空表。
栈的顺序存储结构
栈的顺序存储称为顺序栈,它是利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶的位置。
#define MaxSize 50//定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize];//存放栈中元素
int top;//栈顶指针
} SqStack;
栈顶指针:S.top,初始时设置S.top=-1;栈顶元素:S.data[S.top]。
出栈操作:栈不满时,栈顶指针先加1,再送值到栈顶元素。
出栈操作:栈不空时,先取栈顶元素值,再将栈顶指针减1。
栈空条件:S.top==-1;
栈满条件:S.top==MaxSize-1;
栈长:S.top+1;
共享栈
利用栈底位置相对不变的特性,可以让两个顺序栈共享一个以为数据空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸,如图。
两个栈的栈顶指针都指向栈顶元素,top=-1是0号栈为空,top1=MaxSize时1号栈为空;仅当两个栈顶指针相邻(top1-top0=1)时,判断为栈满。当0号栈进栈时top0先加1再赋值,1号栈进栈时top1先减1再赋值。
共享栈是为了更有效的利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满的情况下才会发生上溢。
栈的链式存储结构
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在沾满上溢出的情况。通常采用单链表实现,并规定所有的操作都是在单链表的表头进行。
typedef struct Linknode{
ElemType data;//数据域
struct Linknode *next//指针域
} *LiStack;//栈类型定义
采用链式存储,便于结点的插入和删除。链栈的操作与链表类似。
注意:区分带头结点和不带头结点。
队列
队列(Queue)
:是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。
队头(Front)
:允许删除的一端。
队尾(Rear)
:允许插入的一端。
空队列
:不含任何元素的空表。
队列的顺序存储结构
队列的顺序实现是指分配一块连续的存储单元存放队列的元素,并附设两个指针front
和rear
,分别表示队首元素和队尾元素的位置。设队头指针指向队头元素,队尾指针指向队尾元素的下一个位置(也可以让rear指向队尾元素,front指向队头元素的前一个位置)。
#define MaxSize 50//定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize];//存放队列元素
int front,rear;//队头指针和队尾指针
} SqQueue;
初始状态(队空条件):Q.front==Q.rear==0
。
进队操作:队不满时,先送值队尾元素,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1。
注意:判对空可以用上述条件Q.front==Q.rear==0
,但Q.rear==MaxSize
不能作为队满的判断条件,如上图(d)中rear
已经是MaxSize了,但是队列中只有两个元素,这是再添加元素的话,rear
超过MaxSize
产生“上溢出”,所谓的假溢出。
循环队列
对于顺序队列,假溢出会浪费极大的存储空间,因此引入循环队列的概念,将顺序队列臆造为一个环状的空间,即把存储队列的元素的表从逻辑上看成一个环,称为循环队列,当队首指针Q.front==MaxSize-1后,再前进一个位置自动到0,这可以通过除法取余运算(%)实现。
初始时:Q.front=Q.rear=0
队首指针进1:Q.front=(Q.front+1)%MaxSize
队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize
出队入队时:指针都按顺时针方向进1,如图。
那么,循环队列的队空和队满的判断条件是什么呢?显然队空的条件是Q.front==Q.rear
。如果入队速度快于出队速度,队尾指针很快就会赶上队首指针,如图(d1),此时的队满也有条件Q.rear==Q.front
。
所以为了区分队空和队满,有三种处理办法:
- 牺牲一个单元,约定以“队头指针在队尾指针的下一位置作为队满标志”,即:
(Q.rear+1)%MaxSize==Q.front
,如图(d2)。 - 类型中增设表示元素个数的数据成员,这样队空的条件为
Q.size==0
,队满的条件为Q.size==MaxSize
。 - 类型中增设tag数据成员,tag为0时,表示是因为出队导致的
Q.front==Q.rear
,此时为队空;tag为1时,表示是因为入队导致的Q.front==Q.rear
,此时为队满。
循环队列的操作
(1)初始化
void InitQueue(SqQueue &Q){
Q.rear=Q.front=0;//初始化队首、队尾指针
}
(2)判队空
bool isEmpty(SqQueue Q){
if(Q.rear==Q.front) return true;//队空条件
else return false;
}
(3)入队
bool EnQueue(SqQueue &Q,int x){
if((Q.rear+1)%MaxSize==Q.front) return false; //队满
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize;//队尾指针加1取模
return true;
}
(4)出队
bool DeQueue(SqQueue &Q,int &x){
if(Q.rear==Q.front) return false;//队空
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize;//队头指针加1取模
return true;
}
队列的链式存储结构
队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾节点,即单链表的最后一个节点。
注意:与顺序存储不同,顺序存储的尾指针指向尾结点的后一位。
typedef struct{//链式队列结点
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{//链式队列
LinkNode *front,*rear;//队列的头指针和尾指针
}LinkQueue;
当Q.front=NULL
且Q.rear==NULL
时,链式队列为空。
出队时,首先判断队列是否为空,若不空,则取出队头元素,将其从链表中摘除,并让Q.front
指向下一个结点(若该结点为最后一个节点,则置Q.front
和Q.rear
都为NULL
)。
入队时,建立一个新结点,将新结点插入到链表的尾部,并改让Q.rear
指向这个新插入的结点(若原队列为空队,则令Q.front
也指向该结点)。
链式队列的基本操作
(1)初始化
void InitQueue(LinkQueue &Q){
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));//建立头结点
Q.front->next=NULL;//初始为空
}
(2)判断空
bool IsEmpty(LinkQueue Q){
if(Q.front==Q.rear) return true;
else return false;
}
(3)入队
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));//创建新结点
s->data=x;
s->next=NULL;
Q.rear->next=s;
Q.rear=s;
}
(4)出队
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.front==Q.rear) return false;//空队
LinkNode p=Q.front->next;
x=p->data;
Q.front->next=p->next;
if(Q.rear==p)
Q.rear=Q.front;//若原队列中只有一个结点,删除后变空。
free(p);
return true;
}
双端队列
双端队列是允许两端都可以入队和出队操作的队列,其元素的结构仍是线性结构。将队列的两端称为前端和后端,两端都可以进行入队和出队。
在双端队列进队时:前端进的元素排列在队列中后端进入的元素的前面,后端进入的元素排列在对列中前端进入的元素的后面。在双端队列出队时:无论前端还是后端出队,先出的元素排列在后面的元素的前面。
思考:如何由入队序列a,b,c,d得到出队序列d,c,a,b?
输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列。
输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列。
例 设有一个双端队列,输入序列为1,2,3,4,试分别求出以下条件的输出序列。(考试时,至少花15分钟)
(1)能由输入受限的双端队列得到,但不能由输出受限的双端队列得到的输出序列。
(2)能由输出受限的双端队列得到,但不能由输入受限的双端队列得到的输出序列。
(3)既不能由输出受限的双端队列得到,也不能由输入受限的双端队列得到的输出序列。
解答
先看输入受限的双端队列,假设end1端输入1, 2, 3, 4,那么end2端的输出相当于队列的输出:1, 2, 3, 4;而end1端的输出相当于栈的输出,n=4时仅通过end1端有14种输出序列(由前面提到的Catalan公式计算得出),仅通过end1端不能得到的输出序列4!-14=10种,它们是:
1,4,2,3 2,4,1,3 3,4,1,2 3,1,4,2 3,1,2,4
4,3,1,2 4,1,3,2 4,2,3,1 4,2,1,3 4,1,2,3
通过end1和end2端混合输出,可以输出这10种中的8种,参看下表。其中,SL、XL 分别代表end1端的进队和出队,XR代表end2端的出队。
输出序列 | 进队出队顺序 | 输出序列 | 进队出队顺序 |
---|---|---|---|
1,4,2, 3 | SLXRSLSLSLXLXRXR | 3,1,2,4 | SLSLSLXLXRSLXRXR |
2,4,1,3 | SLSLXLSLSLXLXRXR | 4,1,2,3 | SLSLSLSLXLXRXRXR |
3,4,1,2 | SLSLSLXLSLXLXRXR | 4,1,3,2 | SLSLSLSLXLXRXLXR |
3,1,4,2 | SLSLSLXLXRSLXLXR | 4,3,1,2 | SLSLSLSLXLXLXRXR |
剩下两种是不能通过输入受限的双端队列输出的,即4, 2, 3, 1和4, 2, 1, 3。
再看输出受限的双端队列,如图3-14所示。假设end1端和end2端都能输入,仅end2 端可以输出。如果都从end2端输入,从end2端输出,就是一个栈了。当输入序列为1, 2, 3, 4时,输出序列有14种。对于其他10种不能得到的输出序列,通过交替从end1和end2端 输入,还可以输出其中8种。设SL代表end1端的输入,SR、XR分别代表end2端的输入 和输出,则可能的输出序列及进队和出队顺序见下表。
输出序列 | 进队出队顺序 | 输出序列 | 进队出队顺序 |
---|---|---|---|
1,4,2,3 | SLXRSLSLSRXRXRXR | 3,1,2,4 | SLSLSRXRXRSRXLXR |
2,4,1,3 | SLSRXRSLSRXRXRXR | 4,1,2, 3 | SLSLSLSRXRXRXRXR |
3,4,1,2 | SLSLSRXRSRXRXRXR | 4,2,1,3 | SLSRSLSRXRXRXRXR |
3, 1,4,2 | SLSLSRXRXRSRXRXR | 4, 3, 1,2 | SLSLSRSRXRXRXRXR |
通过输出受限的双端队列不能得到的两种输出序列是4,1,3,2和4,2,3, 1。
综上所述:
1)能由输入受限的双端队列得到,但不能由输出受限的双端队列得到的是4,1,3,2。
2)能由输出受限的双端队列得到,但不能由输入受限的双端队列得到的是4,2,1,3。
3)既不能由输入受限的双端队列得到,又不能由输出受限的双端队列得到的是4,2,3,1。
栈的应用
括号匹配
假设表达式中允许两种括号:圆括号和方括号,其嵌套的顺序任意,即([]())或[()[])]等均为正确的格式,[(])或([()为不正确的格式。
考虑下列括号序列:
[ ( [ ] ) ]
1 2 3 4 5 6
分析如下:
1)计算机接收到第1个括号“[”后,期待与之匹配的第6个括号“]”的出现。
2)获得了第2个括号“(”,此时第一个括号”[“先暂且放到一边,而急迫期待与之匹配的第5个括号“)”的出现。
3)获得了第3个括号“[”,此时第2个括号“(”先暂且放到一边,而急迫期待与之匹配的第4个括号的出现。第3个括号的期待得到满足后,第2个括号的期待匹配又成为当前最急迫的任务了。
4)以此类推,可见,该处理过程与栈的思想吻合。
算法的思想如下:
1)首先设置一个空栈,顺序读入括号。
2)若是右括号,则或者使置于栈顶的最急迫期待括号得到消解,或者是不合法的括号(退出程序)。
3)若是左括号,则作为一个新的更急迫的期待压入栈中,算法结束时,栈为空,否则括号序列不匹配。
表达式求值
前缀表达式、中缀表达式和后缀表达式都是对表达式的记法。它们之间的区别在于运算符相对与操作数的位置不同。前缀表达式的运算符位于与其相关的操作数之前。
举例:
A+B*(C-D)-E/F 中缀表达式
ABCD-*+EF/- 后缀表达式(也可由表达式树后序遍历而来)
中缀表达式不仅依赖运算符的优先级,而且还要处理括号。在后缀表达式中已经考虑了运算符的优先级,没有括号,只有操作数和运算符。
通过后缀表达式计算表达式值得过程为:
顺序扫描表达式的每一项,然后根据它的类型做如下相应操作:如果该项是操作符,则连续从占中退出两个操作数Y和X,形成运算指令XY,并将计算结果重新压入栈中。当表达式的所有项扫描完成后,栈顶存放的就是最后的计算结果。
例如,后缀表达式ABCD-*+EF/-求值的过程需要12步,见表。
步 | 扫描项 | 项类型 | 动作 | 栈中内容 |
---|---|---|---|---|
1 | 置空栈 | 空 | ||
2 | A | 操作数 | 进栈 | A |
3 | B | 操作数 | 进栈 | AB |
4 | C | 操作数 | 进栈 | ABC |
5 | D | 操作数 | 进栈 | ABCD |
6 | - | 操作符 | D、C退栈,计算C-D,结果R~1~进栈 | ABR~1~ |
7 | * | 操作符 | R1、B退栈,计算B*R~1~,结果R2进栈 | AR~2~ |
8 | + | 操作符 | R2、A退栈,计算A+R~2~,结果R~3~进栈 | R~3~ |
9 | E | 操作数 | 进栈 | R~3~E |
10 | F | 操作数 | 进栈 | R~3~EF |
11 | / | 操作符 | F、E退栈,计算E/F,结果R~4~进栈 | R~3~R~4~ |
12 | - | 操作符 | R~4~,R~3~退栈,计算R~3~-R~4~,结果R~5~进栈 | R~5~ |
递归
在一个函数、过程或数据结构的定义中又应用了它自身,那么这个函数、数据结构、过程称为是递归的。他的优点是将复杂问题简单化,减少代码量,但在通常情况下,效率不太高。
以斐波那契数列为例,其定义为
这是一个典型的递归的例子:
int Fib(n){//斐波那契数列的实现
if(n==0)
return 0;//边界条件
else if(n==1)
return 1;//边界条件
else
return Fib(n-1)+Fib(n-1);//递归表达式
}
必须注意递归模型不能是循环定义的,其必须满足以下两个条件:
递归表达式(递归体)
边界条件(递归出口)
递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题。
在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈进行数据存储,递归次数过多容易造成栈溢出。而其效率不高的原因是递归调用过程包含许多重复的计算。下面以n=5为例,列出递归调用的执行过程,如图。
显然,在递归过程中,Fib(3)被计算了2次,Fib(2)被计算了3次,Fib(1)被调用了5次,Fib(0)被调用了3次。
进制转换
假如N为输入的数,n为要转换为的进制,若要将十进制231转换为8进制数,过程如下;
N N/n N%n
231 28 7
28 3 4
3 0 3
则输出为347,可以看出,首先得到的应该是7,然后才是4,最后是3,但是要逆序显示,自然就类似压栈出栈的数据结构了所以,只需要初始化栈后,将N%n不断的压入栈底,需要注意的是如果要转换为16进制,则需要对大于9的数字作字符处理。
迷宫求解
队列的应用
图的广度优先搜索
广度优先搜索类似于二叉树的层序遍历,它的基本思想就是:首先访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点w1,w2,…,wi,然后再依次访问w1,w2,…,wi的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,再访问它们所有未被访问过的邻接顶点……依次类推,直到图中所有顶点都被访问过为止。
广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记录正在访问的顶点的下一层顶点。
如上图所示,为一个有向图,从顶点2开始广度优先遍历整个图,可知结果为2,0,3,1。
层次遍历
在信息处理中有一大类问题需要逐层处理或逐行处理。这列问题的解决方法往往是在处理当前层或当前行时就对下一层或下一行做预处理,把处理顺序安排好,待当前层或当前行处理完毕就可以处理下一层或下一行。使用队列是为了保存下一步的处理顺序。下面用二叉树层次遍历的例子说明队列的应用。
该过程简单描述如下:
① 根结点入队
② 若队空(所有结点处理完毕),则结束遍历;否则重复③操作
③ 队列中第一个结点出队,并访问之。若其有左孩子,则其左孩子入队;若其有右孩子,则将其右孩子入队,返回②。
队列在计算机系统中的应用
第一个方面是解决主机和外部设备的速度不匹配的问题(如主机和打印机之间的缓冲区)
第二方面是解决多用户引起CPU资源竞争的问题(先到先得CPU的使用权)
矩阵的压缩存储
压缩存储:指多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省存储空间。
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。常见的特殊矩阵有对称矩阵
,上(下)三角矩阵
、对角矩阵
。
对称矩阵的压缩存储
对于n阶对称矩阵,上三角区所有元素和下三角区对应元素相同,如果还采用二维数组A存储,就要浪费掉几乎一半的空间,为此将对称矩阵存放在一维数组B里,这时只需要存储主对角线和上(下)三角的元素即可。
在数组B中,位于元素a~i,j~(i>=j)前面的元素个数为
第1行:1个元素(a~1,1~);
第2行:2个元素(a~2,1~,a~2,2~);
……
第i-1行:i-1个元素(a~i-1,1~,,a~i-1,2~,…,a~i-1,i-1~);
第i行:j-1个元素(a~i,1~,a~i,2~,…,a~i,j-1~)。
故元素a~i,j~在数组B中的下标k=1+2+3+…+(i-1)+(j-1)= i(i-1)/2+j-1(数组下标从0开始)
因此元素下标之间的对应关系为:
三角矩阵的压缩存储
下三角矩阵
在下三角矩阵中,上三角的所有元素均为同一常量。因为
元素下标之间的对应关系为:
上三角矩阵
在上三角矩阵中,下三角区的所有元素均为同一常量。只需要存储主对角线、上三角区的元素和下三角区的常量一次。
元素下标之间的对应关系为:
对角矩阵
对角矩阵又称带状矩阵。对于n阶方阵A中的任一元素a~i,j~,当|i-j|>1时,有a~i,j~=0,则称为三对阵矩阵,如图
在三对称矩阵中,所有非零元素都集中存储在以主对角线为中心的3条对角线的区域中,其他区域都为0。所以只需要存储这些非零元素即可。
元素下标之间的对应关系为:
稀疏矩阵
稀疏矩阵是指非零元素和矩阵容量相比很小(t<<m*n),且分布没有规律,使用三元组(行标,列标,值)的顺序存储结构,存取下标为i和j的元素时,要扫描三元组表,下标不同的元素,存取时间也不同,最好情况下存取时间为O(1),最差情况下是O(n),因此失去了随机存取的功能。
小知识
n个不同元素进栈,出栈序列个数为
题目
P75 例
P78 1、2、3
P87 1、2、3、4