1.61k likes | 1.82k Views
数据结构与算法 Data Structures and Algorithm. 课程目录. 第 1 章 概论 第 2 章 线性表 第 3 章 栈和队列 第 4 章 串和数组 第 5 章 递归和广义表 第 6 章 树和二叉树 第 7 章 图 第 8 章 查找 第 9 章 内排序 第 10 章 文件 第 11 章 算法设计技术. 第一章 概论. 本章内容: 1.1 什么是数据结构 1.2 算法和算法分析. 本章学习要点: 熟悉各名词、术语的含义,掌握基本概念。 理解算法五个要素的确切含义。 掌握计算语句频度和估算算法时间复杂度的基本方法。.
E N D
课程目录 第1章 概论 第2章 线性表 第3章 栈和队列 第4章 串和数组 第5章 递归和广义表 第6章 树和二叉树 第7章 图 第8章 查找 第9章 内排序 第10章 文件 第11章 算法设计技术
第一章 概论 本章内容: 1.1 什么是数据结构 1.2 算法和算法分析 本章学习要点: • 熟悉各名词、术语的含义,掌握基本概念。 • 理解算法五个要素的确切含义。 • 掌握计算语句频度和估算算法时间复杂度的基本方法。
数据结构的定义 数据结构是一门讨论“描述现实世界实体的数学模型(非数值计算)及其上的操作在计算机中如何表示和实现”的学科。 数据结构:是指数据以及数据元素相互之间的联系,可以看作是相互之间存在着某种特定关系的数据元素的集合。
数据结构的内容 对数据结构的内容包括以下几个方面: • 数据的逻辑结构,指数据元素之间的逻辑关系。 • 数据的存储结构,指数据元素及其关系在计算机存储器中的存储方式,也称为数据的物理结构。 • 数据运算,指施加在数据上的操作。
什么是算法 算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中,每一条指令表示一个或多个操作。 粗略地说,算法是为了求解问题而给出的一个指令序列,程序是算法的一种具体实现。 一个算法必须满足以下五个重要特性: 1. 有穷性2. 确定性3. 可行性 4. 有输入5. 有输出
算法的重要特征 (1) 有穷性: 算法在有穷步之后结束,每一步在有穷时间内完成。 (2) 确定性: 算法中的每一条指令有明确的含义,无二义性。 (3) 可行性: 可通过已经实现的基本运算执行有限次来实现算法中的所有操作。
算法分析 算法分析的两个主要方面是分析算法的时间复杂度和空间复杂度。算法的执行时间主要与问题的规模有关。问题规模是一个与输入有关的量。 语句频度是指算法中该语句被重复执行的次数。算法中所有语句的频度之和记作T(n),是该算法所求解问题规模的函数。当问题规模趋向无穷大时,T(n)的数量级称为渐进时间复杂度,简称时间复杂度,记作T(n)=O(f(n))。 通常采用算法中表示基本运算的语句的频度来分析算法的时间复杂度。
练习题举例 • 数据结构中的逻辑结构是指(),物理结构是指( )。 • 算法的基本特征包括有穷性、( )、( )、有输入和输出 。 • 算法的有穷性是指( )。 • 算法的确定性是指( )。 • 算法的可行性是指( )。 • 算法分析的两个主要方面是分析算法的( )和空间复杂度。
练习题举例 • 语句频度是指( )。 • 设n为正整数,对下面的程序段,写出语句①、②、③的频度及该程序段的时间复杂度。 • for (i=1; i <=n; i++) • { • s=0; ① n • for (j=1; j <=n; j++) • s=s+i×j; ② n2 • if (s%2) ③ n • print(s); • }
第二章 线性表 本章内容: 2.1 线性表的基本概念 2.2 线性表的顺序存储结构 2.3 线性表的链式存储结构 2.4 双向链表 2.5 链表的应用 本章学习要点: • 掌握线性关系的定义。 • 熟练掌握线性表的顺序存储结构(顺序表)和链表存储结构(链表),及线性表的各种基本操作的实现。 • 从时间和空间复杂度的角度综合比较线性表两种存储结构的不同特点及其适用情况。
d ... a1 a2 ... ai an 第二章 线性表 本章内容: 2.1 线性表的基本概念 2.2 线性表的顺序存储结构 2.3 线性表的链式存储结构 2.4 双向链表 2.5 链表的应用 • 一个线性表是有n个数据元素的有限序列: (a1,a2,…,ai,…,an) • 线性表的顺序存储指用一组地址连续的存储单元依次存储线性表的数据元素。这称为顺序表。 • 特点: • 存储单元地址连续(需要一段连续空间) • 逻辑上相邻的数据元素其物理位置也相邻 • 存储密度大(100%) • 随机存取,元素的存储位置存在如下关系: • Loc(ai) = Loc(a1) + (i - 1) * d (1≤i≤n)
head … a1 a2 ai-1 ai 头指针 头结点 首元结点 … ai+1 an ^ 单链表示意图 第二章 线性表 本章内容: 2.1 线性表的基本概念 2.2 线性表的顺序存储结构 2.3 线性表的链式存储结构 2.4 双向链表 2.5 链表的应用 • 线性表的链式存储指用任意的存储单元存放线性表中的元素,每个元素与其直接前驱和(或)直接后继之间的关系用指针来表示。这称为链表。
head … a1 a2 ai-1 ai 头指针 头结点 首元结点 … ai+1 an ^ 单链表示意图 第二章 线性表 • 头指针是指向链表第一个结点的指针。 • 头结点是链表中的第一个结点但是头结点中不存放数据元素。
head … a1 a2 ai-1 ai 头指针 头结点 首元结点 … ai+1 an ^ (a)单链表示意图 … a1 a2 an-1 an (b) 循环单链表 tail head ∧ … … a1 a2 ai an-1 an ∧ (c)双向链表示意图 单链表、循环链表和双向链表
head … a1 a2 ai-1 ai 头指针 头结点 首元结点 … ai+1 an ^ (a)单链表示意图 … a1 a2 an-1 an (b) 循环单链表 tail 单链表、循环链表的类型定义 typedef struct LNode { ElemType data;/*存储数据元素*/ struct LNode *next;/*指向后继结点*/ }SLink,*LinkList; LinkList head,tail;
练习题举例 • 顺序表中,逻辑上相邻的数据元素在物理位置上( 也相邻)。线性表L=(a1,a2,...,an)采用顺序存储,假定在不同的n+1个位置上的插入概率相同,则插入一个新元素平均需要移动的元素个数是( n/2 )。 • 在一个长度为n的顺序表中, 在第i个元素(1 i n)之前插入一个新元素时, 需向后移动( n-i+1 )个元素。 • 在表长为n的单链表中, 取得第i (1 i n)个数据元素必须从( 头指针)出发开始寻找。 • 在一个具有n个结点的有序单链表中,插入一个新结点并保持链表有序的算法时间复杂度为( O(n) ) 。 • 若线性表中最常用的操作是删除第一个结点或在最后一个结点后插入一个结点,则( C ) 存储方式最节省运行时间及空间。 • A. 单向链表 B. 仅含头指针的单向循环链表 • C. 仅含尾指针的单向循环链表 D. 双向链表
练习题举例 • 请对线性表进行顺序存储和链式存储的特点作比较。 • 基于空间的考虑:顺序表的存储空间是静态分配的,而链表的存储空间是动态分配的。因此,当线性表的长度变化较大,难以估计其存储空间规模时,宜采用动态链表作为存储结构。反之,则采用顺序表作为存储结构。另外,存储密度越大,则存储空间的利用率越高。显然,顺序表的存储密度为1,而链表的存储密度小于1。因此,当线性表的长度变化不大,易于事先确定其大小时,为了节省空间,采用顺序表为宜。 • 基于时间的考虑:顺序表是由向量实现的,它是一种随机存取结构,而链表是一种顺序存储结构。因此,若线性表的操作主要是进行查找,很少进行插入和删除操作时,采用顺序表作存储结构为宜。反之,则采用链表作为存储结构。
练习题举例 • 编写算法,将一个带头结点的单链表就地逆置。(请先说明算法思路,再用类高级语言编写算法。 ) • 在单链表的删除结点算法中,如果已知p指针指向的被删结点存在直接后继,则我们可以通过把p的直接后继值送到p中再删去p的后继来完成,这样只需要O(1)时间。试编写实现这一思想的算法。 • 设有一个循环链表,其结点值均为正整数。试写出一个算法,反复找出链表中结点值最小的结点,并输出该值;然后将该结点从链表中删除,直到链表空为止,最后再删除表头结点。(简要说明算法思想)
… … ∧ head a1 a2 ai-1 ai ai+1 an-1 an 练习题举例 7. 编写算法,将一个带头结点的单链表就地逆置。(请先说明算法思路,再用类高级语言编写算法。 ) 单链表逆置 … … ∧ head a1 a2 ai-1 ai ai+1 an-1 an
单链表就地逆置算法思路 • 将原链表看成由两部分组成:已经完成逆置的部分和未完成逆置的部分。 • 设置两个指针p和s,p指向已完成逆置部分链表的第一个结点,s指向未逆置部分的第一个结点。 p s … … ∧ ∧ head a1 a2 ai-1 ai ai+1 an-1 an
单链表就地逆置算法思路 • 将原链表看成由两部分组成:已经完成逆置的部分和未完成逆置的部分。 • 设置两个指针p和s,p指向已完成逆置部分链表的第一个结点(表头结点),s指向未逆置部分的第一个结点。 • 初始时令指针p指向第一个元素结点,s指向p所指结点的后继。将第一个元素结点的指针域置为空。 p s p = head->next; if (p) s = p->next; p->next = NULL; ∧ ∧ head 9 14 15 27 40
单链表就地逆置算法思路 • 将原链表看成由两部分组成:已经完成逆置的部分和未完成逆置的部分。 • 设置两个指针p和s,p指向已完成逆置部分链表的第一个结点(表头结点),s指向未逆置部分的第一个结点。 • 初始时令指针p指向第一个元素结点,s指向p所指结点的后继。将第一个元素结点的指针域置为空。 • 对于s所指结点,令t指向其后继结点,然后将其指针域改为指向p所指结点,并用s更新p,t更新s。重复该过程,直到未逆置部分的结点全部处理完。 t p s t = s ->next; s->next = p; // ① p = s; // ② s = t; // ③ ② ③ ∧ ∧ head ① ① 9 14 15 27 40
单链表就地逆置算法思路 • 将原链表看成由两部分组成:已经完成逆置的部分和未完成逆置的部分。 • 设置两个指针p和s,p指向已完成逆置部分链表的第一个结点,s指向未逆置部分的第一个结点。 • 初始时令指针p指向第一个元素结点,s指向p所指结点的后继。将第一个元素结点的指针域置为空。 • 对于s所指结点,令t指向其后继结点,然后将其指针域改为指向p所指结点,并用s更新p,t更新s。重复该过程,直到未逆置部分的结点全部处理完。 算法: void reverse(LinkList head) { //带头结点的单链表就地逆置 p = head->next; if (!p) return; //空链表 s = p->next; p -> next = NULL; while (s) { t = s -> next; s -> next = p; p = s; s = t; } head -> next = p; } t p s t = s ->next; s->next = p; // ① p = s; // ② s = t; // ③ ② ③ ∧ ∧ head ① ① 9 14 15 27 40
单链表就地逆置算法思路(二) • 将原链表看成由两部分组成:已经完成逆置的部分和未完成逆置的部分。 • 设置两个指针p和s,p指向已完成逆置部分链表的第一个结点,s指向未逆置部分的第一个结点。 • 初始时令指针p指向第一个元素结点,s指向p所指结点的后继。将第一个元素结点的指针域置为空。 • 将未逆置部分的结点逐个插入到头结点之后。
单链表就地逆置算法思路(二) p s p = head->next; if (p) s = p->next; p->next = NULL; ∧ ∧ head 9 14 15 27 40 p = s; s = s ->next; p->next = head -> next; head -> next = p; p s ∧ ∧ head 9 14 15 27 40
单链表就地逆置算法思路(二) p s ∧ ∧ head 9 14 15 27 40 p = s; s = s ->next; p->next = head -> next; head -> next = p; p s ∧ ∧ head 9 14 15 27 40
单链表就地逆置算法思路(二) • 将原链表看成由两部分组成:已经完成逆置的部分和未完成逆置的部分。 • 设置两个指针p和s,p指向已完成逆置部分链表的第一个结点,s指向未逆置部分的第一个结点。 • 初始时令指针p指向第一个元素结点,s指向p所指结点的后继。将第一个元素结点的指针域置为空。 • 将未逆置部分的结点逐个插入到头结点之后。 算法: void reverse(LinkList head) { //带头结点的单链表逆置 p = head->next; if (!p) return; s = p->next; p->next = NULL; while (s) { p = s; s = s -> next; p -> next = head -> next; head->next = p; } }
第三章 栈和队列 本章内容: 3.1 栈 3.2 队列 本章学习要点: • 掌握栈和队列的运算特点,并能在相应的应用问题中正确选用。 • 熟练掌握栈类型的两种实现方法,特别应注意栈满和栈空的条件以及其描述方法。 • 熟练掌握循环队列和链队列的基本操作实现算法,特别注意队满和队空的描述方法。
第三章 栈和队列 本章内容: 3.1 栈 3.2 队列 • 栈是后进先出的线性表,队列是先进先出的线性表; • 在应用中,栈和队列都是容器,因此需要判断是否“栈空”或“队空”; • 采用顺序存储结构时,栈和队列都可能存在“溢出”问题,分别称为“栈满”、“队满”; • 采用顺序结构的队列常见形式是循环队列。
BCD CD D ABCD 栈的基本运算 • 栈的运算演示 (1)A、B、C、D四个元素依次进入一个栈,再依次出栈,得到一个输出序列DCBA 。
栈的基本运算 • 栈的运算演示 (1)A、B、C、D四个元素依次进入一个栈,再依次出栈,得到一个输出序列DCBA。 DCBA DCB DC D
BCD CD ABCD 栈的基本运算 • 栈的运算演示 (1)A、B、C、D四个元素依次进入一个栈,再依次出栈,得到一个输出序列BCDA 。 B
CD D 栈的基本运算 • 栈的运算演示 (1)A、B、C、D四个元素依次进入一个栈,再依次出栈,得到一个输出序列BCDA 。 BC BCD
栈的基本运算 • 栈的运算演示 (1)A、B、C、D四个元素依次进入一个栈,再依次出栈,得到一个输出序列BCDA 。 BCD BCDA
练习题举例 • 用S、X分别表示入栈、出栈操作,若元素入栈顺序为1、2、3、4,为得到出栈序列1、3、4、2,则相应的S和X操作串是( SXSSXSXX)。 • 若push、pop分别表示入栈、出栈操作,初始栈为空且元素a、b、c依次进栈,则经过操作序列push、push、pop、pop、push、pop之后,得到的出栈序列为( b a c )。 • 如果进栈的序列为1、2、3, 则不能得到的出栈序列为( 3 1 2)。
e8 e9 e7 7 0 e10 6 1 5 2 e6 4 3 e5 e4 循环队列 • 令队列空间中的一个单元闲置,使得在任何时刻,保持Q.rear和Q.front之间至少间隔一个空闲单元 • 队列满(full): (Q.rear+1)%MAXSIZE== Q.front • 队列空: Q.rear==Q.front • 队列长度: (Q.rear - Q.front+ MAXSIZE)% MAXSIZE Q.rear Q.front MAXSIZE=8
… a1 a2 an-1 an tail 练习题举例 • 在循环队列结构中(队列空间容量为M),Q.rear指向队尾元素所在位置,Q.front指向队头元素所在位置,则队列的长度为( (Q.rear - Q.front + 1 + M) % M )。 • 用循环链表表示的队列长度为n,若只设头指针,则出队和入队的时间复杂度分别是( O(1))和(O(n));若只设尾指针,则出队和入队的时间复杂度分别是(O(1))和(O(1))。 … a1 a2 an-1 an front 循环链队列
e8 e9 e7 7 0 e10 6 1 5 2 e6 4 3 e5 e4 练习题举例 1. 若以域变量rear和length分别指示循环队列中队尾元素的位置和队列中元素的个数。请完善下面的入队列和出队列的算法。 #define MAXQSIZE 100 //最大队列长度 typedef struct { Qelemtype *base; //base为队列所在存储区域的首地址 int length; //队列长度 int rear; //队尾元素位置 }SqQueue; Q.rear MAXSIZE=8
练习题举例 1. 若以域变量rear和length分别指示循环队列中队尾元素的位置和队列中元素的个数。请完善下面的入队列和出队列的算法。 #define MAXQSIZE 100 //最大队列长度 typedef struct { Qelemtype *base; //base为队列所在存储区域的首地址 int length,rear; //队列长度和队尾元素位置 }SqQueue; Status EnQueue(SqQueue &Q, Qelemtype e) {//将元素e插入队列Q if( (1) ) return ERROR;//队列满,无法插入 Q.rear = (2) ; //计算元素e的插入位置 (3) = e; //在队尾加入新的元素 Q.length++; //队列长度加1 Return OK; } // (1) Q.length > = MAXSIZE(2) (Q .rear + 1)%MAXSIZE(3) Q.base[Q .rear]
练习题举例 1. 若以域变量rear和length分别指示循环队列中队尾元素的位置和队列中元素的个数。请完善下面的入队列和出队列的算法。 #define MAXQSIZE 100 //最大队列长度 typedef struct { Qelemtype *base; //base为队列所在存储区域的首地址 int length,rear; //队列长度、队尾元素位置 }SqQueue; Status DeQueue(SqQueue &Q, Qelemtype &e) { //删除队头元素,并用e带回其值 if( (4) ) return ERROR;//队列空 e = Q.base[(5) ]; //取队头元素 Q.length--; //队列长度减1 return OK; } // (4) Q.length <=0 (5) (Q .rear – Q.length + 1+MAXSIZE)% MAXSIZE
练习题 • 写出用单链表表示栈的入栈算法和出栈算法。 • 写出用循环单链表表示队列的判断队空、入队列和出队列算法。
第四章 串和数组 本章内容: 4.1 串 4.2 数组 4.3 稀疏矩阵 本章学习要点: • 掌握串的定义、基本运算及模式匹配过程。 • 掌握数组的定义及存储,掌握对特殊矩阵进行压缩存储时的下标变换公式。 • 了解稀疏矩阵的压缩存储方法的特点。
串和数组 1. 串的基本运算 串赋值、串复制、求串长、判断串相等、串连接、求子串等 2. 模式匹配 BF算法(基本的模式匹配算法,布鲁特-福斯算法) KMP算法(改进的模式匹配算法) 3. 数组元素的顺序存储(按行存储、按列存储) 4. 特殊矩阵的压缩存储(对称矩阵、三角矩阵、对角矩阵) 5. 稀疏矩阵的压缩存储(三元组线性表)
j 0 1 2 3 4 模式串 a b c a c next[j] -1 0 0 0 1 -1 当 j==0, 表示与模式串第一个字符比较就失败,应使主串i指向下一字符再与模式串的第一字符比较(i++;j++)。 next[j] = Max{k|0<k<j 且‘t0...tk-1’==‘tj-k...tj-1’} 0 其他情况(j==1或之前没有匹配部分) KMP模式匹配算法中求模式串的next函数值
a11 a12… a1n a21 a22… a2n …… aij … am1 am2… amn Am×n= 数组的表示 • 行优先顺序或以行为主序存储方式:将数组元素按行排列,第i+1行的元素紧接在第i行元素之后。以二维数组为例,按行优先顺序存储的线性序列为: a11,a12,…,a1n,a21,a22,…a2n,……,am1,am2,…,amn • LOC(aij) = LOC(a11)+[(i-1)*n+j-1]*d
a11 a12… a1n a21 a22… a2n …… aij… am1 am2… amn Am×n= 数组的顺序表示 • 列优先顺序或以列为主序存储方式:将数组元素按列向量排列,第j+1个列向量紧接在第j个列向量之后,A的m*n个元素按列优先顺序存储的线性序列为: a11,a21,…,am1,a12,a22,…am2,……,an1,an2,…,anm • LOC(aij)=LOC(a11)+[(j-1)*m+i-1]*d
特殊矩阵的压缩存储 • 特殊矩阵是指非零元素或零元素的分布有一定规律的矩阵,为了节省存储空间,特别是在高阶矩阵的情况下,可以利用特殊矩阵的规律,对它们进行压缩存储,也就是说,使多个相同的非零元素共享同一个存储单元,对零元素不分配存储空间。 • 常见的特殊矩阵有对称矩阵、三角矩阵、对角矩阵等。
a1,1 a2,1 a2,2 … ai,j … an,1 an,2… an,n a1,1 a1,2… a1,n a2,1 a2,2… a2,n …… ai,j… an,1 an,2… an,n a1,1 a2,1 a2,2 a3,1 1 2 3 4 对称矩阵的压缩存储 • n*n的对称矩阵A元素压缩存储到一维数组B 对称矩阵 … ai,j … 数组B an,n 下标 k m k = i(i-1)/2 + j – 1 + 1 = i(i-1)/2+j
a0,0 a0,1 a1,0 a1,1 a1,2 a2,1 a2,2 a2,3 …. ai,j…. an-2,n-3 an-2,n-2 an-2,n-1 an-1,n-2 an-1, n-1 对角矩阵的压缩存储 • 对角矩阵是指所有的非零元素都集中在以主对角线为中心的带状区域中。下图为三对角矩阵示意图。 k = 3i - 1+ j– i +1 = 2i + j 数组B a0,0 a0,1 a1,0 ... ai,j ... 下标 0 1 2 k k+1 m