程序设计 = 数据结构 + 算法
数据结构分为逻辑结构与物理结构
逻辑结构:是指数据对象中数据元素之间的相互关系;物理结构:是指数据的逻辑结构在计算机中的存储形式。
逻辑结构可以分为:集合结构、线性结构、树形结构、图形结构。
数据元素的存储形式有两种:顺序存储和链式存储
顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。
链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以连续,也可以是不连续的。
算法:比如计算1+2+…+99+100 = ?如果一个数字一个数字的加便不是明智之举,而高斯利用利用自己总结出的公式 ( 1 + n ) ∗ n / 2 (1+n)*n / 2 (1+n)∗n/2 就可以轻松的计算1加到任何一个数字,而高斯总结的这个公式就可以看做一个算法。
算法具有五个基本特征:输入、输出、有穷性、确定性、可行性
输入:算法具有零个或多个输入。
输出:算法至少有一个或多个输出。
有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
确定性:算法的每一个步骤都具有确定的含义,不会出现二义性;算法在一定条件下,只有一条执行路基你给,相同的输入只能有唯一的输出结果;算法的每个步骤都应该被精确定义而无歧义。
可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
算法效率的度量方法
时间复杂度:评估执行程序所需的时间。可以估算出程序对处理器的使用程度。
空间复杂度:评估执行程序所需的存储空间。可以估算出程序对计算机内存的使用程度。
时间频度: 一个算法中的语句执行次数称为语句频度或时间频度。记为 T ( n ) T(n) T(n)。
时间复杂度: 时间频度 T ( n ) T(n) T(n)中, n n n 称为问题的规模,当 n n n 不断变化时,时间频度 T ( n ) T(n) T(n)也会不断变化。一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用 T ( n ) T(n) T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,$T(n) /f(n) 的 极 限 值 为 不 等 于 零 的 常 数 , 则 称 的极限值为不等于零的常数,则称 的极限值为不等于零的常数,则称f(n) 是 是 是T(n) 的 同 数 量 级 函 数 , 记 作 的同数量级函数,记作 的同数量级函数,记作T(n)=O(f(n))$,它称为算法的渐进时间复杂度,简称时间复杂度。
下图为 O ( 1 ) O(1) O(1)、 O ( n ) O(n) O(n)、 O ( n 2 ) O(n^2) O(n2)的大 O O O 时间复杂度
推导大O阶
推导大O阶,我们可以按照如下的规则来进行推导,得到的结果就是大O表示法:
- 用常数1来取代运行时间中所有加法常数。
- 修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。
常数阶
int sum = 0, n = 100;
printf("Change the world by program!");
sum = (1+n)*n/2;
面算法的运行的次数的函数为 f ( n ) = 3 f(n)=3 f(n)=3,根据推导大 O O O阶的规则1,我们需要将常数3改为1,则这个算法的时间复杂度为 O ( 1 ) O(1) O(1)。
线性阶
一般含有非嵌套循环设计线性阶,线性阶就是随着问题规模 n n n 的扩大,地应计算次数呈现直线增长。
int i, n = 100, sum = 0;
for( i = 0; i < n; i++)
{
sum += i;
}
上面这个代码的时间复杂度就是 O ( n ) O(n) O(n) 。
平方阶
int i, j, n = 100;
for(i = 0; i < n; i++)
{
for(j = 0; j < n; j++)
{
printf("I can make it\n");
}
}
内层循环执行100次,外层循环执行100次,总共执行100*100 次,就是n的平方,即时间复杂度为 O ( n 2 ) O(n^2) O(n2) .
注意下面程序的时间复杂度:
int i, j, n = 100;
for(i = 0; i < n; i++)
{
for(j = i; j < n; j++)
{
printf("I can make it\n");
}
}
分析如上代码,由于当i=0时,内层循环执行了n次,当i = 1时,内循环执行n-1次…,当i = n -1 时,内循环执行1次,所以中的执行次数应该为: n + ( n − 1 ) + . . . + 2 + 1 = n ( n + 1 ) / 2 n + (n-1) + ... + 2 + 1 = n(n+1)/2 n+(n−1)+...+2+1=n(n+1)/2 .
n ( n + 1 ) / 2 = n 2 / 2 + n / 2 n(n+1)/2 = n^2 / 2 + n / 2 n(n+1)/2=n2/2+n/2 , 根据推到大O的策略,第二条只保留最高项,所以 n / 2 n/2 n/2 去掉,第三条,去除最高次项相乘的常数,最终获得 O ( n 2 ) O(n^2) O(n2) .
对数阶
int i = 1, n = 100;
while(i < n)
{
i = i * 2;
}
由于每次 i * 2 之后,就距离n更近一步,假设有x个2相乘后大于或等于n,则会退出循环。
由 2 x = n 2^x=n 2x=n 得到 x = l o g 2 n x = log_2 n x=log2n ,所以这个循环的执行时间复杂度为 O ( l o g n ) O(log n) O(logn) 。
函数调用的时间复杂度分析
int i, j;
for(i = 0;i < n;i++)
{
function(i);
}
void function(int count) {
printf("%d", count);
}
function函数的时间复杂度为 O ( 1 ) O(1) O(1), 所以整体的时间复杂度就是循环的次数为 O ( n ) O(n) O(n).
int i, j;
for(i = 0;i < n;i++)
{
function(i);
}
void function(int count) {
int j;
for(j = count; j < n; j++)
{
printf("%d", j);
}
}
事实上此时的代码和平方阶举的第二个例子是一样的,function内部的循环次数随着count的增加而减少,根据之前的分析,该程序算法时间复杂度为 O ( n 2 ) O(n^2) O(n2);
n++; //1
function(n); //1
for(i = 0; i < n; i++)
{
function(i);
}
for(i = 0; i < n; i++)
{
for(j = i; j < n; j++)
{
printf("%d", j);
}
}
void function(int count) {
int j;
for(j = count; j < n; j++)
{
printf("%d", j);
}
}
此时的程序的算法时间复杂度还是 O ( n 2 ) O(n^2) O(n2)
常见的时间复杂度表
例子 | 时间复杂度 | 装逼术语 |
---|---|---|
123456 | O ( 1 ) O(1) O(1) | 常数阶 |
3 n + 4 3 n + 4 3n+4 | O ( n ) O(n) O(n) | 线性阶 |
3 n 2 + 4 n + 5 3 n^2 + 4 n + 5 3n2+4n+5 | O ( n 2 ) O(n^2) O(n2) | 平方阶 |
3 log 2 n + 4 3 \log_2 n + 4 3log2n+4 | O ( log n ) O(\log n) O(logn) | 对数阶 |
2 n + 3 n log 2 n + 14 2 n + 3 n \log_2 n + 14 2n+3nlog2n+14 | O ( n log n ) O(n\log n) O(nlogn) | nlogn阶 |
n 3 + 2 n 2 + 4 n + 6 n^3 + 2 n^2 + 4 n + 6 n3+2n2+4n+6 | O ( n 3 ) O(n^3) O(n3) | 立方阶 |
2 n 2^n 2n | O ( 2 n ) O(2 ^ n) O(2n) | 指数阶 |
该表所对应的图:
常用的时间复杂度所耗费的时间从小到大依次为:
O ( 1 ) < O ( log n ) < O ( n ) < O ( n log n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
最坏时间复杂度与平均时间复杂度
例如我们查找一个有n个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为 O ( 1 ) O(1) O(1) ,最坏情况下就是这个数字在最后一个位置,那么时间复杂度为$O(n) $ .
平均运行时间就是期望的运行时间。
而我们平时进行时间复杂度分析一般只考虑最坏时间复杂度。
写代码的时候完全可以用空间来换取时间