670 likes | 920 Views
第 3 章 栈和队列. 复习总结:. 线性表的顺序存储与链式存储的特点对比 链式存储的三种类型: 单链表,双向链表,循环链表. ( 1 )顺序表 优点 : 随机存取 ; 存储空间利用率高 缺点 : 插入、删除需移动元素 ( 2 )链表: 优点 : 插入、删除不需移动元素 缺点 : 不能随机存取 ; 存储空间利用率不高. s=(LNode*)malloc(sizeof(LNode)); s->data=a[i];. q=p→next; p→next=q→next;
E N D
复习总结: • 线性表的顺序存储与链式存储的特点对比 • 链式存储的三种类型:单链表,双向链表,循环链表 (1)顺序表 优点: 随机存取; 存储空间利用率高 缺点: 插入、删除需移动元素 (2)链表: 优点: 插入、删除不需移动元素 缺点: 不能随机存取; 存储空间利用率不高 s=(LNode*)malloc(sizeof(LNode)); s->data=a[i]; q=p→next; p→next=q→next; e=qdata; free(q); r->next=s; r=r->next; s->next =c->next; c->next=s;
主要内容 • 栈和队列是操作受限的线性表。 • 重点: • 栈和队列的定义 • 表示方法、实现、 • 应用实例
3.1 栈 1、栈的基本概念 • 栈:限定仅在表尾进行插入和删除操作的线性表 • 空栈:不含任何数据元素的栈。 • 允许插入和删除的一端称为栈顶 • 另一端称为栈底。 • 顺序栈:需要一个记录栈顶元素所在数组位置标号的整形变量 • 链式栈:需要一个记录栈顶元素所在结点地址的指针
栈底 栈顶 栈顶 栈顶 栈顶 1、栈的基本概念 栈的示意图 入栈 出栈 插入:入栈、进栈、压栈 删除:出栈、弹栈 c b a
栈底 栈顶 栈顶 1、栈的基本概念 栈的示意图 入栈 出栈 插入:入栈、进栈、压栈 删除:出栈、弹栈 c b a 栈的操作特性:后进先出
如何改造数组实现栈的顺序存储? 2、栈的顺序存储结构及实现——顺序栈 1、确定用数组的哪一端表示栈底。 2、附设指针top指示栈顶元素在数组中的位置。
2、栈的顺序存储结构及实现——顺序栈 typedef struct { selemtype *base; //栈底指针 Selemtype *top; //栈顶指针 Int stacksize; //栈当前可用的最大容量 }seqstack; Int data[maxsize]; Int top; #define maxsize 100 Typedef struct { Int data[maxsize]; Int length; }sqlist #define LIST_INIT_SIZE 100 #define LISTINCREAMENT 10 type struct{ ElemType *elem;//存储空间基址 int length; int listsize; }SqList// 线性表顺序存储结构定义:指针
2、栈的顺序存储结构及实现——顺序栈 顺序栈的操作算法:(P38-39) 初始化 SeqStack( ) 取栈顶元素 Top(S ) • 二个状态: • 判断栈空: Empty( ):st.top=-1//st.top=st.base • 判断栈满:st.top==maxsize-1 • 两个操作: • 入栈 Push(St x): st.data[st.top]=x;++(st.top) • 出栈 Pop( St) : x= st.data[st.top]; --(st.top)
算法讲解 (1) 置栈空 SETNULL(seqstack *s) { s->top=-1; } (2) 判栈空 int EMPTY(seqstack s) {if (s->top>=0) return FALSE; else return TRUE; }
算法讲解 (3) 进栈 seqstack *PUSH(seqstack *s ,datatype x) {if (s->top==maxsize-1) {printf(“overflow”); return NULL;} else { s->data[++s->top]=x; return s; } } s->data[s->top]=x; ++s->top (4) 退栈 datatype POP(seqstack *s ) {if (EMPTY(s)) {printf(“overflow”); return NULL;} else return( s->data[s->top--]);} s->data[s->top]); s->top--;
算法讲解P58.1顺序栈 • 编写算法,判断一个表达式中的括号是否正确配对,表达式已存入字符数组exp[]中(元素从数组下标1开始存储),表达式中字符个数为n Int match(char exp[],int n) { char stack[maxsize]; int top=-1; Int i; For(i=1;i<=n;++i) { If(exp[i]==‘(‘) stack[++top]=‘(‘; If(exp[i]==‘)‘) {if(top==-1) return 0 else --top;} } If(top==-1) return 1; else return 0; }
算法实例:数制转换 • 将十进制数N转换成d进制 N=(N div d)*d + N mod d • 计算过程是从低位到高位顺序产生d进制数的各个数位。 • 算法思想: 将计算过程的d进制数逐位进栈,然后逐个出栈。 void conversion(seqstack *s) {SETNULL(s); scanf(“%d%d”,N,d); while(N) {PUSH(s,N%d); N=N/d; } while(!EMPTY(s)) {e=pop(s); printf(“%d”,e);}
考试中实用的写法: • 声明一个栈并初始化 Int stack[maxsize]; int top=-1 • 元素X进栈 stack[++top]=x • 元素X出栈: X=stack[top--];
1、栈是只准在一端进行插入和删除操作的线性表,允许插入和删除的一端叫栈顶,另一端叫栈底。最后插入的元素最先删除,故栈也称后进先出(LIFO)表。1、栈是只准在一端进行插入和删除操作的线性表,允许插入和删除的一端叫栈顶,另一端叫栈底。最后插入的元素最先删除,故栈也称后进先出(LIFO)表。 考研真题 1. 名词解释:栈。【燕山大学 1999 一、1(2分)】【吉林工业大学 1999 一、3(2分)】 5. 有5 个元素,其入栈次序为:A,B,C,D,E,在各种可能的出栈次序中,以元素C,D最先出栈(即C第一个且D第二个出栈)的次序有哪几个?【西南交通大学 2000 二、1】 6. 如果输入序列为1 2 3 4 5 6,试问能否通过栈结构得到以下两个序列:4 3 5 6 1 2和1 3 5 4 2 6;请说明为什么不能或如何才能得到。【武汉交通科技大学 1996 二、3 (3分)】 7. 若元素的进栈序列为:A、B、C、D、E,运用栈操作,能否得到出栈序列B、C、A、E、D和D、B、A、C、E?为什么?【北京科技大学 98 一、2】 8. 设输入序列为a,b,c,d,试写出借助一个栈可得到的两个输出序列和两个不能得到的输出序列。【北京科技大学 2001 一、4(2分)】 9. 设输入序列为2,3,4,5,6,利用一个栈能得到序列2,5,3,4,6吗?栈可以用单链表实现吗?【山东师范大学 1996 五、4(2分)】 5、三个:CDEBA,CDBEA,CDBAE 6、输入序列为123456,不能得出435612,其理由是,输出序列最后两元素是12,前面4个元素(4356)得到后,栈中元素剩12,且2在栈顶,不可能栈底元素1在栈顶元素2之前出栈。 得到135426的过程如下:1入栈并出栈,得到部分输出序列1;然后2和3入栈,3出栈,部分输出序列变为:13;接着4和5入栈,5,4和2依次出栈,部分输出序列变为13542;最后6入栈并退栈,得最终结果135426。
如何改造链表实现栈的链接存储? 3、栈的链式存储结构及实现——链栈 1、将哪一端作为栈顶? 2、链栈需要加头结点吗?
链栈 • 栈的单链形式 :“前插链表”就是链栈。由于栈的操作在栈顶,所以,链栈一般不设头结点。 typedef int datatype; typedef struct node { datatype data; struct node *next; }node,linkstack,*top; • 单链表中的每一个结点中只包含一个指针域的称为单链表或线性链表。可由头指针唯一确定,用结构指针来描述。 typedef struc LNode { ElemType data; struct LNode *next }LNode
链栈基本算法操作 • 栈空:lst->nnxt==Null • 栈满:不存在 • 入栈: • p->next=lst->next; • lst->next=p • 出栈: • p=lst->next; • x=p->data; • lst->next=p->next; • free(p);
栈的基本操作在链栈下的实现 • 压栈(入栈)核心语句有: p=(linkstack*) malloc(sizeof(node)); p->data=x; p->next=top; top=p; /*无头结点栈*/ • 退栈 pop(s) if (st==null) printf(“\n underflow”); else { p=s->next; s->next=p->next; free(p); } 取栈顶元素top(s) if (st==null) { printf(“\n underflow”); return NULL;} else return(x=s->data) 判栈空empty(s) if (s==null) return TRUE;; else return(true)
栈的应用 • 应用的思想: 在解决问题的过程中出现了一个状态,但凭现有条件不能判断当前的状态是否可以解决,需要记下,等待以后出现可以解决当前状态的条件后返回再解决。 栈具有记忆功能。
4、栈的应用 • 中缀表达式求值是程序设计语言编译中的一个最基本问题。它的实现需要借助栈来完成。这里介绍一种简单直观,广为使用的算法,即“算符优先法”。 • 为实现算符优先算法,可以使用两个工作栈。 • 1.运算符OPTR栈, 用以寄存运算符; • 2.操作数OPND栈, 用以寄存操作数或运算结果。
算法思想: • 1)首先置操作数栈为空栈,表达式起始符“@”为运算符的栈底元素; • 2)依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符,则与OPTR栈的栈顶运算符进行优先级的比较,若优先级高于栈顶操作符,则入栈;否则,栈顶运算符退栈作相应操作,直至整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符均为“@”)。
运算优先级 当前运算符 栈顶运算符
19、中缀表达式8-(3+5)*(5-6/2)的后缀表达式是: 8 3 5 + 5 6 2 / - * - 考研真题 • 19. 用栈实现将中缀表达式8-(3+5)*(5-6/2)转换成后缀表达式 • 20. 在表达式中,有的运算符要求从右到左计算,如A**B**C的计算次序应为(A**(B**C)),这在由中缀生成后缀的算法中是怎样实现的?(以**为例说明)【东南大学1993一、2(6分) 1997一、1(8分)】 中缀表达式exp1转为后缀表达式exp2的规则如下: 设操作符栈s,初始为空栈后,压入优先级最低的操作符‘#’。对中缀表达式从左向右扫描,遇操作数,直接写入exp2;若是操作符(记为w),分如下情况处理,直至表达式exp1扫描完毕。 (1)w为一般操作符(’+’,’-‘,’*’,’/’等),要与栈顶操作符比较优先级,若w优先级高于栈顶操作符,则入栈;否则,栈顶运算符退栈到exp2,w再与新栈顶操作符作上述比较处理,直至w入栈。 (2)w为左括号(’(’),w入栈。 (3)w为右括号(’)’),操作符栈退栈并进入exp2,直到碰到左括号为止,左括号退栈(不能进入exp2),右括号也丢掉,达到exp2中消除括号的目的。 (4)w为‘#’,表示中缀表达式exp1结束,操作符栈退栈到exp2,直至碰到‘#’,退栈,整个操作结束。 • 简单方法:中缀表达式转为后缀表达式有三步: • 将中缀表达式中所有的子表达式按计算规则用嵌套括号括起来; • 顺序将每对括号中的运算符移到相应括号的后面; • 删除所有括号。 • 如,将中缀表达式8-(3+5)*(5-6/2)转为后缀表达式。 • 执行完上面第一步后为:(8-((3+5)*(5-(6/2)))); • 执行完上面第二步后为:(8((35)+(5(62)/)-)*)- ; • 执行完上面第三步后为:8 3 5 + 5 6 2 / - * - 。
算法讲解:表达式求值:算符优先法 OperandType EvaluateExpression(){ //op为运算符集合 InitStack(optr); Push(optr,’#’); //optr是运算符栈 Initstack(opnd); c=getchar(); //opnd是运算数栈 While(c!=‘#’||gettop(optr)!=‘#’) {if (!In(c,op){push((opnd,c);c=getchar();} //不是运算符则进栈 Else switch(precede(gettop(optr),c)){ Case’<‘:push(optr,c);c=getchar();break;//栈顶元素优先权低 Case’=‘:pop(optr,x); c=getchar();break; Case’>’:pop(optr,theta); //theta变量用于暂存要计算的算符 Pop(opnd,b);pop(opnd,a); push(opnd,operate(a,theta,b));break; }//swich }//while Retrun gettop(opnd); }evaluateexpression
写出后缀表达式 • A*B*C • (A+B)*D+E/(F+A*D)+C • (A&&B)||(!(E>F))
算法练习二P67.1 为充分利用空间,顺序栈S0,S1共享一个存储区elem[0,maxsize-1]. 试设计共享栈S0和S1,及有关入栈和出栈的操作算法。栈中元素为int型。 • 给出基本设计思想 • 用C描述算法(对于共享栈要写出其结构定义),关键之处给出注释。 思想:顺序栈栈底固定不变,因此将栈底设在存储区的两端,S0栈底在0处,S1栈底在maxsixe-1处,栈顶在0-maxsize-1之间变动。当两栈相遇时为栈满 入栈和出栈的思想: S0栈顶为top0, 入栈时:top0先自增1,然后存入元素 出栈时:先取出栈顶元素,top0再自减1 S1栈顶为top1, 入栈时:top1先自减1,然后存入元素 出栈时:先取出栈顶元素,top1再自增1 算法:P68
小结 • 栈的含义及性质 • 栈的顺序存储: • 顺序栈定义 • 四个操作 • 栈的链式存储: • 链栈的定义 • 操作 • 栈的应用
考研英语三、并列关系 • P42. 2011年阅读3 P2S7 • Besides generating income, the presence of other marketers makes the site seem objective, gives companies opportunities to learn valuable information about the appeal of other companies’ marketing, and may help expand user traffic for all companies concerned. 其它营销者的投放除了可以带来收入,也能使该网站看起来客观公正,使企业有机会了解其他公司市场营销的独到之处,还能有助于扩大所有相关企业的用户流量。
复习: • 栈的含义及性质 • 栈的顺序存储: • 顺序栈定义 • 四个操作 • 栈的链式存储: • 链栈的定义 • 操作 • 栈的应用
新课: • 队列的含义及性质 • 队列的顺序存储:循环队列 • 队列的链式存储 • 假溢出
队头 队尾 3.2队列 1、队列的逻辑结构 • 队列:只允许在一端进行插入操作,而另一端进行删除操作的线性表。 入队 出队 a1 a2 a3 先进先出 队列的操作特性:
2、队列的顺序存储结构及实现(顺序队列) typedef struct{ int data[maxsize]; int front;//指向队列头元素 //指向刚出队的元素所占的位置 int rear; //指向队尾元素的下一个位置 //指向刚入队的元素的位置 }SqQueue;
如何改造数组实现队列的顺序存储? 2、队列的顺序存储结构及实现(顺序队列) 1、仿照顺序表,将n个元素存放在前n个连续单元中。 入队操作时间性能为O(1),出队操作时间性能为O(n) 2、放宽队列的所有元素必须存储在数组的前n个连续单元这一条件 ,只要求队列的元素存储在数组中连续的位置。——设置队头、队尾两个指示 入队操作时间性能为O(1),出队操作时间性能为O(1),但会出现假上溢 3、循环队列——将存储队列的数组头尾相接。
如何实现循环队列? 2、队列的顺序存储结构及实现(顺序队列) 1、不存在物理的循环结构 2、如何判断循环队列队空? front=rear 如何判断循环队列队满? front=rear
队空、队满的判定条件出现二义性。 如何将队空和队满的判定条件分开? 2、队列的顺序存储结构及实现(顺序队列) 方法一:浪费一个元素空间。将图3-8(c)所示的情况视为队满,此时的状态是,队尾指针加1就会从后面赶上队头指针,此时队满的条件是(rear+1) % MaxSize==front,这样就能与空队区别开。 方法二:使用一个计数器记录队列中元素的个数。附设一个存储队列中元素个数的变量如num,当num==0时队空,当num==MaxSize时队满。
循环队列顺序存储 • 二个状态: • 队空状态:qu.rear==qu.front • 队满状态: (qu.rear+1)%maxsize==qu.front • 二个操作: 元素X进队操作: Qu.rear=(qu.rear+1)%maxsize; qu.data[qu.rear]=x 元素X出队操作: qu.front=(qu.front+1)%mazsize; x=qu.data[qu.front];
Typedef struct{ int data[maxsize]; Int front,rear; }cycqueue; 算法讲解:P74.7 如果允许在循环队列的两端都可以进行插入和删除操作,要求: • 写出循环队列的类型定义 • 分别写出从队尾删除和从队头插入的算法 Int dequeue(cycqueue &Q, int &x) {//从队尾删除 If (O.front==Q.rear) return 0;//队列为空? Else {x=Q.data[Q.rear]; Q.rear=(Q.rear-1+maxsize)%maxsize; Return 1;}} Int dequeue(cycqueue &Q, int &x) {//从队头删除 If (O.front==Q.rear) return 0; Else {Q.front=(Q.front+1)%maxsize; x=Q.data[Q.front]; Return 1;}} Int enqueue(cycqueue &Q, int x) {//从队尾插入 If (O.front==(Q.rear+1)%maxsize return 0; Else {Q.rear=(Q.rear+1)%maxsize; Q.data[Q.rear]=x; Return 1;}} Int enqueue(cycqueue &Q, int x) {//从队头插入 If (O.rear==(Q.front-1+maxsize)%maxsize return 0;// 判断队列满则不能插入 Else {Q.data[Q.front]=x; Q.front=(Q.front-1+maxsize)%maxsize; Return 1;}}
考研真题 2. 名词解释:队列【大连海事大学 1996 一、6 ( 1分 )】 3. 什么是循环队列?【哈尔滨工业大学 2001 三、2(3分)】【河南大学 1998 一、4(3分)】 2、队列是允许在一端插入而在另一端删除的线性表,允许插入的一端叫队尾,允许删除的一端叫队头。最先插入队的元素最先离开(删除),故队列也常称先进先出(FIFO)表。 3、用常规意义下顺序存储结构的一维数组表示队列,由于队列的性质(队尾插入和队头删除),容易造成“假溢出”现象,即队尾已到达一维数组的高下标,不能再插入,然而队中元素个数小于队列的长度(容量)。循环队列是解决“假溢出”的一种方法。通常把一维数组看成首尾相接。在循环队列下,通常采用“牺牲一个存储单元”或“作标记”的方法解决“队满”和“队空”的判定问题。
考研真题 30. 举例说明顺序队的“假溢出”现象,并给出解决方案。【福州大学 1998 三、5 (6分)】 31. 怎样判定循环队列的空和满【燕山大学 99 二、3(4分) 32. 简要叙述循环队列的数据结构,并写出其初始状态、队列空、队列满时的队首指针与队尾指针的值。 【南京航空航天大学 1995 七(5分)】 33. 利用两个栈sl,s2模拟一个队列时,如何用栈的运算实现队列的插入,删除以及判队空运算。请简述这些运算的算法思想。【北京邮电大学 92 一、5分东南大学 99 一、1 (7分) 34.一个循环队列的数据结构描述如下【西北工大 1999 三 (8分)】 TYPE sequeuetp=RECORD elem:ARRAY[1..MAXSIZE] OF elemtp; front,rear:0..MAXSIZE; END; 给出循环队列的队空和队满的判断条件,并且分析一下该条件对队列实际存储空间大小的影响,如果为了不损失存储空间,你如何改进循环队列的队空和队满的判断条件? 30、设顺序存储队列用一维数组q[m]表示,其中m为队列中元素个数,队列中元素在向量中的下标从0到m-1。设队头指针为front,队尾指针是rear,约定front指向队头元素的前一位置,rear指向队尾元素。当front等于-1时队空,rear等于m-1时为队满。由于队列的性质(“删除”在队头而“插入”在队尾),所以当队尾指针rear等于m-1时,若front不等于-1,则队列中仍有空闲单元,所以队列并不是真满。这时若再有入队操作,会造成假“溢出”。其解决办法有二,一是将队列元素向前“平移”(占用0至rear-front-1);二是将队列看成首尾相连,即循环队列(0..m-1)。在循环队列下,仍定义front=rear时为队空,而判断队满则用两种办法,一是用“牺牲一个单元”,即rear+1=front(准确记是(rear+1)%m=front,m是队列容量)时为队满。另一种解法是“设标记”方法,如设标记tag,tag等于0情况下,若删除时导致front=rear为队空;tag=1情况下,若因插入导致front=rear则为队满。
如何改造单链表实现队列的链接存储? 3、队列的链式存储结构及实现(链队列) • 二个指针front 和rear指示队列头元素和尾元素的位置。 • 初始化空队列:front=rear=0 • 插入新的队尾元素: • 尾指针增1,尾指针始终指向队列尾元素的下一个位置 • //指向链表的尾结点 • 删除队列头元素: • 头指针增1,头指针始终指向队列头元素。 • //指向链表的头结点
a1 a2 an 3、队列的链式存储结构及实现(链队列) 非空链队列 head ∧ rear 空链队列 front ∧ rear
链队列:用链表示的队列 typedef struct QNode{ QelemType data ; struct QNode *next ; }QNode, *QueuePtr; typedef struct{ QueuePtr *front ; //队头指针,指向头结点 QueuePtr *rear ;//队尾指针,指向尾结点 } • 空链队列:队头指针和队尾指针均指向头结点。
单链表的链队 • 二个状态 队空状态:lqu->rear==NULL 或lqu->front==NULL 队满状态:不存在 • 二个操作: 元素入队: lqu->rear->next=p; lqu->rear=p; 元素出队:p=lqu->front; lqu->front=p->next; X=p->data; free(p);
链队列的运算: • 初始化链队initqueue • 判队列是否为空Queueempty(Q) 若队列Q为空,返回true,否则,返回false • 入队 Enqueue(&Q, e) 在队列Q中插入元素e。 • 出队Dequeue(&Q, &e) 若队列Q不空,则删除其队头元素。
队列操作在链式存储结构下的实现 • 初始化链队 Void initqueue(Liqueue *&lqu) { Lqu=(liQueue*)malloc(sizeof(Liqueue)); lqu->front= lqu->rear; } • 判队列是否为空 Int queueempty(Liqueue *lqu) { if ( lqu->rear==null || lqu->front==null ) return 1; Else return 0; }
队列操作在链式存储结构下的实现 • 入队 Enqueue(&Q, e){ p=(QueuePtr) malloc(sizeof(node)); p->data=e; p->next=NUll; Q.rear->next=p; Q.rear=p } 尾插法 出队 Dequeue(&Q, &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; free(p); }
算法实例P68,2 • 利用二个栈S1和S2来模拟一个队列,栈中元素为int型,栈中元素最多为maxsize,已知栈的三个运算定义如下: • PUSH(ST,x), POP(ST,&x); Sempty(ST); • 如何利用栈的运算来实现队列的入队、出队、判对空的三个运算? • 给出基本设计思想 • 用C描述算法,关键处给出注释
思想:S1作输入栈,模拟队列元素的入队; 出队时,S1退栈压入S2栈中,S2出栈即相当于队列的出队。 当S2和S1都为空时,队列空。 栈S1和S2来模拟一个队列,栈中元素为int型,栈中元素最多为maxsize,已知PUSH(ST,x), POP(ST,&x); Sempty(ST); 利用栈的运算来实现队列的入队、出队、判对空的运算 Int enqueue(Sqstack &s1,Sqstack &s2,int x) {//S1容量为maxsize Int y; If(S1.top==maxsize-1)// 栈s1满,S2有空? {if (!sempty(s2)) return 0; elseif(sempty(s2)) {While(!sempty(s1)) {pop(S1,y); push(s2,y);} Push(S1,x); return1;} Else(push(s1,x); return 1;} } Int dequeue(SqStack&s2,SqStack&s1,int &x) { Int y; If(!Sempty(s2)) {pop(s2,x); return 1;} Else {if(sempty(S1)) return 0; Else{ while(!Sempty(s1)) {pop(s1,y); push(s2,y);} Pop(s2,x);Return 1; } } } Int queue_empty(SqStack s1, SqStack s2) { if(Sempty(s1)&&Sempty(s2) return 1; Else return 0 }