Ch.3 栈和队列 黄刘生 中国科学技术大学计算机系 国家高性能计算中心(合肥)

Slides:



Advertisements
Similar presentations
电子成绩单项目实现.
Advertisements

第三章 鏈結串列 Linked List.
数据结构——树和二叉树 1/96.
第六章 树和二叉树.
第二章 线性表 1 线性表的逻辑结构及其基本操作 2 线性表的顺序存储结构 3 线性表的链式存储结构 4 静态链表 5 应用实例.
第二章 线性表.
資料結構與C++程式設計進階 資料結構概論 講師:林業峻 CSIE, NTU 6/ 7, 2010.
第三章 堆疊與佇列的基本應用 3-1 簡介堆疊(Stack) 3-2 迷宮問題研究 3-3 佇列(queue)的介紹
第九章 系 统 安 全 性 9.1 结构体 9.2 结构体型数组  9.3 结构体型指针 9.4 内存的动态分配 9.5 共用体
第4章 鏈結串列(Linked Lists) 4-1 動態記憶體配置-(6) 4-2 鏈結串列的基礎-(7)
Linked List Operations
第3章 栈和队列 3.1 栈 3.2 队列 3.3 应用.
單向鏈結串列 Singly Linked Lists.
第2章 线性表 线性结构 是一个数据元素的有序集合。.
第6章 佇列(Queues) 6-1 佇列的基礎 6-2 佇列的表示法 6-3 環狀佇列 6-4 雙佇列.
第7章 结构体、联合体和枚举类型 本章导读 本章主要知识点 《 C语言程序设计》 (Visual C++ 6.0环境)
C程序设计 第9章 自定义数据类型 主讲教师: 鲁 萍 西安建筑科技大学 理学院.
程式設計 博碩文化出版發行.
内容提要 对象的生命周期 构造函数 析构函数 拷贝构造函数. 常宝宝 北京大学计算机科学与技术系
佇列 (Queue).
資料結構 第5章 佇列.
Chap 3 堆疊與佇列 Stack and Queue.
补充内容 结构体 概述 定义结构体类型和定义结构体变量 结构体变量的引用 结构体变量的初始化 指针与结构体 用typedef定义类型的别名.
STRUCTURE 授課:ANT 日期:2010/5/12.
Function.
C语言程序设计 李祥.
线性表小结 元素之间的线性关系 顺序表 顺序表:元素相邻存储 单链表:后继指针链接 一维数组 给定下标随机存取
cn/~dongeliu/dsa.html 刘 东 信息学院6系 中国科学技术大学
第3章 栈和队列(二) 1/.
程序设计专题一 结构化程序设计与递归函数 主讲教师: 刘新国.
第2章 线性表 线性表抽象数据类型 顺序表 主要知识点 单链表 循环单链表 循环双向链表 静态链表 设计举例.
第十章 C高级程序应用—链表* 10.1链表的基本概念 10.2单向链表 10.3双向链表 10.4应用举例.
第5章 堆疊(Stacks) 5-1 堆疊的基礎 5-2 堆疊的表示法 5-3 堆疊的應用 - 運算式的計算與轉換
第5章 堆疊(Stacks) 5-1 堆疊的基礎 5-2 堆疊的表示法 5-3 堆疊的應用 - 運算式的計算與轉換
第3章 堆栈和队列 堆栈 堆栈应用 队列 队列应用 优先级队列 主要知识点.
第三章 栈与队列 £3.1 栈 £3.3 队列 £3.2 栈的应用举例 £3.1.1 栈的定义 £3.1.2 栈的顺序存储结构
第三章 栈和队列 3.1 栈 3.2 栈的应用举例 3.3 栈与递归的实现 3.4 队列.
第3章 栈和队列(一).
第三章 栈和队列.
数据结构 Data Structure 中南大学 主讲人:王国军,郑瑾 中南大学信息院计科系
資料結構與C++程式設計進階 堆疊與佇列(Stack & Queue) 講師:林業峻 CSIE, NTU 6/ 21, 2010.
第一章 绪论.
陈海明 副教授 信息学院 计算机系 电子信息类非计算机专业选修课 程序设计实践 陈海明 副教授 信息学院 计算机系
陈海明 副教授 信息学院 计算机系 电子信息类非计算机专业选修课 程序设计实践 陈海明 副教授 信息学院 计算机系
内容回顾 线性表的定义和特点 线性表的顺序存储及查找、插入和删除.
佇列(queue) Lai Ah Fur.
6.3 遍历二叉树和线索二叉树(知识点二) 遍历二叉树 一、问题的提出
第三章 栈和队列.
計數式重複敘述 for 迴圈 P
資料結構 第4章 堆疊.
严蔚敏、吴伟民编著 清华大学出版社 学习网站:
第四章 堆疊與佇列 課前指引 資料結構學科中,堆疊(Stack)與佇列是兩種相當典型的抽象資料型態,也是一種有特定進出規則的線性串列應用,主要特性是限制了資料插入與刪除的位置和方法。本章中將會介紹堆疊與佇列的概念與特性,及在計算機領域的各種相關應用。
陈海明 副教授 信息学院 计算机系 电子信息类非计算机专业选修课 程序设计实践 陈海明 副教授 信息学院 计算机系
王玲 第 2 章 线性表 王玲 2019/2/25.
自我參考結構 (self-reference – 1)
常宝宝 北京大学计算机科学与技术系 数据结构(三) 常宝宝 北京大学计算机科学与技术系
第五章 递归与广义表 递归的概念 递归过程与递归工作栈 递归与回溯 广义表.
第十章 结构体与链表 西安工程大学.
Linked Lists Prof. Michael Tsai 2013/3/12.
第四章 栈和队列 栈 ( Stack ) 队列 ( Queue ) 优先队列 (Priority Queue) 小结.
Chap 5 函数 5.1 计算圆柱体积 5.2 使用函数编写程序 5.3 变量与函数.
本教學投影片係屬教科書著作之延伸,亦受著作權法之保護。
第 六 讲 栈和队列(一).
1. 志明打算就客戶服務開發一個語音互動(IVR)系統, 顧客可透過電話鍵盤與系統進行互動。
第七章  数 组.
第4章 鏈結串列(Linked Lists) 4-1 動態記憶體配置-(6) 4-2 鏈結串列的基礎-(7)
第1章 数据结构基础概论 本章主要介绍以下内容 数据结构研究的主要内容 数据结构中涉及的基本概念 算法的概念、描述方法以及评价标准.
Chapter 2 Entity-Relationship Model
安排座位.
Presentation transcript:

Ch.3 栈和队列 黄刘生 中国科学技术大学计算机系 国家高性能计算中心(合肥)

§3.1 栈 定义和运算 入栈 出栈 栈 —— 仅在表的一端插、删的线性表 栈顶 —— 插删的一端 栈底 —— 另一端 插入——进(入)栈、删除——出(退)栈 栈顶 —— 插删的一端 栈底 —— 另一端 结构特征 --“后进先出” 修改原则:退栈者总是最近入栈者 服务原则:后来者先服务 (LIFO表) 例: 入栈 出栈

§3.1 栈 Note:后入栈者先出栈,但不排除后者未进栈,先入栈者先出栈。 基本运算:① 判栈空 ②入栈 ③出栈 ④判栈 满 ⑤读栈顶 ⑥置空栈

§3.1.1 顺序栈 底相对固定 可设在向量的任一端 Top指针为下标类型 (整型量) #define StackSize 100 typedef struct { DataType data[StackSize]; int top; }SeqStack;

§3.1.1 顺序栈 设栈底在向量低端data[0],则: 入栈 :top+1 出栈:top-1 栈空:top<0 栈满:top=StackSize-1 上溢:满时入栈 下溢:空时出栈(不一定是错误,可能是转移控制条件)

§3.1.1 顺序栈 Note:top指针不是指物理地址,与C的指针变量含义不同。可理解为相对地址,是指示元素的所在位置

§ 3.1.1 顺序栈 实现 初始化 (置空栈) int StacEmpty(SeqStack *S) { //亦可用非指针类型 判栈满 void InitStack(SeqStack *S) { S->top=-1; } 判栈空 int StacEmpty(SeqStack *S) { //亦可用非指针类型 return S->top<0; 判栈满 int StackFull (SeqStack *S) { return S->top==StackSize-1;

§ 3.1.1 顺序栈 入栈 void Push(SeqStack *S, DataType x) { if ( StackFull(S) ) Error(“overflow”);//上溢,退出运行 S->data[++S->top]=x; // 指针先加1,后x入栈 } 出栈 DataType Pop(SeqStack *S) { if ( StackEmpty(S) ) Error(“UnderFlow”); //下溢 return S->data[S->top--]; //返回栈顶后指针减1 读栈顶

§ 3.1.2 链栈 只在表头操作,故头指针即为top,无需头结点 定义 typedef struct snode { DataType data; struct snode *next; } StackNode; typedef struct { StackNode *top; } LinkStack; 链栈动态分配结点,无需考虑上溢

§ 3.1.2 链栈 void InitStack(LinkStack *S) { S->top=NULL; } int StackEmpty (LinkStack *S) { return S->top==NULL; void Push(LinkStack *S, DataType x) { StackNode *p=(StackNode*) malloc (sizeof(StackNode)); p->data=x; p->next=s->top; s->top=p;

§ 3.1.2 链栈 DataType Pop(LinkStack *S) { DataType x; StackNode *p=S->top; //保存栈顶指针 if ( StackEmpty(S) ) Error (“underflow”); // 下溢 x=p->data; //保存栈顶数据 S->top=p->next; //将栈顶结点从链头摘下 free(p); //释放原栈顶结点空间 return x; } EX.3.6,3.17,3.19

§3.2 队列 定义 队列:运算受限的线性表,一端插入(队尾),另一端删除(队头)。 结构特征 先进先出,先来先服务,FIFO表 例子:排队现象 操作 入队(插入)序列: 出队(删除)序列:

§ 3.2 队列 顺序队列 —— 队列的顺序存储结构 空队列:front = rear; 初值为0 入队:插入rear位置,然后加1

§ 3.2 队列 上溢:队满时入队 下溢:队空是出队 (不一定是错误,可能是转移控制条件) 伪上溢:队非满但不能插入 原因:f,r 只增不减 例如:入,出,入,出,…… 尽管任一时刻,队中最多只有1个元素,但无论事先定义多大的空间,均会产生指针越界

§ 3.2 队列 怎样消除伪上溢? 循环队列 实用顺序队列多为循 重复利用已删除的结点空间,将向量视为首尾相接的环。这种用循环向量实现的队列称为循环队列。 f,r在循环意义下的加1动作:越界时,令其指向下界0 i = (i+1==QueueSize) ? 0:i++; // i为front或rear 可用模运算简化: i=(i+1)%QueueSize; 循环队列 实用顺序队列多为循 环队列

§ 3.2 队列 边界问题 定义 队满和队空时,front=rear,如何区分?解决方法: 设一布尔量以示区分 留一个结点不用。约定 入队前,测试尾指针+1 (循环定义下) 是否等于头指针。若相等则为满 使用1个计数器,记录队列长度 (用此法) 定义 #define QueueSize 100 typedef struct { int front; int rear; int count; DataType data [QueueSize]; } CirQueue;

§ 3.2 队列 操作实现 置空队 void InitQueue (CirQueue *Q) { Q->front = Q->rear = 0; Q->count = 0; // 队空 } 判队空 int QueueEmpty (CirQueue *Q) { return Q->count == 0; } 判队满 int QueueFull (CirQueue *Q) { return Q->count ==QueueSize; }

§ 3.2 队列 入队 void EnQueue (CirQueue *Q, DataType x) { if (QueueFull (Q) ) Error(“overflow”); //上溢 Q->count++; Q->data [Q->rear] = x; // 插入 Q->rear = (Q->rear+1)%QueueSize; // 尾指针加1 } 出队 DataType DeQueue (CirQueue *Q) { if (QueueEmpty (Q) ) Error (“underflow”); //下溢,不一定是错 temp = Q->data[Q->front]; Q->count--; Q->front= (Q->front+1)%QueueSize; // 头指针加1 return temp; }

§ 3.2 队列 链队列 仅限于在表头和尾插删的单链表 typedef struct qnode{ DataType data; struct qnode *next; }QNode; typedef struct { QNode *front; QNode *rear; } LinkQueue; LinkQueue Q; 不带头节点:

void InitQueue (LinkQueue *Q) { Q->front = Q->rear = NULL; //若有头结点则不同 } int QEmpty (LinkQueue *Q) { //无上溢,故不判满队 return Q->front == NULL; //头尾指针均为空,有头结点时f = r void EnQueue (LinkQueue *Q, DataType x) { QNode *p = (QNode *) malloc( sizeof(QNode) ); p->data = x; p->next = NULL; // 新结点作为新的队尾 if ( QEmpty(Q) ) // 若有头结点无需判此项 Q->front = Q->rear = p; // *p插入空队 else { // 插入非空队尾,有无头结点均要做此项 Q->rear->next = p; // *p链到原尾结点之后。 Q->rear = p; // 指向新尾*p

3.链队列 DataType DeQueue (LinkQueue *Q) { if ( QEmpty(Q) ) } Ex.3.28 Error (“underflow”); //下溢 p = Q->front; // 指向队头 x = p->data; Q->front = p->next; // 队头摘下 if (Q->rear == p) // 原队中只有一个结点,删去后队变空 Q->rear = NULL; free (p) ; return x; } Ex.3.28

§ 3.3 栈和队列的应用 § 3.3.1 栈的应用 数据转换 问题: 一非负十进制整数N  B进制整数 例:2810= 3•81 + 4•80 = 348 7710= 1•43 + 0•42 + 2•41 + 5•40 = 10254 规律: 其中 表示B进制数的第 i 位数字 十进制数N可用 位B进制数表示为 (3.1)

§3.3.1 栈的应用 如何确定bj ? 令 ,则(3.1)式为: (3.2) 该式整除B,则余数为b0,商为括号内的和式。故 (3.2)式可表达为: N = ( N/B )•B + N%B // “/”为整除 算法思想 模除求余数: N%Bb0 整除求商:N/B,令其为新N,重复①求b1,反复至某N为0时结束 上述过程依次求出的B进制各位为(从低位至高位): b0,b1 ,… , bj,用栈保存,退栈输出bj,bj-1 ,… , b0即为所求。

§ 3.3.1 栈的应用 例如:

while (N) { //从右向左产生bi , i=0,1, …, j Push( &S, N%B); // 将 bi 入栈 N=N/B; 实现 typedef int DataType; void MultiBaseOutput (int N, int B) { // N为非负十进制整数, B为基 int i; SeqStack S; //顺序栈S InitStack( &S ); //置空栈 while (N) { //从右向左产生bi , i=0,1, …, j Push( &S, N%B); // 将 bi 入栈 N=N/B; } while( !StackEmpty(&S) ) { // 栈S非空 i = Pop(&S); printf(“%d”,i); 时间复杂度:O(logBN ) 25 25

2.栈与递归 § 3.3.1 栈的应用 递归是一种强有力的数学工具,可使问题的描述和求解变得简洁与清晰 递归 若一函数、过程或数据结构定义的内部又直接或间接使用了定义本身,则称它们是递归的,或递归定义的 26 26

2.栈与递归 例:Bakus-Naur Form (BNF) 巴科式范式早期定义Algol 60 语法。 直接递归 <标识符> ::= <字母>|<标识符><字母>|<标识符><数字> <字母> ::= a| b | …… | y | z <数字> ::= 0 | 1| 2| | …… | 8 | 9 由此定义的标识符是由字母打头的字母数字串 简接递归 <表达式> ::= <项> + <项> | <项> - <项> | <项> <项> ::= <因子> * <因子> | <因子> / <因子> | <因子> <因子> ::= (<表达式>) | <字母> | <数字> 合法的表达式:A+5,B*C+D,(A+B*C)/D 非终结符 终结符 可选分隔符

//递归终结条件 //递归步骤 (n-1)!的规模比n!小1 2.栈与递归 例 1: 递归算法设计(分治法)分解、求解、组合 step1: 将原问题分解为一个或多个规模更小,但与问题特性类似的子问题 (递归步骤) // 解子问题为调用语句 step2:确定一个或多个无须分解,可直接求解的最小子问题 (终结条件)// 归纳基础 例 1: //递归终结条件 //递归步骤 (n-1)!的规模比n!小1

2.栈与递归 int F (int n) { //设n为非负整数 if (n==0) return 1; else return n*F(n-1) ; } 至少有一个直接求解的最小子问题,保证递归终止,否则无限循环 分解为一个子问题,若F(n)是解n!,则F(n-1)可解(n-1)! 29 29

2.栈与递归 有些问题表面上不是递归定义的,但可通过分析,抽象出递归的定义。 例2:写一个就地生成n个元素a1, a2, …, an全排列 (n!) 的算法,要求算法终止时保持a1, a2, …, an原状 解:设 A[0 ..n-1] 基类型为char,“就地”不允许使用 A 以外的数组 生成a1, a2, …, an全排列  n个子问题 求n-1个元素的全排列 + nth个元素 1st子问题 a1, a2, …, an-1 an // 2nd子问题 a1, …, an-2, an an-1 //A[n-1]↔A[n] 3rd子问题 a1, …, an, an-1 an-2 //A[n-2]↔A[n] ┊ ┊ ┊ nth子问题 an,a2 …, an-1 a1 //A[1]↔A[n] 递归终结分支 当 n=1 时, 一个元素全排列只有一种,即为本身。实际上无须进一步递归,可直接打印输出A

2.栈与递归 算法:以A[0..7]为例 void permute (char A[ ], int n) { //外部调用时令 n=7 if (n==0) print (A); // 打印A[0..n] else { Permute(A,n-1); //求A[0..n-1]的全部排列。1st子问题不用交换 for (i=n-1; i>0; i--) { Swap(A[i], A[n]); // 交换ai 和an 内容,说明为引用 Permute(A,n-1); // 求A[0..n-1] 全排列 Swap(A[i],A[n]); //交换,恢复A[0..n]原状 }//endfor }//endif } 时间: 所以实验时,n不能太大

2.栈与递归 例3:n阶Hanoi塔问题 将X上的圆盘移到Z上,要求按同样次序排列,且满足: 每次只能移动一片 圆盘可插在X,Y,Z任一塔座上 任一时刻大盘不能压在小盘上

分解 设 n>1 原问题:将n片从X移到Z,Y为辅助塔,可分解为: 将上面n-1 个盘从X移至Y,Z为辅助盘 将 nth 片从X移至 Z 将Y上n-1个盘子移至Z,X为辅助盘 终结条件 n = 1时,直接将编号为1的盘子从 X 移到Z void Hanoi (int n, char x, char y, char z ) { // n个盘子从 X 移至 Z,Y 为辅助 if ( n==1 ) move(X,1,Z); // 将1号盘子从 X 移至 Z, 打印 else { Hanoi (n-1,x,z,y); //源X,辅Z,目Y move (x,n,z); Hanoi (n-1,y,x,z); //源Y,辅X,目Z } //子问题特征属性与原 //问题相同规模小1,参 //数不同

§ 3.3.1 栈的应用 3.递归的内部实现 调用 调用一个函数时,系统将为调用者构造一个活动记录,并将其压入栈的顶,然后将程序控制权转移到被调用函数。若被调用函数有局部变量,则在栈顶也要为其分配空间,形成一个活动结构。 实际上所有的递归或非递归函数都是这样实现的 活动结构: 局部变量 活动记录:参数表的内容为实参 返址为函数调用语句的下一指令的位置

3.递归的内部实现 返回 当被调用函数执行完毕时,系统将活动结构退栈,并根据退栈返回地址将程序控制权转移给调用者继续执行 例: F(4)为例 改写: int F (int n) { int temp; if (n==0) return 1; //返回语句引 起退栈 else { //调用F(n-1) 引起入栈 temp = n*F(n-1); return temp; // 退栈 } void main(void) { int n; n = F(4); //调用引起压栈 } Ret L1 Ret L1: 赋值语句的地址 Ret L2: 计算乘法的地址 为简单起见,假设局部变量不入栈 Ret L2

3.递归的内部实现 执行返回指令 Ret L2: temp=1*1; //从F(0)返回 0! = 1 是递归终结条件,故执行F(0)引起返回(return 1) 退栈 , 返回到F(1)的Ret L2处,继续执行temp = 1*1; 按着执行return temp又引起 退栈,返回到F(2)的Ret L2处,执行temp = 2*1,… 执行返回指令 Ret L2: temp=1*1; //从F(0)返回

§ 3.3.1 栈的应用 4.递归算法的正确性 初学者很难相信递归程序的正确性 原因:一个函数尚未完成 (即对本函数的正确性还未确 信) 时又调用了自身,故对递归函数正确性缺乏信心 例:非递归函数或过程 假设Q正确的情况下,证明了P正确, 则一旦证明了被调过程Q是正确的,我们就对P的正确性深信不疑! 由于P、Q各自独立,独立于P来证明Q的正确性很容易,这大概是对自己写非递归程序较有信心的缘故

4.递归算法的正确性 若P是递归过程,则不可能独立于P来证明被调过程 (亦自身) 是否正确。因此,我们只能假设过程P内部所有递归的调用是正确的 (不考虑其内部实现细节),才能由此证明P的正确性。其理论依据是数学归纳法: (1)证明参数满足递归终结条件时函数或过程正确 (相当于归纳基础)。 (2)假设过程函数内部的所有递归调用语句正确 (相当于归纳假设),由此证明过程正确或函数是正确的 (相当于归纳步骤) Note: 函数内的递归调用语句的参数应比函数本身的参数更接近递归终结条件参数,这样才能确保递归是可终止的

§ 3.3.1 栈的应用 上机题: 调试跟踪:全排列和Hanoi塔

§3.3.2 队列的应用 例:周末舞会,男、女各排成一队,跳舞时,依次从男、女队的头上各出一人配成舞伴,若两队人数不同,较长的队中未配对者等下一轮舞曲 typedef struct { char name[20]; char sex; // M—男,F—女 } Person; typedef person DataType; //将队列定义中的数据类型改为Person void DancePartners(Person dancer[ ], int num){ int i; Person P; CirQueue Mdancers, Fdancers; InitQueue(&Mdancers); // 男士队列 InitQueue(&Fdancers);

§3.3.2 队列的应用 for( i=0; i<num; i++ ) {//num个男女依其性别入队 P = dancer[ i ]; if (P.sex == ‘M’) EnQueue (&Mdancers, P); // 入男队 else EnQueue (&Fdancers, P); // 入女队 } printf (“The dancing partners are:\n\n”); while (!QueueEmpty(&Fdancers) && !QueueEmpty(&Mdancers)) { // 男女队列均非空 P=DeQueue(&Fdancers); // 女士出队 printf(“%s ”, P.name); // 女士姓名 P=DeQueue(&Mdancers); //男士出队 printf(“%s\n”, P.name);

§3.3.2 队列的应用 if (!QueueEmpty(&Fdancers)) { //女队非空,输出剩余人数及队头者名字 printf(“\n There are %d women waiting for the next round.\n”, Fdancers.count);// count是队列属性,长度 P=QueueFront(&Fdancers); // 取队头 printf (“%s will be the first to get a partner.\n”, P.name); } else{ // 男队类型处理 } 时间复杂度:O(n)