数据:数据是信息的载体,是描述客观事物属性的数字符以及所有能够输入到计算机中并被计算机识别和处理的符号的集合
数据元素:数据元素是数据的基本单位,通常作为一个整体来进行考虑和处理。数据元素可由若干个数据项组成。
数据项:数据项是数据记录中最基本的、不可分的有名数据单位,是具有独立含义的最小标识单位。
数据对象:数据对象是具有相同性质的数据元素的集合,是数据的一个子集
举例:某班由若干名学生组成
其中,001、002等等一整条的数据是数据元素
每条数据元素中的学号、姓名、性别、年龄是数据项
男生的数据对象就是表中所有性别为男的集合N={student[001,张三,男,18],student[003,派大汤,男,22]}
学号 | 姓名 | 性别 | 年龄 |
---|---|---|---|
001 | 张三 | 男 | 18 |
002 | 李四 | 女 | 20 |
003 | 派大汤 | 男 | 22 |
数据类型:一个值的集合和定义在此集合上的一组操作的总称
1)原子类型:值不可再分的数据类型,例如int、float
2)结构类型:值可以再分解为若干成分的数据类型
或者理解为:一种数据结构 + 定义在这种数据结构上的一组操作。
例如:数组、结构体
3)抽象数据类型:抽象数据组织以及与之相关的操作
抽象数据类型的描述方法(D,S,P)
D是数据对象,S是D上的关系集,P是对D的基本操作集。
抽象数据类型的定义方法
ADT 抽象数据类型{
数据对象(数据对象的定义)
数据关系(数据关系的定义)
基本操作(基本操作的定义)
}ADT 抽象数据类型名
数据结构:数据机构是相互之间存在一种或多种特定关系的数据元素集合。数据元素之间的关系叫做结构,数据结构包括三方面的内容:逻辑结构、存储结构、数据的运算。
数据的逻辑结构和存储结构是密不可分的两方面,算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的的存储结构
逻辑结构是指数据元素之间的逻辑关系。它与数据的存储无关,独立于计算机。
存储结构是指数据结构在计算机中的表示(映像),也称为物理结构,包括数据元素的表示和关系的表示。数据的存储结构是用计算机语言实现的逻辑结构,依赖于计算机语言。数据结构的存储方式主要有顺序存储、链式存储、索引存储、散列存储。
存储方式描述与优缺点对比:
存储方式 | 具体操作 | 优点 | 缺点 |
---|---|---|---|
顺序存储 | 把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现 | 可以实现随机存取,每个元素占用最少的内存空间 | 只能使用相邻的一整块存储单元,可能产生较多的外部碎片 |
链式存储 | 借助指示元素存储地址的指针来表示元素之间的逻辑关系 | 不会出现碎片现象,能充分利用所有内存单元 | 每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取 |
索引存储 | 在存储元素信息的同时,还建立附加的索引表,索引表的每项称为索引项,其一般形式为(关键字,地址) | 检索速度快 | 附加的索引表额外占用存储空间。增加删除也需要修改索引表,会花费更多时间。 |
散列存储 | 根据元素的关键字直接计算出该元素的存储地址,又称哈希存储 | 检索、增加、删除结点操作都很快 | 若散列函数不好,则可能出现元素存储单元的冲突,解决冲入会增加时间和空间开销 |
重要的一点:
数据的逻辑结构独立于存储结构
但是存储结构是依赖于逻辑结构
算法:对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或者多个操作。
算法的特性:
算法应达到的目标:
基本概念:
计算时间复杂度两条规则:
计算时间复杂度三条原则:
计算方法和步骤:
找到一个基本操作(最深层循环)
分析该基本操作的执行次数t与问题规模的关系t=f(n),主要关注循环条件与循环内部对变量的操作。
① 如果循环主体的变量参与循环条件的判断,设执行次数为t,找t与n之间的关系
② 如果循环主体中的变量与循环条件无关,采用数学归纳法,或者累计循环次数。递归程序一般使用公式进行递推。非递归程序直接累加次数。
t的数量级O(t)就是算法时间复杂度T(n)
具体计算:
1.常数阶
void main()
{
int x=0;
x++;
printf("%d",x);
}
基本操作:x++(无循环)
执行次数:1
关系:t=f(1)=1
T(n)=O(1)
2.线性阶
void main()
{
int n=100;
for (int i=0;i<n;i++){
printf("%d\n",i);
}
}
基本操作:printf(“%d”,n);
执行次数:n
关系:t=f(n)=n
T(n)=O(n)
3.平方阶
void main()
{
int n=5,x=0;
for( int i =2; i <= n; ++i ){
for( int j =2; j <= i - 1; ++j ){
++x;
printf("%d",x);
}
}
}
基本操作:printf(“%d”,x);
执行次数:(1)+(1+1)+(1+1+1)+…+(n-2)
= (1+n-2)×(n-2)/2
= (n-1)(n-2)/2
= 1 2 {1} \over {2} 21 n 2 n^2 n2- 3 2 {3} \over {2} 23 n n n+1
关系:t=f( 1 2 {1} \over {2} 21 n 2 n^2 n2- 3 2 {3} \over {2} 23 n n n+1)= n 2 n^2 n2
T(n)=O( n 2 n^2 n2)
4. log 2 n \log_2 n log2n阶
#include
int fun(int n)
{
int i=1;
while(i<=n){
i=i*2;
}
return i;
}
基本操作:i=i*2;
执行次数:
设执行次数t, 2 t 2^t 2t
关系:t=f( log 2 n \log_2 n log2n)= log 2 n \log_2 n log2n
T(n)=O( log 2 n \log_2 n log2n)
5. n log 2 n n\log_2 n nlog2n阶
int solve(int left, int right,int num[] )
{
if(left == right)
return num[left];
int mid = (left + right) / 2;
int lans = solve(left, mid,num);
int rans = solve(mid + 1, right,num);
int sum = 0, lmax = num[mid], rmax = num[mid + 1];
for(int i = mid; i >= left; i--) {
sum += num[i];
if(sum > lmax) lmax = sum;
}
sum = 0;
for(int i = mid + 1; i <= right; i++) {
sum += num[i];
if(sum > rmax) rmax = sum;
}
int ans = lmax + rmax;
if(lans > ans) ans = lans;
if(rans > ans) ans = rans;
return ans;
}
int main(void)
{
int n;
int num[100],*p;
p=num;
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%d", &num[i]);
printf("%d\n", solve(1,n,p));
return 0;
}
方法1:
基本操作:sum += num[i];
该函数利用递归将数组进行不断的等量分割,直到不可再分。类似于在在外边套了一层
while (i
每次递归都会使用时间复杂度为O(n)的for循环。
所以有执行次数= n 2 {n} \over {2} 2n*1+ n 4 {n} \over {4} 4n*2+ n 8 {n} \over {8} 8n4+…+ n 2 m {n} \over {2^m} 2mn 2 m − 1 {2}^{m-1} 2m−1
= n 2 {n} \over {2} 2n+ n 2 {n} \over {2} 2n+ n 2 {n} \over {2} 2n+…+ n 2 {n} \over {2} 2n (m项)
= n 2 {n} \over {2} 2nm
= n 2 {n} \over {2} 2n log 2 n \log_2 n log2n
关系:t=f( n 2 {n} \over {2} 2n log 2 n \log_2 n log2n)=n log 2 n \log_2 n log2n
T(n)=O(n* log 2 n \log_2 n log2n)
方法2:
找递归关系:T(n)=2T( n 2 {n} \over {2} 2n) +cn//将n除以2,再进行两遍,cn是下面的循环语句,复杂度为线性阶。
T(n)= 2 1 2^1 21T( n 2 1 {n} \over {2^1} 21n)+1cn
T(n)= 2 2 2^2 22T( n 2 2 {n} \over {2^2} 22n)+2cn
T(n)= 2 3 2^3 23T( n 2 3 {n} \over {2^3} 23n)+3cn
…
T(n)= 2 k 2^k 2kT( n 2 k {n} \over {2^k} 2kn)+kcn
另n= 2 k 2^k 2k,所以k= log 2 n \log_2n log2n
可得T(n)=cn+cn log 2 n \log_2n log2n
T(n)=O(n log 2 n \log_2n log2n)
ps:也可以用上面的公式直接得出答案O(n log 1 n \log_1n log1n)
证明:时间复杂度与 log \log log中的底数无关,(即O( log 2 n \log_2n log2n)=O( log 3 n \log_3n log3n))
log a n \log_an logan/ log b n \log_bn logbn
=( log c n \log_cn logcn/ log c a \log_ca logca)/( log c n \log_cn logcn/ log c b \log_cb logcb)
=( log c b \log_cb logcb/ log c a \log_ca logca)
= log a b \log_ab logab
所以,底数只是系数,不影响最终的时间复杂度。
6. n 3 \sqrt[3]{n} 3n阶
void fun(int n){
int i=0;
while (i*i*i<=n){
i++;
}
}
基本操作:i++
执行次数:
设执行次数为t,t * t * t<=n,即 t 3 t^3 t3<=n,所以t<= n 1 3 {n}^{1\over3} n31
关系:t=f( n 3 \sqrt[3]{n} 3n)= n 3 \sqrt[3]{n} 3n
T(n)=O( n 3 \sqrt[3]{n} 3n)
7. 2 n 2^n 2n阶
long aFunc(int n) {
if (n <= 1) {
return 1;
} else {
return aFunc(n - 1) + aFunc(n - 2);
}
}
递归算法求斐波那契数列
T(1)=T(1)
T(2)=T(1)+T(0)
T(3)=T(2)+T(1)=T(1)+T(0)+T(1)
T(4)=T(3)+T(2)=T(1)+T(0)+T(1)+T(1)+T(0)
T(5)=T(4)+T(3)=T(1)+T(0)+T(1)+T(1)+T(0)+T(1)+T(0)+T(1)
像是二叉树一样,随着n的增长,T(n)以 2 n 2^n 2n的指数增长速度增长。
T(n)= 2 n 2^n 2n个T(0)或者T(1)相加= 2 n 2^n 2n
常见时间复杂度之间的大小关系:
O(1) < O( log 2 n \log_2 n log2n) < O(n) < O(n* log 2 n \log_2 n log2n) < O( n 2 n^2 n2) < O( n 3 n^3 n3) < O( 2 n 2^n 2n) < O( n ! n! n!) < O( n n n^n nn)
基本概念:
空间复杂度:算法的空间复杂度S(n)定义为该算法所消耗的存储空间,是问题规模n的函数,记为S(n)。
原地工作:指算法所需的辅助空间为常量,即为O(1)。
理解:
算法执行所需存储空间:
空间复杂度=内存调用的深度。
循环语句中的变量大多只开辟一次,空间复杂度为O(1)
递归算法自己调用自己,并且开辟新的内存,所以通常会增加空间复杂度。
计算空间复杂度两条规则:
常见空间复杂度之间的大小关系:
O(1) < O( log 2 n \log_2 n log2n) < O(n) < O(n* log 2 n \log_2 n log2n) < O( n 2 n^2 n2) < O( n 3 n^3 n3) < O( 2 n 2^n 2n) < O( n ! n! n!) < O( n n n^n nn)
计算方法和步骤:
普通程序:
1.找到所占空间大小与问题规模有关的变量
2.分析所占空间x与问题规模n的关系x=f(n)
3.x的数量级O(x)就是算法的空间复杂度S(n)
递归程序:
1.找到递归调用的深度x与问题规模n的关系x=f(n)
2.x的数量级O(x)就是算法的空间复杂度S(n)
注意:有的算法各层函数所需要的存储空间不同,方法略有区别。
具体计算:
void fun(int n)
{
printf("%d", n % 10);
if(n / 10 != 0)
fun(n / 10);
}
int main(void)
{
fun(10203);
return 0;
}
开辟空间的语句:void fun(int n)
执行次数:问题规模n的位数就是执行次数
所以可知执行次数t=f( log 10 n \log_{10} n log10n)= log 10 n \log_{10} n log10n
S(n)=O( log 10 n \log_{10} n log10n)