第一讲 时空复杂度:
算法的概念及其特性
1.1.1算法的定义
**算法(algorithm):**是指在解决问题时,按照某种机械的步骤一定可以得到问题的结果(有解时给出问题的解,无解时给出无解的结论)的处理过程。当面临某个问题时,需要找到用计算机解决这个问题的方法和步骤,算法就是解决这个问题的方法和步骤的描述。
所谓机械步骤是指,算法中有待执行的运算和操作,必须是相当基本的。换言之,它们都是能够精确地被计算机运行的算法,执行者(计算机)甚至不需要掌握算法的含义,即可根据该算法的每一步骤,进行操作并最终得出正确的结果。
“算法”这个词其实并不是一个陌生的词,因为从小学大家就开始接触算法了。例如做四则运算,必须按照一定的算法步骤一步一步地做。“先运算括号内再运算括号外,先乘除后加减”可以说是四则运算的算法。以后学习的指数运算、矩阵运算和其他代数运算的运算规则都是一种算法。
就本课程而言,算法就是计算机解决问题的过程。在这个过程中,无论是形成解决问题思路还是编写算法,都是在实施某种算法。前者是推理实现的算法,后者是操作实现的算法。
1.1.2算法的3要素
算法由操作、控制结构、数据结构3要素组成。
1、操作
算法实现平台尽管有许多种类,它们的函数库、类库也有较大差异,但必须具备的最基本的操作功能是相同的。这些操作包括以下几个方面。
算术运算:加、减、乘、除。
关系比较:大于、小于、等于、不等于
逻辑运算:与、或、非
数据传送:输入、输出、赋值(计算)。
2、算法的控制结构
一个算法功能的实现不仅取决于所选用的操作,还取决于各操作之间的执行顺序,即控制结构。算法的控制结构给出了算法的框架,决定了各操作的执行次序。这些结构包括以下几个方面。
顺序结构:各操作是依次进行的。
选择结构:由条件是否成立来决定选择执行。
循环结构:有些操作要重复执行,直到满足某个条件时才结束,这种控制结构也称为重复或迭代结构。
3、数据结构
算法操作的对象是数据,数据间的逻辑关系、数据的存储方式及处理方式就是数据结构。它与算法设计是紧密相关的。在后面的具体案例分析讲解中来进行描述。
1.1.3算法的基本性质
进一步理解,算法就是把人类找到的求解问题的方法,经过过程化、形式化后,用以上的要素表示出来。在算法的表示中要满足以下的性质:
目的性—-算法有明确的目的,算法能完成赋予它的功能。
分步性—-算法为完成其复杂的功能,由一系列计算机可执行的步骤组成。
有序性—-算法的步骤是有序的,不可随意改变算法步骤的执行顺序。
有限性—-算法是有限的指令序列,算法包含的步骤是有限的。
操作性—-有意义的算法总是对某些对象进行操作,使其改变状态,完成其功能。
1.1.4 算法的基本特征
并不是所有问题都有可以解决它们的方法,也不是所有人类解决问题的方法都能设计出相应的算法。算法必须满足以下5个重要特性。
1、有穷性
一个算法在执行有穷步骤之后必须结束,也就是说一个算法它所包含的计算步骤是有限的,即算法中的每个步骤都能在有限时间内完成。
2、确定性
对于每种情况下所应执行的操作,在算法中都有确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件下,算法都只有一条执行路径。
3、可行性
算法中描述的操作都可以通过已经实现的基本操作运算有限次实现之。
4、算法有零各或多个的输入
有输入作为算法加工对象的数据,通常体现为算法中的一组变量。有些输入量需要在算法执行过程中输入,而有的算法表面上可以没有输入,实际上已被嵌入算法之中。
5、算法有一个或多个的输出
它是一组与输入有确定关系的量值,是算法进行信息加工后得到的结果。
12时间复杂度
计算机的运行能力估计:
做一个简单实验,让程序循环1亿次,10亿次看看需要多少时间。
可以看出,一般电脑在1亿次可在1秒内完成,超过10亿次在1秒内难以完成。
时间复杂度估算方法
- 简单语句
简单的赋值语句,读写语句,可以看作所用时间为常量记为O(1)。
- 分支语句
在分支语句中,以所耗费时间最多的那个分支来计算时间复杂度。
- 循环语句
在循环语句中,每循环1次记为O(1),循环n次时间复杂度为O(n)。
- 嵌套循环
在嵌套循环中,时间复杂度是多个循环的叠加,如下所示:n2
| for(int i=1;i<=n;i++)for(int j=1;j<=n;j++) ……. | O(n*n)=O(n2) |
| for(int i=1;i<=n;i++)for(int j=1;j<=n;j++) for(int k=1;k<=n;i++) ……. | O(n*n)=O(n3) |
| for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){…..} for(int k=1;k<=n;i++){ …….}} | O(n*n)=O(2*n2) |
_1.****3****空间复杂度_
我们编写的程序运行时的代码和数据等信息,保存在我们的内存中,我们的内存到底能装多少数据,这个与计算机内存大小,运行其它程序的多少有关。一般来讲全局数据内存大可达到(亿级个整数),局部变量内存较少可达到(千万级个整数)。例如下程序:
| int a[100000000]int dis(){ int b[10000000];} | //全局变量数组 //局部变量数组 |
同学们可以初步实验一下自己计算机的存储能力,为后续设计算法做好准备。
数据结构概念
在计算机内存中,不仅存放整数,它可以存放各种不同的数据类型,例如:
| struct student{ int num; char name[10]; int age; …..} a,b, c[100]; |
上面定义的学生结构体中:
student:是一个整体,是计算机的基本存储单元,称为数据元素。
其中num,name,age等 称为一个个数据项
a,b,c[100]等称为具体的数据对象
数据结构中的逻辑结构与物理结构:
在计算机中所有数据在内存都是以0和1的方式保存,本质上没有任何区别,只是为了使程序员设计算法的需要,把这些数据按物理和逻辑上进行分类。
物理结构:是指数据实实在在的存储在内存的方式。一种是按地址连续不断的进行存储,称为顺序存储结构。另一种,数据存储随意,而用其它方式(例如指针)使得这些数据关联起来,称为链式存储结构。无论那种存储结构,数据就是数据,是客观存在的,不带有任何主观意识。
如果把周末在解放碑闲逛的人看作是一个个数据,那么人就是人,不存在任何关系,如果让他们排队去看电影,一个紧靠一个,就构成了顺序存储结构,如果让他们拿号去取钱,人的位置可以随意走动,就构成了链式存储结构。
逻辑结构:就是在开发人员的脑海里,主观地认为这些数据存在某些关联,以实现某种要求,而而认为这些数据存在的结构。如上面的人,无论是排队看电影还是拿号取钱,都依照一定的顺序,存在一对一的关系,这叫线性结构,若是一个家庭在逛解放碑,这样父亲—>儿子—>孙子,存在一对多的关系,这叫树形结构。如果是一个班的同学在迋解放碑,这就构成多对多的关系,这叫图形结构。
所以,物理结构是客观存在的东西,逻辑结构是认为强加的意思,是算法设计与实现不可缺少的部分。
_1.****4****内存管理机制_
程序运行将计算机内存分为如下四个区域:
- 程序代码区:运行程序本身所需要的存储空间,它与程序规模大小有关,一旦程序运行,不能进行动态管理,除非进程杀掉它。
- 全局数据区:用以存放全局变量、静态变量和常量的地方,生命周期与程序一致,即程序运行它就会占用空间,直到程序结束。
- 栈区:存放局部变量和形参变量的地方,在函数调用时自动分配,调用完毕后会自动释放,包括主函数。
- 堆区:动态分配的指针变量,这需要开发人员手动和手动释放。
_1.****5****函数调用机制_
主调函数调用被调函数
- 主动权给被调函数
- 为形参和局部变量分配内存
- 实参与形参匹配传值
被调函数返回主调函数
- 主动权返回主调函数
- 计算结果返回
- 释放形参和局部变量内存空间
四种交换函数(了解)
下面四个交换函数,在函数进行调用用以实现数据交换,其结果为:
| void swap1(int a,int b){ int c=a; a=b,b=c;} | 不能实现主函数数据交换:在被调函数内实现了交换,但在函数调用结束后被释放了。 |
| void swap2(int &a,int &b){ int c=a; a=b,b=c;} | 能实现交换,将主函数的变量作为副本传入,形参与实参是统一个地址。 |
| void swap3(int *a,int *b){ int *c=a; a=b,b=c;} | 不能,函数内实现了指针的交换,而指针所指向地址的值没变,而a,b,c释放并没影响原有地址内容。 |
| void swap4(int *a,int *b){ int c=*a; *a=*b,*b=c;} | 能实现交换,实现的是指针所指向地址的值进行了交换。 |
循环与递归的时空复杂度
循环1亿次比递归1亿次要快,主要在于函数调用和形参、局部变量分别要花去时间,但都能在1秒内完成。
求1到n的和,循环和递归实现。
int sum(int n){
if(n==1)return 1;
return sum(n-1)+n;
}
当n=100000时,内存溢出。递归调用层次不能过多,它使用的是栈区内存。
_1.****6****算法设计的步骤:_
1、认真读取题目,理解含义,注意细节。
2、根据题目初步确定算法。
3、根据初步算法,进行数据结构的设计。
4、查看数据规模,衡量算法的时间复杂度和空间复杂度,从而确定初步算法是否可行,若不行,从新确定算法。
5、编程实现代码。
6、进行测试,注意边界条件,(数据类型等)
_1.****7****案例讲解(****时空复杂度****)_
1.7.1 质数问题
有m组数据,每组数据有n个小于1000的正整数,分别求每组数据中所有素数的和。其中1<=m<=10;1<=n<=1000;
【算法一】
| #include<stdio.h>#include <stdlib.h> int main(){ int m,n; scanf(“%d”,&m); for(int i=1;i<=m;i++){ int sum=0; scanf(“%d”,&n); for(int j=1;j<=n;j++){ int x; scanf(“%d”,&x); for(int k=2;k<x;k++){ if(x%k==0) break; } if(k==x) sum=sum+x; } printf(“%d \n”,sum); }} | 该算法直接的打表算法,时间复杂度为: 10*1000*1000, 即千万级不会超时 |
如果m为1000以内,n为10000以内,而每个整数可达到1000000呢?若仍用上面算法,时间复杂度可达到1013次。无法仍受。
【算法二】
分析:先用a[1000000]的数组进行预处理,存放1百万内的所有质数,若为质数其值为1,和数其值为0,先对前十个进行处理,11后去奇数,寻找每个数的因子时从3开始取奇数,时间复杂度将小于500000*500即2.5亿。
而后计算求和时可以用sum=sum+k*a[k]得到,因为k为合数 a[k]=0,k为质数a[k]=1, 时间复杂度为千万级。
| #include<stdio.h>#include <stdlib.h> int a[1000001]={0,0,1,1,0,1,0,0,0};int main(){ int i,j,k,n,m,sum; for(int j=11;j<=1000000;j=j+2){ for(i=3;i*i<=j;i=i+2){ if(j%i==0) break; } if(i*i>j) a[j]=1; } scanf(“%d”,&m); for(i=1;i<=m;i++){ scanf(“%d”,&n); for(int j=1,sum=0;j<=n;j++){ scanf(“%d”,k); sum=sum+k*a[k]; } printf(“%d\n”,sum); }} |
【算法三】
欧拉算法求质数:(时间复杂度 O(n))【了解】
| using namespace std;int a[1000000]={0};int b[2050]={0};int main(){ int i,j,k=0; for(i=2; ;i++){ if(!a[i]) b[k++]=i; for(int j=0;j<k &&i*b[j]<=1000000;j++){ a[i*b[j]]=1; if(i%b[j]==0) break; } if(k==2019) break; } for(i=1;i<=k;i++){ cout<<b[i-1]<<” “; if(i%10==0) cout<<endl; }} |
1.7.2珠心算测验 P42017
【问题描述】
珠心算是一种通过在脑中模拟算盘变化来完成快速运算的一种计算技术。珠心算训练, 既能够开发智力,又能够为日常生活带来很多便利,因而在很多学校得到普及。
某学校的珠心算老师采用一种快速考察珠心算加法能力的测验方法。他随机生成一个正整数集合,集合中的数各不相同,然后要求学生回答:其中有多少个数,恰好等于集合中另外两个(不同的)数之和?
最近老师出了一些测验题,请你帮忙求出答案。
【输入】
输入共两行,第一行包含一个整数 n,表示测试题中给出的正整数个数。
第二行有 n 个正整数,每两个正整数之间用一个空格隔开,表示测试题中给出的正整数。
【输出】
输出共一行,包含一个整数,表示测验题答案。
【输入输出样例】
【样例说明】 由 1+2=3,1+3=4,故满足测试要求的答案为 2。注意,加数和被加数必须是集合中的 两个不同的数。
【数据说明】
对于 100%的数据,3 ≤ n ≤ 100,测验题给出的正整数大小不超过 10,000。
【算法一】
应用i,j,k三个变量进行三重循环,i从1到100,j从i+1到100,k从j+1到100,寻找a[i]=a[j]+a[k]即可,总的运行时间的复杂读为百万级。代码实现如下:
| #include<stdio.h>#include <stdlib.h> int a[101]={0};int n,ans=0;int main(){ int i,j,k; scanf(“%d”,&n); for(i=1;i<=n;i++ ){ scanf(“%d”,&a[i]); } for(i=1;i<=n;i++){ bool flag=0; for(j=i+1;j<=n;j++){ for(k=j+1;k<=n;k++){ if(a[i]==(a[j]+a[k])){ flag=1;break; } } if(flag==1){ ans++;break; } } } printf(“%d\n”,ans); } |
【算法二】
如果将数据量从100变为10000,测试数据大小从10000变为1000000,那么使用上面的算法,时间复杂度将达到1012。无法忍受,我们可以定义a[1000001]的数组,用下标表示所要输入的数字,其值为1记录所输入的数字,为0表示未输入该数字。这样可以牺牲空间换取时间,判断一个整数i是否有其它两个整数的和,则变为:i从达到小循环,j从i-1到i/2循环,只需判定a[i]是否为1,a[j]+a[i-j]是否为2,即i,j,i-j都是输入的数字。时间复杂度小于1010。参考代码如下:
| #include<stdio.h>#include <stdlib.h> int a[1000001]={0};int n;int main(){ int i,j,x,ans=0; scanf(“%d”,&n); for(i=1;i<=n;i++){ scanf(“%d”,&x); a[x]++; } for(i=1000000;i>0;i–){ if(a[i]==0)continue; for(j=i-1;j>i/2;j–){ if(a[j]+a[i-j]==2){ ans++; break; } } } printf(“%d\n”,ans);} |
_1.****8****思考题:_
1.8.1清单的奸细
【问题描述】
我党有N名(保证N为偶数)特工,这其中混入了2名奸细,谁也不知道是哪个。好在真正的特工都有自己的编号,并且编号都是成对出现的,既2名真正特工拥有相同编号,而那2名奸细只能各自瞎编一个编号。
【输入格式】
2行
第1行包含一个整数N(保证为偶数,0 < N <= 1000000)
第2行包含N个整数(每个整数小于10000000),代表每个人的编号,空格隔开。
【输出格式】
2名奸细的编号(顺序和输出一致),空格隔开
【样例输入】
6
54321 34 12345 54321 33 12345
【样例输出】
34 33
_1****.****9 A****CM集训题库习题_
【练习1】求完全平方数的个数 P12016
【问题描述】
输入一个正整数n,求1~n中某个数m,m本身是完全平方数,其各个数值之和也是
完全平方数,输出满足这两个条件的正整数的个数。
【输入格式】:
一个正整数n
【输出格式】:
输出满足条件的完全平方个数
【样例输入】
1
【样例输出】
1
【说明】
数据规模:
1<n<100000
**【练习2】**数字统计 P42001
【问题描述】
请统计某个给定范围[L, R]的所有整数中,数字2出现的次数。比如给定范围[2, 22],数字2在数2中出现了1次,在数12中出现1次,在数20中出现1次,在数21中出现1次,在数22中出现2次,所以数字2在该范围内一共出现了6次。
【输入格式】
输入共1行,为两个正整数L和R,之间用一个空格隔开。
【输出格式】
输出共1行,表示数字2出现的次数。
【样例输入1】
2 22
【样例输出1】
6
【样例输入2】
2 100
【样例输出2】
20
【说明】
【数据范围】
1≤L≤R≤10000。
**【练习3】**记数问题 P42013
【问题描述】
试计算在区间1到n的所有整数中,数字x(0 ≤ x ≤ 9)共出现了多少次?例如,在1到11中,即在1、2、3、4、5、6、7、8、9、10、11中,数字1出现了4次。
【输入格式】
输入共 1 行,包含 2 个整数 n、x,之间用一个空格隔开。
【输出格式】
输出共 1 行,包含一个整数,表示 x 出现的次数。
【样例输入1】
11 1
【样例输出1】
4
【说明】
对于 100%的数据,1≤ n ≤ 1,000,000,0 ≤ x ≤ 9。
**【练习4】**卡片 LQB2021_A P60036
【问题描述】
小蓝有很多数字卡片,每张卡片上都是数字0到9。
小蓝准备用这些卡片来拼一些数,他想从1开始拼出正整数,每拼一个, 就保存起来,卡片就不能用来拼其它数了。
小蓝想知道自己能从1拼到多少。
例如,当小蓝有30张卡片,其中0到9各3张,则小蓝可以拼出1到10, 但是拼11时卡片1巳经只有一张了,不够拼出11。
现在小蓝手里有0到9的卡片各2021张,共20210张,请问小蓝可以从1 拼到多少?
提示:建议是用计算机编程解决问题。
【答案提交】
这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为- 个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
【练习5】 P60026 门牌制作 LQB2020-A
【问题描述】
小蓝要为一条街的住户制作门牌号。这条街一共有2020位住户,门牌号从1到2020编号。小蓝制作门牌的方法是先制作0到9这几个数字字符,最后根据需要将字符粘贴到门牌上,例如门牌1017需要依次粘贴字符1、0、1、7,即需要1个字符0,2个字符1,1个字符7。请问要制作所有的1到2020号门牌,总共需要多少个字符2?
【答案提交】这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 25 天,点击查看活动详情