0.写在前面
关于本文,被收集在我的专栏《数据结构与算法教程笔记》中,笔记整理来自李春葆老师的《数据结构教程第六版》。更多章节可参考该专栏。【该专栏目前处于持续更新状态 -2022/9/11 】
1.数据结构总览
1.1 内容
1.2 数据结构在计算机课程体系(偏软)中的地位
1.3 数据结构与程序设计类课程的关系
1.4 数据结构的学习目标
-
掌握数据结构的基本概念、基本原理和基本方法。
-
掌握数据的逻辑结构、存储结构及基本运算的实现过程。
-
掌握算法基本的时间复杂度与空间复杂度的分析方法,能够设计出求解问题的高效算法。
✏️ 同一求解问题通常有多种实现算法,通过时间复杂度与空间复杂度的分析,找出最好的实现算法。
1.5 数据结构的学习方法
-
理解各种数据结构的逻辑特性和存储结构设计
-
掌握各种数据结构算法设计的基本方法
✏️ 只有掌握了数据的存储结构表示,才能在此之上设计算法
-
利用各种数据结构来求解实际问题
-
演绎和归纳相结合
2.什么是数据结构
2.1 数据结构的定义
2.1.1 数据结构中的几个概念
-
数据:所有能够输入到计算机中,且能被计算机处理的符号的集合。数据结构中主要讨论结构化数据。
-
数据元素:是数据(集合)中的一个“个体”,它是数据的基本单位。
-
数据项:数据项是用来描述数据元素的,它是数据的最小单位。
-
数据对象:具有相同性质的若干个数据元素的集合,如整数数据对象是所有整数的集合。默认情况下,数据结构中讨论的数据都是数据对象。
-
数据结构:是指带结构的数据元素的集合。
2.1.2 一个数据结构的构成
- 数据的逻辑结构:数据元素之间的逻辑关系
- 数据的存储结构(或物理结构):数据元素及其关系在计算机存储器中的存储方式
- 数据运算:施加在该数据上的操作
2.1.2.1 数据的逻辑结构表示
-
数据的逻辑结构是面向用户的,它有多种表示形式。
-
二元组是一种通用的逻辑结构表示方法。
-
每个关系的用若干个序偶来表示
2.1.2.2 数据的存储结构表示
1️⃣ 数据在计算机存储器中的存储方式就是存储结构。它是面向程序员的。逻辑结构映射存储结构。
2️⃣ 设计存储结构的这种映射应满足两个要求:
-
存储所有元素
-
存储数据元素间的关系
-
结构体数组(顺序存储结构)
- 所有元素占用一整块内存空间
- 逻辑上相邻的元素,物理上也相邻。
-
链表(链式存储结构)
- 一个逻辑元素用一个结点存储,每个结点单独分配,所有结点的地址不一定是连续的。
- 用指针来表示逻辑关系。
2.1.2.3 数据运算
数据运算是对数据的操作。分为两个层次:运算描述 和 运算实现。
- 同一逻辑结构可以对应多种存储结构。
- 同样的运算,在不同的存储结构中,其实现过程是不同的
2.2 逻辑结构类型
各种各样的数据呈现出不同的逻辑结构,归纳为4种。
2.2.1 集合
- 元素之间关系:无。
- 特点:数据元素之间除了“属于同一个集合”的关系外,别无其他逻辑关系。是最松散的,不受任何制约的关系。
2.2.2 线性结构
- 元素之间关系:一对一。
- 特点:开始元素和终端元素都是唯一的,除此之外,其余元素都有且仅有一个前驱元素和一个后继元素。
2.2.3 树形结构
- 元素之间关系:一对多。
- 特点:开始元素唯一,终端元素不唯一。除终端元素以外,每个元素有一个或多个后继元素;除开始元素外,每个元素有且仅有一个前驱元素。
2.2.4 图形结构
- 元素之间关系:多对多。
- 特点:所有元素都可能有多个前驱元素和多个后继元素。
2.3 存储结构类型
在软件开发中,人们设计了各种存储结构。 归纳为4种基本的存储结构。
- 顺序存储结构
- 链式存储结构
- 索引存储结构
- 哈希(散列)存储结构
2.4 数据类型和抽象数据类型
2.4.1 数据类型
1️⃣ 在高级程序语言中提供了多种数据类型。不同数据类型的变量,其所能取的值的范围不同,所能进行的操作不同。
2️⃣ 数据类型 是一个值的集合和定义在此集合上的一组操作的总称
3️⃣ 数据类型和数据结构的关系:数据类型就是已经实现了的数据结构。
2.4.2 抽象数据类型
1️⃣ 抽象数据类型(ADT)指的是从求解问题的数学模型中抽象出来的数据逻辑结构和运算(抽象运算),而不考虑计算机的具体实现。
2️⃣ 抽象数据类型 = 逻辑结构 + 抽象运算
3️⃣ 抽象数据类型实质上就是对一个求解问题的形式化描述(与计算机无关),程序员可以在理解基础上实现它。
3.算法及其描述
3.1 什么是算法
数据元素之间的关系有逻辑关系和物理关系,对应的运算有基于逻辑结构的运算描述和基于存储结构的运算实现。
1️⃣ 数据是各种信息的表现形式
2️⃣ 算法表现为“处理”和“数据”的结合
通常把基于存储结构的运算实现的步骤或过程称为算法。更一般地,算法
是 解决问题的处理步骤
✏️ 算法的五个重要的特性
有穷性
:在有穷步之后结束,算法能够停机。确定性
:无二义性。可行性
:可通过基本运算有限次执行来实现,也就是算法中每一个动作能够被机械地执行。输入
:0个或多个输入【表示存在数据处理】输出
:1个或多个输出【表示存在数据处理】
✏️ 算法和程序的区别
-
程序
是指使用某种计算机语言对一个算法的具体实现,即具体要怎么做。程序不一定满足
有穷性
-
算法
侧重于对解决问题的方法描述,即要做什么。算法一定满足
有穷性
-
算法用计算机语言描述 --> 程序
3.2 算法描述
❓ 如何描述输出型参数
📍 C++语言中提供了一种引用运算符“&”用于描述输出型参数。
✏️ 算法可以采用自然语言
、流程图
或者表格
方式等来描述。但是,一个学习计算机的学生应该使用某种计算机语言来描述算法。
4.算法分析
4.1 算法分析概述
算法分析目的:分析算法的时空效率以便改进算法性能。
4.2 算法时间复杂度分析
- 一个算法是由
控制结构
(顺序、分支和循环三种)和原操作
构成的。顺序结构
:按照所述顺序处理
分支结构
:根据判断条件改变执行流程
循环结构
:当条件成立时,反复执行给定的处理操作- 指固有数据类型的操作,如+、-、*、/、++和–等
- 算法执行时间取决于两者的综合效果。
4.2.1 算法分析方式
-
事后分析统计方法
:编写算法对应程序,统计其执行时间。编写程序的语言不同,执行程序的环境不同以及其它因素造成不能用绝对执行时间进行比较。
-
🚩
事前估算分析方法
:撇开上述因素,认为算法的执行时间是问题规模n的函数。
4.2.2 分析算法的执行时间
- 求出算法所有原操作的执行次数(也称为频度),它是问题规模n的函数,用T(n)表示。
- 算法执行时间大致 = 原操作所需的时间×T(n)。所以T(n)与算法的执行时间成正比。为此用T(n)表示算法的执行时间。
- 比较不同算法的T(n)大小得出算法执行时间的好坏。
4.2.3 算法的执行时间用时间复杂度来表示
算法中执行时间T(n)是问题规模n的某个函数f(n),记作:T(n) = O(f(n))
记号“O”读作“大O
”,它表示随问题规模n的增大算法执行时间的增长率和f(n)的增长率相同
--> 趋势分析
也就是只求出T(n)的最高阶,忽略其低阶项和常系数,这样既可简化T(n)的计算,又能比较客观地反映出当n很大时算法的时间性能。
本质上讲,是一种T(n)最高数量级的比较。
例如:T(n) = 2 n 2 n^2 n2+2n+1 = O( n 2 n^2 n2)
📝 一般地:
- 一个没有循环的算法的执行时间与问题规模n无关,记作O(1),也称作
常数阶
。 - 一个只有一重循环的算法的执行时间与问题规模n的增长呈线性增大关系,记作O(n),也称
线性阶
。 - 其余常用的算法时间复杂度还有
平方阶O
( n 2 n^2 n2)、立方阶
O( n 3 n^3 n3)、对数阶
O( l o g 2 n log_2n log2n)、指数阶
O( 2 n 2^n 2n)等。
各种不同算法时间复杂度的比较关系如下:
算法时间性能比较:假如求同一问题有两个算法:A和B,如果算法A的平均时间复杂度为O(n),而算法B的平均时间复杂度为O( n 2 n^2 n2)。 一般情况下,认为算法A的时间性能好比算法B。
某算法的时间复杂度为O( n 2 n^2 n2),表明该算法的执行时间与 n 2 n^2 n2成正比。
4.2.4 简化的算法时间复杂度分析
4.2.5 最好、最坏和平均时间复杂度分析
4.2.6 算法空间复杂度分析
空间复杂度
:用于量度一个算法运行过程中临时占用的存储空间大小(算法中需要的辅助变量所占用存储空间的大小)。一般也作为问题规模n的函数,采用数量级形式描述,记作:S(n) = O(g(n))
若一个算法的空间复杂度为O(1)
,则称此算法为原地工作
或就地工作
算法。该算法执行所需辅助空间大小与问题规模n无关。
❓ 为什么空间复杂度分析只考虑临时占用的存储空间
⭐️ 分析如下算法的空间复杂度
// 临时占用的存储空间:函数体内分配的空间
int fun(int n){
int i,j,k,s;
s=0;
for (i=0;i<=n;i++)
for(j=0;j<=i;j++)
for (k=0;k<=j;k++)
s++;
return(s)
}
📝 算法中临时分配的变量个数与问题规模n无关,所以空间复杂度均为O(1)
5.数据结构+算法=程序
5.1 程序和数据结构
- 程序总是以某些数据为处理对象。将松散、无组织的数据按某种要求组成一种数据结构,对于设计一个简明、高效、可靠的程序是大有益处的。
- 沃思:程序就是在数据的某些特定的表示方法和结构的基础上,对抽象算法的具体表述,所以说程序离不开数据结构。
- 程序是通过某种程序设计语言描述的,程序设计语言具有实现数据结构和算法的机制,其中类型声明与对象定义用于实现数据结构,而语句实现实现算法,描述程序的行为。
5.2 算法和程序
- 由程序设计语言描述的算法就是计算机程序。
- 对一个求解问题而言,算法就是解题的方法,没有算法,程序就成了无本之末,无源之水,有了算法,将它表示成程序是不困难的。
- 算法是程序的灵魂。算法在整个计算机科学中的地位都是极其重要的。
5.3 算法和数据结构
✏️ 数据结构角度求解问题的过程
- 问题求解 --> 算法设计
- 算法实现 --> 选择合适的存储结构
- 存储结构设计取决于数据的逻辑结构,并且为运算高效实现服务。
- 程序员可以直接使用它来存放数据 —— 作为存放数据的容器。
- 程序员可以直接使用它的基本运算 —— 完成更复杂的功能。
5.4 数据结构的发展
6.总结
6.1 从数据结构角度求解问题的过程
✏️ 设计集合的顺序存储结构类型如下:
typedef struct //集合结构体类型
{
int data[MaxSize]; //存放集合中的元素,其中MaxSize为常量【静态分配方式】
int length; //存放集合中实际元素个数
} Set; //将集合结构体类型用一个新类型名Set表示
✏️ 采用Set类型的变量存储一个集合
- createset(Set &s,int a[],int n):创建一个集合
- dispset(Set s):输出一个集合
- inset(Set s,int e):判断e是否在集合s中
- add(Set s1,Set s2,Set &s3):求集合的并集
- sub(Set s1,Set s2,Set &s3):求集合的差集
- intersection(Set s1,Set s2,Set &s3):求集合的交集
//创建一个集合
void createset(Set &s,int a[],int n)
{
int i;
for (i=0;i<n;i++)
s.data[i]=a[i];
s.length=n;
}
//输出一个集合
void dispset(Set s)
{
int i;
for (i=0;i<s.length;i++)
printf("%d ",s.data[i]);
printf("\n");
}
//判断e是否在集合s中
bool inset(Set s,int e)
{
int i;
for (i=0;i<s.length;i++)
if (s.data[i]==e)
return true;
return false;
}
//求集合的并集
void add(Set s1,Set s2,Set &s3)
{
int i;
for (i=0;i<s1.length;i++) //将集合s1的所有元素复制到s3中
s3.data[i]=s1.data[i];
s3.length=s1.length;
for (i=0;i<s2.length;i++) //将s2中不在s1中出现的元素复制到s3中
if (!inset(s1,s2.data[i]))
{
s3.data[s3.length]=s2.data[i];
s3.length++;
}
}
//求集合的差集
void sub(Set s1,Set s2,Set &s3)
{
int i;
s3.length=0;
for (i=0;i<s1.length;i++) //将s1中不出现在s2中的元素复制到s3中
if (!inset(s2,s1.data[i]))
{
s3.data[s3.length]=s1.data[i];
s3.length++;
}
}
//求集合的交集
void intersection(Set s1,Set s2,Set &s3)
{
int i;
s3.length=0;
for (i=0;i<s1.length;i++) //将s1中出现在s2中的元素复制到s3中
if (inset(s2,s1.data[i]))
{
s3.data[s3.length]=s1.data[i];
s3.length++;
}
}
6.2 算法描述―输出型参数
返回值 函数名(输入参数, 输出参数) //输出参数 --> 采用引用类型参数
{
//实现代码;
}