Upload
brody-wyatt
View
86
Download
4
Embed Size (px)
DESCRIPTION
第三章 栈和队列. 通常称,栈和队列是限定 插入和删除只能 在表的“ 端点 ”进行的线性表。. 线性表 栈 队列 Insert(L, i , x) Insert(S, n+1 , x) Insert(Q, n+1 , x) 1≤i≤n+1 Delete(L, i ) Delete(S, n ) Delete(Q, 1 ) 1≤i≤n. 栈和队列是两种常用的数据类型. 3.1 栈的类型定义. 3.2 栈的应用举例. 3.3 栈类型的实现. - PowerPoint PPT Presentation
Citation preview
通常称,栈和队列是限定插入和删除只能在表的“端点”进行的线性表。
线性表 栈 队列Insert(L, i, x) Insert(S, n+1, x) Insert(Q, n+1, x) 1≤i≤n+1 Delete(L, i) Delete(S, n) Delete(Q, 1) 1≤i≤n
栈和队列是两种常用的数据类型
3.1 栈的类型定义
3.2 栈的应用举例
3.3 栈类型的实现
3.4 队列的类型定义
3.5 队列类型的实现
3.1 栈的类型定义栈 ( Stack ) 定义 : 是限定仅在表尾进行插入或删除操作的线性表。
允许插入和删除的一端称为栈顶 (top) ,另一端称为栈底 (bottom)
特点:后进先出 (LIFO)
a1
top
bottom
an
.
.
.
.
进栈 出栈
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)DestroyStack(&S)
ClearStack(&S)
StackEmpty(S)StackLength(S)
GetTop(S, &e)
Push(&S, e)Pop(&S, &e)
StackTravers(S, visit())
InitStack(&S)
操作结果:构造一个空栈 S 。
DestroyStack(&S)
初始条件:栈 S 已存在。 操作结果:栈 S 被销毁。
StackEmpty(S)
初始条件:栈 S 已存在。 操作结果:若栈 S 为空栈,则返回 TRUE ,否则 F
ALE 。
StackLength(S)
初始条件:栈 S 已存在。 操作结果:返回 S 的元
素个数,即栈的长度。
GetTop(S, &e)
初始条件:栈 S 已存在且非空。操作结果:用 e 返回 S
的栈顶元素。
a1 a2 an… …
ClearStack(&S)
初始条件:栈 S 已存在。 操作结果:将 S 清为空
栈。
Push(&S, e)
初始条件:栈 S 已存在。 操作结果:插入元素 e
为新的栈顶元素。
a1 a2 an e … …
Pop(&S, &e)
初始条件:栈 S 已存在且非空。
操作结果:删除 S 的栈顶元素,并用 e 返回其值。
a1 a2 anan-1 … …
3.2 栈类型的实现
顺序栈链 栈
栈的表示和实现 顺序栈:栈的顺序存储结构,利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,指针 top 指向栈顶元素在顺序栈中的下一个位置,base 为栈底指针,指向栈底的位置。
base
空栈 a 进栈 b 进栈
a abtop base
topbase
top
top top
abcde
e 进栈
abcde
f 进栈溢出
ab
d
e 出栈
c
base basebase
top
顺序栈的类型表示 :#define STACK_INIT_SIZE 100 ;#define STACKINCREMENT 10 ;
typedef char SElemType;
typedef struct { // 顺序栈定义 SElemType *base; // 栈底指针 SElemType *top; // 栈顶指针
int stacksize ; // 当前已分配的存储空间} SqStack ;
判栈空int StackEmpty (SqStack S) {
if( S.top == S.base ) return 1 // 判栈空 , 空则返回 1 else return 0; // 否则返回 0
} 判栈满int StackFull (SqStack S) {
if( S.top- S.base >= S. StackSize ) return 1 // 判栈满 , 满则返回 1else return 0; // 否则返回 0
}
顺序栈的基本运算 :
初始化Status InitStack ( SqStack &S) {// 置空栈S.base =( SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType)); if (!S.base) exit(OVERFLOW);S.top = S.base ; S.stacksize= STACK_INIT_SIZE ;return OK;}
入栈
Status Push (SqStack &S, SElemType e)
{ // 若栈不满,则将 e 插入栈顶 if (S.top - S.base >= S.stacksize) // 栈满 return OVERFLOW;
*S.top++ = e;
return OK;
}
取栈顶元素
Status GetTop (SqStack S, SElemType &e){// 若栈空返回 ERROR, 否则栈顶元素读到 e并返回 OK
if ( S.top==S.base ) return ERROR;e = *(S.top-1);
return OK;}
出栈Status Pop (SqStack &S, SElemType &e) {
// 若栈不空,则删除 S 的栈顶元素, // 用 e 返回其值,并返回 OK ; // 否则返回 ERROR
if (S.top == S.base) return ERROR;
e = *--S.top;
return OK;
}
链式栈 : 栈的链接表示 链式栈无栈满问题,空间可扩充 插入与删除仅在栈顶处执行 链式栈的栈顶在链头 适合于多栈操作
top
链栈
top
链栈
∧a1an
注意 : 链栈中指针的方向注意 : 链栈中指针的方向
an-1
栈底元素
3.3 栈的应用举例例一、 数制转换例二、 括号匹配的检验例三、 行编辑程序问题例四、 迷宫求解例五、 表达式求值例六、 实现递归
例一、 数制转换
算法基于原理: N = (N div d)×d + N mod
d
例如:( 1348)10 = (2504)8 ,其运算过程如下:
N N div 8 N mod 8
1348 168 4 168 21 0 21 2 5 2 0 2
计算顺
序 输出顺
序
void conversion () { InitStack(S); scanf ("%d",&N); while (N) { Push(S, N % 8); N = N/8; } while (!StackEmpty(S)) { Pop(S,e); printf ( "%d", e ); }} // conversion
例二、 括号匹配的检验假设在表达式中 ([]())或[([ ][ ])]等为正确的格式, [( ])或([( ))或 (( )
])
均为不正确的格式。则 检验括号是否匹配的方法可用“期待的急迫程度”这个概念来描述。
分析可能出现的不匹配的情况 :• 到来的右括弧非是所“期待”的 ;
例如:考虑下列括号序列: [ ( [ ] [ ] ) ] 1 2 3 4 5 6 7 8
• 到来的是“不速之客” ;
• 直到结束,也没有到来所“期待”的括弧 ;
算法的设计思想:1 )凡出现左括弧,则进栈;2 )凡出现右括弧,首先检查栈是否空 若栈空,则表明该“右括弧”多余 否则和栈顶元素比较, 若相匹配,则“左括弧出栈” 否则表明不匹配3 )表达式检验结束时, 若栈空,则表明表达式中匹配正确 否则表明“左括弧”有余
Status matching(string& exp) { int state = 1; while (i<=Length(exp) && state) { switch of exp[i] { case 左括弧 :{Push(S,exp[i]); i++; break;} case’)’: { if(NOT StackEmpty(S)&&GetTop(S)=“(“ {Pop(S,e); i++;} else {state = 0;} break; } … … } if (StackEmpty(S)&&state) return OK; …...
例三、行编辑程序问题如何实现?
“每接受一个字符即存入存储器” ?
并不恰当!
设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区 ; 并假设“ #” 为退格符,“ @” 为退行符。
在用户输入一行的过程中,允许 用户输入出差错,并在发现有误时 可以及时更正。
合理的作法是:
假设从终端接受了这样两行字符: whli##ilr#e ( s#*s)
outcha@putchar(*s=#++);
则实际有效的是下列两行: while (*s)
putchar(*s++);
while (ch != EOF && ch != '\n') { switch (ch) { case '#' : Pop(S, c); break; case '@': ClearStack(S); break;// 重置 S 为空栈 default : Push(S, ch); break; } ch = getchar(); // 从终端接收下一个字符 }
ClearStack(S); // 重置 S 为空栈if (ch != EOF) ch = getchar();
while (ch != EOF) { //EOF 为全文结束符
将从栈底到栈顶的字符传送至调用过程的数据区;
例四、 迷宫求解通常用的是“穷举求解”的方法
# # # # # # # # # ## # $ $ $ # ## # $ $ $ # ## $ $ # # ## # # # # ## # # ## # # ## # # # # # # ## ## # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
1 1 1
1 2 22 2 23 2 13 3 13 4 42 4 12 5 12 6 4
1 6 31 5 31 4 4
3
$ $ $$$$
$$
0 1 2 3 4 5 6 7 8 9
0
1
2
3
4
5
6
7
8
9
求迷宫路径算法的基本思想是:• 若当前位置“可通”,则纳入路径,继续前进 ;
• 若当前位置“不可通”,则后退,换方向继续探索 ;
• 若四周“均无通路”,则将当前位置从路径中删除出去。
设定当前位置的初值为入口位置; do { 若当前位置可通, 则{将当前位置插入栈顶; 若该位置是出口位置,则算法结束; 否则切换当前位置的东邻方块为 新的当前位置; } 否则 { }} while ( 栈不空);
求迷宫中一条从入口到出口的路径的算法:
… …
若栈不空且栈顶位置尚有其他方向未被探索,则设定新的当前位置为 : 沿顺时针方向旋转 找到的栈顶位置的下一相邻块;
若栈不空但栈顶位置的四周均不可通,则{删去栈顶位置; // 从路径中删去该通道块 若栈不空,则重新测试新的栈顶位置, 直至找到一个可通的相邻块或出栈至栈空;}若栈空,则表明迷宫没有通路。
例五、实现递归
• 将所有的实在参数、返回地址等信息传递给被调用函数保存 ;
• 为被调用函数的局部变量分配存储区 ;
• 将控制转移到被调用函数的入口。
当在一个函数的运行期间调用另一个函数时,在运行该被调用函数之前,
需先完成三项任务:
• 保存被调函数的计算结果 ;
• 释放被调函数的数据区 ;
• 依照被调函数保存的返回地址将控制转移到调用函数。
从被调用函数返回调用函数之前,应该完成下列三项任务:
多个函数嵌套调用的规则是 :
此时的内存管理实行“栈式管理”
后调用先返回 !
例如:void main( ){ void a( ){ void b( ){
… … …
a( ); b( );
… …
}//main }// a }// bMain 的数据区
函数 a 的数据区
函数 b 的数据区
递归工作栈:递归过程执行过程中占用的 数据区。递归工作记录:每一层的递归参数合成 一个记录。当前活动记录:栈顶记录指示当前层的 执行情况。当前环境指针:递归工作栈的栈顶指针。
递归函数执行的过程可视为同一函数进行嵌套调用,例如:
void hanoi (int n, char x, char y, char z) {// 将塔座 x上按直径由小到大且至上而下编号为 1 至 n// 的 n 个圆盘按规则搬到塔座 z上, y 可用作辅助塔座。1 if (n==1)2 move(x, 1, z); // 将编号为1的圆盘从 x 移到 z3 else {4 hanoi(n-1, x, z, y); // 将 x上编号为1至 n-1 的 //圆盘移到 y, z 作辅助塔5 move(x, n, z); // 将编号为 n 的圆盘从 x 移到 z6 hanoi(n-1, y, x, z); // 将 y上编号为1至 n-1 的 //圆盘移到 z, x 作辅助塔7 }8 }
8 3 a b c返址 n x y z
5 2 a c b
5 1 a b c
void hanoi (int n, char x, char y, char z) {
1 if (n==1)2 move(x, 1, z); 3 else {4 hanoi(n-1, x, z, y); 5 move(x, n, z); 6 hanoi(n-1, y, x, z); 7 }8 }
7 1 c a b
3.4 队列的类型定义 队列队列 (Queue)(Queue) 也是一种运算受限的线性表。它只允许在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头队头 (front)(front) ,允许插入的一端称为队尾队尾 (r(rearear)) 。 例如:排队购物。先进入队列的成员总是先离开队列。因此队列亦称作先进先出 (First In First Out) 的线性表,简称 FIFO表。
队列的类型定义 ADT Queue { 数据对象: D = {ai | ai ElemSet, i=1,2,...,n, n≥0}∈ 数据关系: R1 = { <a i-1,ai > | ai-1, ai D, i=2,...,n}∈ 约定其中 a1 端为队列头, an 端为队列尾
基本操作:} ADT Queue
队列的基本操作:
InitQueue(&Q) DestroyQueue(&Q)
QueueEmpty(Q) QueueLength(Q)
GetHead(Q, &e) ClearQueue(&Q)
DeQueue(&Q, &e)EnQueue(&Q, e)
QueueTravers(Q, visit())
InitQueue(&Q)
操作结果:构造一个空队列Q 。
DestroyQueue(&Q)
初始条件:队列 Q 已存在。 操作结果:队列 Q 被销毁,
不再存在。
QueueEmpty(Q)
初始条件:队列 Q 已存在。 操作结果:若 Q 为空队列,则返回 TRUE ,否则返回 FAL
SE 。
QueueLength(Q)
初始条件:队列 Q 已存在。 操作结果:返回 Q 的元素
个数,即队列的长度。
GetHead(Q, &e)
初始条件: Q 为非空队列。 操作结果:用 e 返回 Q 的
队头元素。
a1 a2 an… …
ClearQueue(&Q)
初始条件:队列 Q 已存在。
操作结果:将 Q 清为空队
列。
EnQueue(&Q, e)
初始条件:队列 Q 已存在。 操作结果:插入元素 e 为
Q 的新的队尾元素。
a1 a2 an e … …
DeQueue(&Q, &e)
初始条件: Q 为非空队列。 操作结果:删除 Q 的队头
元素,并用 e 返回其值。
a1 a2 an… …
3.5 队列类型的实现
链队列——链式映象
循环队列——顺序映象
链队列——队列的链式表示
队列的链式存储结构简称为链队列,它是限制仅在表头删除和表尾插入的单链表。显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。于是,一个链队列由头指针和尾指针唯一确定。
队列的链式存储结构
typedef struct QNode {// 结点类型 QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
typedef struct { // 链队列类型 QueuePtr front; // 队头指针 QueuePtr rear; // 队尾指针} LinkQueue;
a1∧an
…Q.front
Q.frontQ.rear
∧空队列
Q.rear
链队列:队列的链式表示 链队列中,有两个分别指示队头和队尾的指针。
链式队列在进队时无队满问题,但有队空问题。
data next
front
reardata next
front
rear
frontrear
x ^ 元素 x 入队
frontrear
x y ^ 元素 y 入队
frontrear
x ^y 元素 x 出队
frontrear ^
空队列^ frontrear
NULL 空队列
Status InitQueue (LinkQueue &Q) {
// 构造一个空队列 Q
Q.front = Q.rear = new QNode;
if (!Q.front) exit (OVERFLOW); // 存储分配失败 Q.front->next = NULL;
return OK;
}
Status EnQueue (LinkQueue &Q, QElemType e) {
// 插入元素 e 为 Q 的新的队尾元素 p = new QNode;
if (!p) exit (OVERFLOW); // 存储分配失败 p->data = e; p->next = NULL;
Q.rear->next = p; Q.rear = p;
return OK;
}
a1∧an
Q.front
Q.rear
∧e
p
Status DeQueue (LinkQueue &Q,
QElemType &e) { // 若队列不空,则删除 Q 的队头元素, // 用 e 返回其值,并返回 OK ;否则返回 ERR
OR
if (Q.front == Q.rear) return ERROR;
p = Q.front->next; e = p->data;
Q.front->next = p->next;
delete (p); return OK;
}
if (Q.rear == p) Q.rear = Q.front;
循环队列——顺序映象#define MAXQSIZE 100 // 最大队列长度typedef struct {
QElemType *base; // 动态分配存储空间 int front; // 头指针,若队列不空, // 指向队列头元素 int rear; // 尾指针,若队列不空,指向 // 队列尾元素 的下一个位置 int queuesize;
} SqQueue;
顺序队列—队列的顺序存储表示。用一组地址连续的存储单元依次存放从队列头到队列尾的元素,指针 front 和 rear 分别指示队头元素和队尾元素的位置。
插入新的队尾元素,尾指针增 1 , rear = rear + 1 , 删除队头元素,头指针增 1 , front = front + 1 , 因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置。
队满时再进队将溢出 解决办法:将顺序队列臆造为一个环状的空间,形成循环 ( 环形 ) 队列
循环队列——顺序映象
队列的进队和出队
front rear 空队列 front rearA,B,C, D 进队
A B C D
front rearA,B 出队
C D
front rearE,F,G 进队
C D E F G
C D E F G
front rear
H 进队 ,溢出
队空: Q.front=Q.rear
队满: Q.rear-Q.front=maxsize(求队长)
入队:新元素按 rear 指示位置加入,再将队尾指针加一 ,即 rear = rear + 1
出队:将 front 指示的元素取出,再将队头指针加一,即 front = front + 1 ,。
非循环队列
和栈类似,队列中亦有上溢和下溢和栈类似,队列中亦有上溢和下溢现象。此外,顺序队列中还存在现象。此外,顺序队列中还存在“假上“假上溢”溢”现象。因为在入队和出队的操作中,现象。因为在入队和出队的操作中,头尾指针只增加不减小,致使被删除元头尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。因此,尽素的空间永远无法重新利用。因此,尽管队列中实际的元素个数远远小于向量管队列中实际的元素个数远远小于向量空间的规模,但也可能由于尾指针巳超空间的规模,但也可能由于尾指针巳超出向量空间的上界而不能做入队操作。出向量空间的上界而不能做入队操作。该现象称为假上溢。该现象称为假上溢。
为充分利用向量空间,克服上述假为充分利用向量空间,克服上述假上溢现象,可以将向量空间想象为一个首尾上溢现象,可以将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量,存相接的圆环,并称这种向量为循环向量,存储在其中的队列称为储在其中的队列称为循环队列(循环队列( Circular QuCircular Queue)eue) 。在循环队列中进行出队、入队操作时,。在循环队列中进行出队、入队操作时,头尾指针仍要加头尾指针仍要加 11 ,朝前移动。只不过当头,朝前移动。只不过当头尾指针指向向量上界(尾指针指向向量上界( QueueSize-1QueueSize-1 )时,)时,其加其加 11 操作的结果是指向向量的下界操作的结果是指向向量的下界 00 。。
显然,因为循环队列元显然,因为循环队列元素的空间可以被利用,除非向量空素的空间可以被利用,除非向量空间真的被队列元素全部占用,否则间真的被队列元素全部占用,否则不会上溢。因此,除一些简单的应不会上溢。因此,除一些简单的应用外,真正实用的顺序队列是循环用外,真正实用的顺序队列是循环队列。队列。
打开 P64 如图 3.14 所示:由于入队时尾指针向前追赶头指针,出队时头指针向前追赶尾指针,故队空和队满时头尾指针均相等。因此,我们无法通过 Q.front=Q.rear来判断队列“空”还是“满”。 解决此问题的方法至少有两种: 其一是另设一个布尔变量以区别队列的空和满;其二是少用一个元素的空间,约定入队前,测试尾指针在循环意义下加 1后是否等于头指针,若相等则认为队满(注意:rear 所指的单元始终为空)
循环队列 (Circular Queue) 队头、队尾指针加 1 ,可用取模 ( 余数 ) 运算实现。 队头指针进 1: front = (front+1) %maxsize; 队尾指针进 1: rear = (rear+1) % maxsize; 队列初始化: front = rear = 0; 队空条件: front == rear;
队满条件: (rear+1) % maxsize == front;
0
1
23
4
5
6 7
循环队列front
rear
Maxsize-1
0
1
23
4
5
6 7front
BCD
rear
一般情况
A
C0
1
23
4
5
6 7
队满
frontrear
DE
FG
A
BC
H
0
1
23
4
5
6 7 rear
空队列
front
C0
1
23
4
5
6 7
队满 ( 正确 )
front
rear
DE
FG
A
BC
解决方案:1. 另外设一个标志以区别队空、队满2. 少用一个元素空间: 队空: front==rear 队满: (rear+1)%queueSize==front
Status InitQueue (SqQueue &Q,
int maxsize) {
// 构造一个最大存储空间为 maxsize
的 // 空循环队列 Q
Q.base = new ElemType[maxsize];
if (!Q.base) exit (OVERFLOW);
Q.queuesize = maxsize;
Q.front = Q.rear = 0;
return OK;
}
Status EnQueue (SqQueue &Q, ElemType e)
{ // 插入元素 e 为 Q 的新的队尾元素 if ((Q.rear+1) % Q.queuesize == Q.front)
return ERROR; // 队列满 Q.base[Q.rear] = e;
Q.rear = (Q.rear+1) % Q.queuesize;
return OK;
}
Status DeQueue (SqQueue &Q, ElemType &e)
{ // 若队列不空,则删除 Q 的队头元素, // 用 e 返回其值,并返回 OK; 否则返回 ERROR
if (Q.front == Q.rear) return ERROR;
e = Q.base[Q.front];
Q.front = (Q.front+1) % Q.queuesize;
return OK;
}
1. 掌握栈和队列类型的特点,并能在相应的应用问题中正确选用它们。 2. 熟练掌握栈类型的两种实现方法,特别应注意栈满和栈空的条件以及它们的描述方法。 3. 熟练掌握循环队列和链队列的基本操作实现算法,特别注意队满和队空的描述方法。 4. 理解递归算法执行过程中栈的状态变化过程。
作业 21. 选择题一个栈的入栈序列是 a,b,c,d,e ,则栈的不可能的输出序列是 ( ) ①edcba decba dceab abcde② ③ ④2. 填空题(1) 已知指向一个顺序栈 S 的栈顶指针为 top ,栈底指针为 base ,
m0 指示栈的可使用的最大容量,则判定栈 S 为空的条件为 ,栈 S 为栈满的条件为 。
(2) 已知指针 front 和 rear 分别指向一个顺序队列 Q 的队头和对尾, m0 指示队列的可使用的最大容量,则判定队列 Q 为空的条件为 ,队列 Q 为满的条件为 。如果该队列 Q 为循环队列,则判定队列 Q 为空的条件为 ,队列 Q 为满的条件为 。
3. 假设表达式中允许包含两种括号:圆括号和方括号,其嵌套循序任意,即 ( [ ]( ) ) 等为正确格式,而 [ ( ] ) 等为不正确格式。试编写算法 Status matching(string &exp, int tag) 检验括号是否匹配,其中 exp 为字符型输入常量,表示被判别的表达式,tag=1 表示符号匹配, tag=0 表示不匹配。