660 likes | 771 Views
第三章 栈和队列. 栈 栈的应用 队列 队列的应用. 3.1 栈 3.1.1 栈的定义及基本运算 栈 (Stack) 是限制在表的一端进行插入和删除运算的线性表,通常称插入、删除的这一端为栈顶 (Top) ,另一端为栈底 (Bottom) 。当表中没有元素时称为空栈。
E N D
第三章 栈和队列 • 栈 • 栈的应用 • 队列 • 队列的应用
3.1栈 3.1.1 栈的定义及基本运算 栈(Stack)是限制在表的一端进行插入和删除运算的线性表,通常称插入、删除的这一端为栈顶(Top),另一端为栈底(Bottom)。当表中没有元素时称为空栈。 假设栈S=(a1,a2,a3,…an),则a1称为栈底元素,an为栈顶元素。栈中元素按a1,a2,a3,…an的次序进栈,退栈的第一个元素应为栈顶元素。换句话说,栈的修改是按后进先出的原则进行的。因此,栈称为后进先出表(LIFO)。
a n 栈顶 a n-1 …… a2 栈底 a1 例:一叠书或一叠盘子。 栈的抽象数据类型的定义如下:P44
3.1.2 顺序栈 由于栈是运算受限的线性表,因此线性表的存储结构对栈也适应。 栈的顺序存储结构简称为顺序栈,它是运算受限的线性表。因此,可用数组来实现顺序栈。因为栈底位置是固定不变的,所以可以将栈底位置设置在数组的两端的任何一个端点;栈顶位置是随着进栈和退栈操作而变化的,故需用一个整型变量top
来指示当前栈顶的位置,通常称top为栈顶指针。因此,顺序栈的类型定义只需将顺序表的类型定义中的长度属性改为top即可。顺序栈的类型定义如下:来指示当前栈顶的位置,通常称top为栈顶指针。因此,顺序栈的类型定义只需将顺序表的类型定义中的长度属性改为top即可。顺序栈的类型定义如下: # define StackSize 100 typedef char datatype; typedef struct { datatype data[stacksize]; int top; }seqstack;
6 5 4 3 2 1 0 -1 top
设S是SeqStack类型的指针变量。若栈底位置在向量的低端,即s–>data[0]是栈底元素,那么栈顶指针s–>top是正向增加的,即进栈时需将s–>top加1,退栈时需将s–>top 减1。因此,s–>top<0表示空栈,s–>top =stacksize-1表示栈满。当栈满时再做进栈运算必定产生空间溢出,简称“上溢”;当栈空时再做退栈运算也将产生溢出,简称“下溢”。上溢是一种出错状态,应该设法避免之;下溢则可能是正常现象,因为栈在程序中使用时,其初态或终态都是空栈,所以下溢常常用来作为程序控制转移的条件。
1、置空栈 void initstack(seqstack *s) { s–>top=-1; } 2、判断栈空 int stackempty(seqstack *s) { return(s–>top==-1); }
3、判断栈满 int stackfull(seqstack *s) { return(s–>top==stacksize-1); } 4、进栈 void push(seqstack *s,datatype x) { if (stackfull(s)) error(“stack overflow”); s–>data[++s–>top]=x; }
5、退栈 datatype pop(seqstack *s) { if(stackempty(s)) error(“stack underflow”); x=s–>data[top]; s–>top--; return(x); //return(s–>data[s–>top--]); }
6、取栈顶元素 Datatype stacktop(seqstack *s) { if(stackempty(s) error(“stack is empty”); return (s–>data[s–>top]); }
3.1.3 链栈 栈的链式存储结构称为链栈,它的运算是受限的单链表,插入和删除操作仅限制在表头位置上进行。由于只能在链表头部进行操作,故链表没有必要像单链表那样附加头结点。栈顶指针就是链表的头指针。 链栈的类型说明如下:
栈的链接存储结构: typedef int datatype; typedef struct node { datatype data; struct node *next; } linkstack;
栈的链接表示—链栈: • 链式栈无栈满问题,空间可扩充 • 插入与删除仅在栈顶处执行 • 链式栈的栈顶在链头 • 适合于多栈操作
maxsize-1 0 lefttop righttop 两个堆栈共享空间:
链栈的进栈算法: • linkstack *PUSHLSTACK(linkstack*top, datatype x) • { • linkstack *p; • p=(linkstack *)malloc(sizeof(linkstack)); • p->data=x; • p->next=top; • return p; • }
链栈的出栈算法: linkstack *POPLSTACK(linkstack *top, datatype datap) { linkstack *p; if (top==NULL) {printf(“under flow\n”); return NULL;} else { *datap=top->data; p=top; top=top->next; free(p); return top; } }
3.2 栈的应用举例 由于栈结构具有的后进先出的固有特性,致使栈成为程序设计中常用的工具。以下是几个栈应用的例子。 栈的应用举例-----数制转换 十进制N和其它进制数的转换是计算机实现计算的基本问题,其解决方法很多,其中一个简单算法基于下列原理: N=(n div d)*d+n mod d ( 其中:div为整除运算,mod为求余运算) 例如 (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); } }
栈的应用举例--文字编辑器 seqstack s; EDIT( ) { char c; initstack(&s); c=getchar(); while (c!=‘*’) { if (c==‘#’) POP(&s); else if (c==‘@’) initstack(&s); else PUSH(&s,c); c=getchar(); } } 输入缓冲区为一堆栈 #为退格符,@为退行符 P49 输入#,执行退栈操作 输入@,执行清栈空 输入其他,进栈
栈的应用举例--表达式计算 中缀表达式:A + ( B – C / D) × E 后缀表达式:ABCD/-E×+ 后缀表达式特点 1、与相应的中缀表达式中的操作数次序相同 2、没有括号
中缀表达式转换为后缀表达式 当前运算符 栈顶运算符
中缀表达式转换为后缀表达式的处理规则: 1、如为操作数,直接输出到队列; 2、如当前运算符高于栈顶运算符,入栈; 3、如当前运算符低于栈顶运算符,栈顶运算符退栈, 并输出到队列,当前运算符再与栈顶运算符比较; 4、如当前运算符等于栈顶运算符,且栈顶运算符为 “(”,当前运算符为“)”,则栈顶运算符退栈, 继续读下一符号; 5、如当前运算符等于栈顶运算符,且栈顶运算符为 “#”,当前运算符也为“#”,则栈顶运算符退栈, 继续读下一符号;
算法步骤: 设置一堆栈,将栈顶预置为#。顺序读入中缀表达式,当读到的单词为操作数就将其输出,并等着读下一个单词; 当读到的单词为运算符就赋予x2,比较 x2与栈顶运算符 xl的优先级,若 x2的优先级高于xl的,将x2进栈,继续读下一个单词; 若x2的优先级低于 xl的,xl退栈并作为后缀表达式的一个单词输出,然后继续比较x2与新的栈顶运算符 x1; 若 x2的优先级等于xl的,且 xl为“(”, X2为“)”,则xl退栈,然后继续读下一个单词;若 x2的优先级等于xl的,且xl为#,x2也为#,则算法结束
算法:中缀表达式变换为后缀表达式 int Postfix(qstype *s,char *a) { char x1,x2; char x; int j=0; s->Stack[0]='#', s->Top=0; x2=a[j]; if ((x1=GetTopQStack(s))==NIL) exit(0); while(1){ if (x2!='+'&&x2!='-'&&x2!='*'&& x2!='/'&&x2!='/'&&x2!='(' &&x2!=')' &&x2!='#')
{ printf("%c",x2); //操作数输出 • j++; • x2=a[j]; //取下一个单词 • } //此种情况下 x2读到的是操作数 • else if(Proceed(x1,x2)=='<') • { if(!PushQStack(s,x2)) exit(0); //读到的操作符入栈 • if((x1=GetTopQStack(s))==NIL) exit(0); • //更新xl的值为新入栈的操作符 • j++; • x2=a[j]; //取下一个单词 • } //栈顶操作符X1小于读到的操作符X2 • else if(Proceed(x1,x2)=='>')
{ if((x=PopQStack(s))==NIL) exit(0); //退栈 • printf("%c",x); //输出原栈顶操作符 • if ((x1=GetTopQStack(s))==NIL) exit(0); //更新xl的值 • } //栈顶操作符大于读到的操作符 • else if(Proceed(x1,x2)=='='&&x1=='('&&x2==')') • { if((PopQStack(s))==NIL) exit(0); • if ((x1= GetTopQStack(s)) == NIL) exit(0); • j++; • x2=a[j]; • } //去掉后缀表达式中的一对括号 • else if(Proceed(x1,x2)=='='&&x1=='#'&&x2=='#') return 1 ; • } • }
中缀表达式转换成后缀表达式的过程: A+(B-C/D)*E
D C T1 B B T2 A A A top top top
E T3 T1 A A T4 top top top
3.4 队列 3.4.1 抽象数据类型队列的定义 队列(Queue)是一种运算受限的线性表。 表的一端进行插入,而在另一端进行删除 队头(front) 允许删除的一端 队尾(rear) 允许插入的一端 先进先出(First In First Out)的线性表,简称FIFO表 空队列 当队列中没有元素
出队 入队 a1a2…an 队头 队尾 A B C D
ADT Queue { 数据对象:D={ai | ai∈ElemSet, i=1,2,...,n, n≥0} 数据关系:R1={ <ai-1,ai > | ai-1, ai ∈D, i=2,...,n} 约定其中a1端为队列头,an-1端为队列尾 基本操作: Initiate(q) 初始化 Enter (q,x) 入队列 Delete(q) 出队列 Gethead(q) 取队头元素 Empty(q) 判队空否 } ADT Queue
3.4.2 循环队列-队列的顺序表示和实现 队列的顺序存储结构简称顺序队列 #define MAXNUM //队列允许存放的最大元素数 typedef struct {elemtype queue[MAXNUM]; int front; int rear; }qtype;
D D D C C B B A A Rear Rear Rear Rear Rear Rear Rear Front Front Front Front Front Front Front E D 若删除D: 若插入E: 队尾、队头、队尾指示器、队头指示器 空队列、进队列、出队列
(l) 初始化 void initiateq(qtype *q){ //用指针类型传引用调用 q->front=-1; q->rear=-1; }
(2) 进队列 int enterq( qtype *q, elemtype x) { if(q->rear==MAXNUM-l) return(false); //队列已满 else { q->rear++; q->queue[q->rear]=x; return(true); } }
(3)出队列 elemtype deleteq(qtype *q) { if(q->front==q->rear)//队列已空 return(ERR); //ERR为elemtype类型常量 else{ q->front++; return(q->queue[q->front]); } }
队列中亦有上溢和下溢现象。 此外,顺序队列中还存在“假上溢”现象。 因为在入队和出队的操作中,头尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。 因此,尽管队列中实际的元素个数远远小于向量空间的规模,但也可能由于尾指针巳超出向量空间的上界而不能做入队操作。该现象称为假上溢。
q->rear=-1 q->front=-1 q->rear=2 q->front=-1 q->rear=2 q->front=1 q->rear=5 q->front=1
an an q->Rear q->Rear an-1 an-1 an q->Rear . . . . . . . . . a1 a1 a2 a0 a1 q->Front q->Front q->Front 解决顺序队列假溢出问题的四种方法: ①按最大可能的进队列操作次数设置顺序队列的最大元素个数 ②修改出队列算法 时间复杂度由O(1)增大为O(n)
③修改进队列算法 即把向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量,存储在其中的队列称为循环队列(Circular Queue)。在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向向量上界(MAXNUM-1)时,其加1操作的结果是指向向量的下界0。 这种循环意义下的加1操作可以描述为: if (q->rear+1== MAXNUM) q->rear =0; else q->rear++; 利用模运算可简化为: q->rear =(q->rear +1)%MAXNUM
q-> front q-> rear 5 4 0 3 1 2 q->rear J5 J6 J4 5 4 0 J4,J5,J6出队 3 1 2 q-> front J7,J8,J9入队 J5 初始状态 J6 J4 5 4 0 3 1 J7 J9 2 J8 q-> front q-> rear 队空:q->front== q-> rear 队满:q->front== q-> rear 如何解决?
解决方案: (1)少用一个数据元素空间,以队尾指示器加 l等于队头指示器判断队满,即队满条件为: (q-> rear+l)%MAXNUM=q->front 队空条件仍为q->rear=q->front。 队空:q->front =q-> rear 队满:q->front =(q->rear + 1) % MAXNUM 入队: q->rear = (q->rear + 1) % MAXNUM 出队: q->front = (q->front + 1) % MAXNUM 求队长:(q->rear - q->front+MAXNUM)%MAXNUM
(2)另设一个标志位以区别队列是满还是空。 (3)使用一个计数器记录队列中元素的总数(实际上是队列长度)。 我们用第三种方法实现循环队列上的六种基本操作,为此先给出循环队列的类型定义。
#define MAXNUM 100 typedef char elemtype; typedef struct{ int front; int rear; int count; elemtype data[MAXNUM]; }cirqueue; 循环队列的进队列和出队列算法如下: