算法:
算法:是解决某一类问题的通法,即一系列清晰无歧义的计算指令。每个算法只能解决具有特定特征的一类问题,但一个问题可由多个算法解决。
一个算法应该有以下五个方面的特性:
- 输入(Input):算法必须有输入量,用以刻画算法的初始条件(特殊情况也可以没有输入量,这时算法本身定义了初始状态);
- 输出(Output):算法应有一个或以上输出量,输出量是算法计算的结果。没有输出的算法毫无意义。
- 明确性(Definiteness):算法的描述必须无歧义,以保证算法的实际执行结果是精确地匹配要求或期望,通常要求实际运行结果是确定的。
- 有限性(Finiteness):算法必须在有限个步骤内完成任务。
- 有效性(Effectiveness):算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现(又称可行性)。
比较算法的优劣我们从两个维度去进行考量:时间 空间(时间复杂度,空间复杂度)
时间复杂度分类:
- 最坏时间复杂度:输入数据状态最不理想情况下的时间复杂度,也就是算法时间复杂度的上界。若没有特别声明,时间复杂度就是指最坏时间复杂度。
- 平均时间复杂度:在所有可能的输入实例均以等概率出现的情况下,算法的期望时间复杂度。
- 最好时间复杂度:输入数据状态最理想情况下的时间复杂度。
时间复杂度预估步骤
- 找出基本语句:算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
- 计算基本语句的执行次数的数量级:只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。
- 用大Ο符号表示算法的时间性能:将基本语句执行次数的数量级放入O()中。如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。例如:
- for (i=1; i<=n; i++)
- x++;
- for (i=1; i<=n; i++)
- for (j=1; j<=n; j++)
- x++;
第一个for循环的时间复杂度为Ο(n),第二个for循环的时间复杂度为Ο(),则整个算法的时间复杂度为Ο(n+
)=Ο(
)。
「 大O符号表示法 」,即 T(n) = O(f(n)),在 大O符号表示法中,时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。(也可以这样理解:时间复杂度就是程序计算次数
)
我们来看一个例子:
for(i=1; i<=n; ++i) (1)
{
j = i; (3)
j++; (4)
}
假设每行代码的执行时间都是一样的,我们用 1颗粒时间 来表示,那么这个例子的第一行耗时是1个颗粒时间,第三行的执行时间是 n个颗粒时间,第四行的执行时间也是 n个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是 1颗粒时间 + n颗粒时间 + n颗粒时间 ,即 (1+2n)个颗粒时间,即: T(n) = (1+2n)*颗粒时间,从这个结果可以看出,这个算法的耗时是随着n的变化而变化,因此,我们可以简化的将这个算法的时间复杂度表示为:T(n) = O(n)
为什么可以简化呢? 因为大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。在上面的例子中,当n无限大的时候,T(n) = time(1+2n)中的常量1就没有意义了,倍数2也意义不大。因此直接简化为T(n) = O(n) 就可以了。
常见的时间复杂度量级有:
-
常数阶O(1)
Ο(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是Ο(1)。int i = 1; int j = 2; ++i; j++; int m = i + j;
-
对数阶O(logN)
int i = 1; while(i<n) { i = i * 2; }
在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x =
,也就是说当循环
次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)
-
线性阶O(n)
for(i=1; i<=n; ++i) { j = i; j++; }
for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。
-
线性对数阶O(
)
for(m=1; m<n; m++) { i = 1; while(i<n) { i = i * 2; } }
线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
-
平方阶O(n²)
for(x=1; i<=n; x++) { for(i=1; i<=n; i++) { j = i; j++; } }
把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²)
-
立方阶O(n³) 相当于三层 n 循环
-
K次方阶O(
)【n的k次方】
-
指数阶(
) :类似于斐波那契数列形式的递归算法。
空间复杂度 类似于时间复杂度
空间复杂度是算法在运行过程中临时占用的存储空间大小的度量,一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。
如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(10g2n);当一个算法的空I司复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
int[] nums_copy = new int[nums.length]; // 数组初始化 都为0
for (int i = 0; i < nums.length; i++) { // 空间复杂度为 O(n)
if (nums_copy[nums[i]] == 0) {
nums_copy[nums[i]] = nums[i];
} else {
return nums[i]; // 返回重复数据
}
}
实际使用中我们经常采用牺牲空间换时间的方法来降低时间复杂度。