数据结构:要处理的信息
算法:处理信息的步骤
数据结构这门课着重关注的是数据元素之间的关系,和对这些数据元素的操作,而不关心具体的数据项内容。
数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。
数据元素是数据的基本单位,通常会作为一个整体进行考虑和处理。
一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。
数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
根据例子理解:假设有两张表,上表为人员表,下表为课程表, 表的格式如下:
这两张表就是数据
单独的一张表就称为数据对象,即人员表是一个数据对象,课程表也是一个数据对象。
每张表中的每一行就称为数据元素。
姓名,性别,身高,课程代号,课程名就称为数据项。
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
注:
1. 每个数据元素也是具有相同性质的
2. 同样的数据元素可以组成不同的数据结构
3. 不同的数据元素可以组成相同的数据结构
数据类型 :一个值的集合和定义在这个集合上的一组操作的总称。
一般包括整型、实型、字符型等原子类型外,还有数组、结构体和指针等结构类型。
当我们在使用一种具体的数据类型时,我们只关心这种数据类型在逻辑上呈现出来的一些特性,以及这种数据类型可以执行什么样的操作。
① 原子类型:其值不可再分的数据类型。
② 结构类型:其值可以再分解为若干成分 (分量) 的数据类型,且分量之间存在一定的逻辑关系。
抽象数据类型 (Abstract Data Type – ADT) : 抽象数据组织及与之相关的操作。
类似C语言中的结构体以及C++、java语言中的类。
数据结构的使用者需要关心:
-抽象数据组织:一个数据结构应该对外呈现出数据元素之间是怎么组织的,是什么样的结构关系
-相关的操作:对于这种数据结构可以执行什么样的操作
抽象数据类型(ADT)描述了数据的逻辑结构和抽象运算,通常用数据对象、数据关系和基本操作集这样的三元组来表示,从而构成一个完整的数据结构定义。
抽象数据类型是对一个数据结构的逻辑结构的描述,以及可以对这种数据结构进行怎样的运算的描述。
数据结构的使用者只需要知道一个数据结构的ADT即可,而数据结构的实现者还需要关注这个数据结构在计算机内部如何存储,以及各种操作在计算机底层的实现。
- 定义一个ADT,就是在“定义”一种数据结构
当我们定义了一种数据类型的时候,我们只是在逻辑层面描述了这个数据类型可以`取得什么值`,这些值之间有哪些`关系`,以及可以对这种数据类型进行什么样的`操作`,而这个数据类型在计算机内部是如何实现的,以及计算机底层是如何实现这些操作的,我们并不需要关心。
因此当我们在使用一种具体的数据类型时,我们只关心这种数据类型在逻辑上呈现出来的一些特性以及会关心这种数据类型可以执行什么样的操作。这是数据类型的使用者所关心的问题。
同样的,作为某一种数据结构的使用者,我们也只需要关心这种数据结构呈现出来的逻辑特性是什么,可以对这种数据结构进行哪些操作,而数据结构的使用者并不关心这个数据结构内部实现的细节是什么样的。
-确定了ADT的存储结构,才能“实现”这种数据结构
在任何问题中,数据元素都不是孤立存在的,它们之间存在某种关系,这种数据元素相互之间的关系称为结构。
由于信息可以存在于逻辑思维领域,也可以存在于计算机世界,因此作为信息载体的数据同样存在于两个世界中。表示一组数据元素及其相互关系的数据结构同样也有两种不同的表现形式:
数据结构的逻辑层面,即数据的逻辑结构;
存在于计算机世界的物理层面,即数据的存储结构。
数据机构包括三方面的内容:逻辑结构、存储结构和数据的运算。
运算的定义是针对逻辑结构的,指出运算的功能。
运算的实现是针对存储结构的,指出运算的具体操作步骤。
逻辑结构 + 数据的运算 = “定义”一种数据结构
物理结构(存储结构) = 用计算机“实现”这种数据结构
逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。它与数据的存储无关,是独立于计算机的。
数据的逻辑结构分为线性结构和非线性结构,线性表是典型的线性结构,集合、树和图是典型的非线性结构。
同一个逻辑结构可以对应多种存储结构。
集合: 各个数据元素除“同属一个集合”外,别无其他关系。
线性结构:数据元素之间是一对一的关系。
有且只有一个开始结点和一个终端结点,除了第一个元素,所有元素都有唯一前驱; 除了最后一个元素,所有元素都有唯一后继。
树形结构: 数据元素之间是一对多的关系。
除了最上面的数据元素以外每个数据元素有且仅有一个直接前驱元素,但是可以有多个直接后续元素。
网状/图状:数据元素之间是多对多的关系。
每个数据元素可以有多个直接前驱元素,也可以有多个直接后续元素。
存储结构:主要包括数据元素本身的存储以及数据结构在计算机中的表示,又称映像/物理结构。
数据的存储结构是数据的逻辑结构在计算机中的表示,它依赖于计算机语言。
无论使用哪种存储结构,这种存储的方式一定要能够反映出数据元素之间的逻辑关系。
采用不同的存储结构会影响数据运算的实现。
数据的存储结构主要有顺序结构、链式存储、索引存储和散列存储。
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
数据元素的存储对应于一块连续的存储空间,数据元素之间的前驱和后续关系通过数据元素在存储器中的相对位置来反映。
优点: 可以随机存取,每个元素占用最少的存储空间。
缺点: 只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片。
链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
数据元素的存储对应的是不连续的存储空间,每个存储节点对应一个需要存储的数据元素。每个结点是由数据域和指针域组成。元素之间的逻辑关系通过存储节点之间的链接关系反映。逻辑上相邻节点物理上不必相邻。
优点:不会出现碎片现象,可以充分利用所有存储单元。
缺点:每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取。
索引存储:在存储元素信息的同时,还建立附加的索引表来标识节点的位置。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。
优点:检索速度快。
缺点:附加的索引表额外占用存储空间;增加和删除数据也要修改索引表,会花费较多时间。
散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。
Java中的HashSet、HashMap底层就是散列存储结构。这是一种神奇的结构,添加、查询速度快。
优点:检索、增加和删除结点的操作都很快
缺点:若散列函数不好,可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。
针对于某种逻辑结构,结合实际需求,定义基本运算。
施加在数据上的运算包括运算的定义和实现。
运算的定义是针对逻辑结构的,指出运算的功能。
运算的实现是针对存储结构的,指出运算的具体操作步骤。
算法(Algorithm):对特定问题求解步骤的一种描述,是指令的有限序列,其中每条指令表示一个或多个操作。
算法可以用自然语言描述,也可以用代码或者伪代码来描述。
一个算法必须具有下列5个重要特性:
有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
注:算法是有穷的,程序是无穷的。
确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
可行性 :算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
正确性:算法应能够正确的解决问题。
可读性:算法应具有良好的可读性,以帮助人们理解。(例如添加注释)
健壮性:输入非法数据时,算法能适当的做出反应或进行处理,而不会产生莫名其妙的输出结果。
高效率和低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。
即算法执行省时、省内存
时间复杂度低、空间复杂度低
不懂的话可以查看:https://blog.csdn.net/qq_41523096/article/details/82142747?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160471359019724838561636%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=160471359019724838561636&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-2-82142747.pc_first_rank_v2_rank_v28&utm_term=%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6&spm=1018.2118.3001.4449
算法是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
那么我们应该如何去衡量不同算法之间的优劣呢?
主要从算法所占用的时间和空间两个维度来衡量:
- 时间维度: 指执行当前算法所消耗的时间,通常用「时间复杂度」来描述。
- 空间维度: 指执行当前算法需要占用多少内存空间,通常用「空间复杂度」来描述。
因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。
然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。
时间复杂度:当数据规模发生变化时,时间频度变化趋势。
空间复杂度:当数据规模发生变化时,计算占用空间的变化趋势。
让算法先运行,事后统计运行时间?当然可以,但是存在问题,因为无法排除与算法本身无关的外界因素:
--与算法本身无关的外界因素
1. 和机器性能有关,如:超级计算机 VS 单片机
2. 和编程语言有关,越高级的语言执行效率越低,如Java写的程序效率较低与C语言写的程序,因为Java是更高级的语言。
3. 和编译程序产生的机器指令质量有关
--能否事先估计?
4. 有的算法是不能时候再统计的,如:导弹控制算法
那么能否排除与算法本身无关的外界因素且实现事先估计?
代码都没有运行,我怎么知道代码运行所花费的时间呢?
由于运行环境和输入规模的影响,代码的绝对执行时间是无法估计的。但我们却可以预估出代码的基本操作执行次数。
时间复杂度:事前预估算法时间开销 T(n)
与问题规模 n
的**关系** ( T
表示 “time”
) 。
语句频度: 指该语句在算法中被重复执行的次数。
顺序执行的代码:不带有循环的代码 -->语句频度为常数
T(n):算法中所有语句的频度之和(简称时间频度),它是该算法问题规模n
的函数,时间复杂度主要分析 T(n) 的数量级。
注意:时间频度与时间复杂度是不同的,时间频度不同但时间复杂度可能相同
算法中基本运算(最深层循环内的语句)的频度和 T(n) 同数量级,因此经常采用算法中基本运算的频度f(n)来分析算法的时间复杂度。
算法的时间复杂度使用大O表示法记为: T(n) = O(f(n))
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
当数据规模产生变化时 时间频度的变化趋势的量级 可以理解为时间复杂度
示例:
void loveYou(int n) { //n:问题规模
int i = 1; //语句频度:1次
while(i <= n){ //语句频度:n + 1次
i++; //语句频度:n次
printf("I Love You", i); //语句频度:n次
}
printf("I Love More Than", n); //语句频度:1次
}
// T(n) = 1 + (n+1) + n + n + 1
// =>时间开销与问题规模n的关系:T(n) = 3n + 3 =O(3n + 3) = O(n);
是否可以简化表达式?
可以的!因为大O符号表示法并不是用于来真实代表算法的执行时间的,而是用来表示代码执行时间的增长变化趋势的。
当计算时间复杂度时,只需要关注最高阶的项,并将其系数化为1,并以大O记法表示:
时间复杂度就是时间频度去掉低阶项和首项常数
取f(n)中随n增长最快的项,将其系数置为1作为时间复杂度的度量。如:f(n)=an^3^ + bn^2^ + cn的时间复杂度为O(n^3^)。
当我们考虑一个算法的时间复杂度时,并不需要给出算法的时间开销T(n)和问题规模n之间的详细的表达式,只需要关注这个表达式的数量级。
顺序执行的代码只会影响常数项,可以忽略。
示例:
加法规则:多项相加,只保留最高阶 (数量级最大) 的项,且系数变为1
示例:
数量级:(常对幂指阶)
O(1)
无论代码执行了多少行,只要没有循环等复杂结构,那这个代码的时间复杂度就是O(1)。
int i = 1;
int j = 2;
i++;
j++;
int m = i + j;
O(logN)
//在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于等于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n,也就是说当循环 log2^n 次以后,这个代码就结束了。
//因此这个代码的时间复杂度为:O(logn)
int i = 1;
while(i < n)
{
i = i * 2;
}
int count = 0;
for(int i = 1; i <= n; i *= 2)
{
count++;
}
O(n)
一个循环的时间复杂度为 O(n)
//for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
O(nlogN)
线性对数阶O(nlogN)
其实非常容易理解,将时间复杂度为 O(logn)
的代码循环 N
遍的话,那么它的时间复杂度就是 n * O(logN)
,也就是了O(nlogN)
。
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。
int count = 0;
for(int i = 1; i <= n; i++)
{
for(j = 1; j <= n; j++)
{
count++;
}
}
这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²)
如果将其中一层循环的n改成m,即:
for(x = 1; x <= m; x++)
{
for(i = 1; i <= n; i++)
{
j = i;
j++;
}
}
乘法规则:多项相乘,都保留
示例:
一般主要关注在最坏时间复杂度和平均时间复杂度,以保证算法的运行时间不会比它更长。
//flag数组中乱序存放了1~n这些数
int flag[n] = {1...n};
test02(flag, n);
//从第一个元素开始查找元素n
void test02(int flag[], int n){
printf("Hello World!");
for(int i = 0; i < n; i++){
if(flag[i] == n){
printf("找到n啦!");
break;//找到后立即跳出循环
}
}
}
//计算上述算法的事件复杂度T(n):
//最好情况:元素n在第一个位置 最好时间复杂度T(n) = O(1)
//最坏情况:元素n在最后一个位置 最坏时间复杂度T(n) = O(n)
//平均情况:假设元素n在任意一个位置的概率相同为1/n 平均时间复杂度T(n) = O(n)
上述算法平均时间复杂度的计算:
最坏情况下算法的时间复杂度。
我们讨论的时间复杂度一般是最坏情况下的时间复杂度。因为最坏时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。例如:在最坏情况下的时间复杂度为T(n)=O(n),它表示对于任何输入实例,该算法的运行时间不可能大于O(n)。
Ο(欧米可荣)符号给出了算法时间复杂度的上界(最坏情况 <=),比如T(n) =O(n2)
所有输入示例等概率出现的情况下,算法的期望运行时间。
鉴于平均复杂度 第一,难计算 第二,有很多算法的平均情况和最差情况的复杂度是一样的。 所以一般讨论最坏时间复杂度
最好情况下算法的时间复杂度。
Ω(欧米伽)符号给出了时间复杂度的下界(最好情况 >=),比如T(n) =Ω(n2)
1. 忽略顺序执行的代码(只会影响常数项),只关注循环中的语句
2. 只需挑最深处循环(最高阶)中的一个基本操作(系数化为1)分析它的执行次数与n的关系即可
1. 找到一个基本操作(最深层循环)
2. 分析该基本操作的执行次数x与问题规模n的关系 x=f(n)
3. x的数量级O(x)就是算法时间复杂度T(n)
计算以下算法的时间复杂度T(n)
void test01(){
int i = 1;
while(i <= n){ //n为问题规模
i = i * 2;
printf("i = %d", i);
}
printf("n = %d", n);
}
解:设最深层循环的语句频度为x,则由循环条件可知,循环结束时刚好满足2x > n,即x = log2n + 1
则有:T(n) = O(x) = O( log2n)
空间开销(内存开销)与问题规模 n 之间的关系
算法的存储量包括:
1.程序本身所占空间 --大小固定,与问题规模无关
2.输入数据所占空间
3.辅助变量所占空间
输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。
空间复杂度是对一个算法在运行过程中临时占用的存储空间大小的量度,是问题规模n的函数,记作:S(n) = O(g(n))
一个程序在执行时除需要存储空间来存放本身所用的指令、常熟、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,与算法无关,则只需分析除输入和程序之外的额外空间。
算法原地工作是指算法所需的辅助空间为常量,即O(1)。
//示例:
void test01(int n){
int i = 1;
while(i <= n){ //n为问题规模
i++;
printf("i = %d", i);
}
printf("n = %d", n);
}
//假设算法本身大小为100B;输入数据n的大小为4B;辅助变量i的大小为4B
//无论问题规模怎么变,算法运行所需的内存空间都是固定的常量: 100B + 4B + 4B = 108B,算法空间复杂度为S(n) = O(1)
计算空间复杂度只需关注存储空间大小与问题规模相关的变量。
//示例一:
void test(int n){
int flag[n];//声明一个长度为n的数组
int i;
//...此处省略很多代码
}
//假设一个int变量占4B,则上述代码所需内存空间为:4 + 4n + 4 = 4n + 8,算法空间复杂度为S(n) = O(n)
//示例二:
void test02(int n){
int flag[n][n];//声明一个n*n的二维数组
int i;
//...此处省略很多代码
}
//算法空间复杂度为S(n) = O(4n^2) + O(1) = O(n^2)
//示例三:
void test03(int n){
int flag[n][n];//声明一个n*n的二维数组
int other[n];//声明一个长度为n的数组
int i;
//...此处省略很多代码
}
//算法空间复杂度为S(n) = O(4n^2) + O(n) + O(1) = O(n^2)
//示例四:函数递归调用的内存开销
void test04(int n){
int a,b,c;//声明一系列局部变量
//...此处省略很多代码
if(n > 1){
test04(n - 1);
}
printf("n = %d", n);
}
//每次调用该函数时,参数n和局部变量a,b,c占用存储空间是固定的:16B
//算法空间复杂度为S(n) = O(n * 16B) = O(n)
//因为每一层的递归调用所需要的内存空间大小都是一个常量kB,故:空间复杂度 = 递归调用的深度!!!
//示例五:
void test05(int n){
int flag[n];//声明一个数组
//...此处省略数组初始化代码
if(n > 1){
test05(n - 1);
}
printf("n = %d", n);
}
//计算结果如下: