算法性能分析
代码风格
时间复杂度分析
logn的底数是多少?
算法超时评测
java的输入形式
在leetcode上练习算法的时候应该都遇到过一种错误是“超时”。
也就是说程序运行的时间超过了规定的时间,一般OJ(online judge)的超时时间就是1s,也就是用例数据输入后最多要1s内得到结果,暂时还不清楚leetcode的判题规则,下文为了方便讲解,暂定超时时间就是1s。
如果写出了一个 O ( n ) O(n) O(n)的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。
如果n的规模已经足够让 O ( n ) O(n) O(n)的算法运行时间超过了1s,就应该考虑log(n)的解法了。
import java.util.*;
public class Main {
// o(n)
public static void function1(long n) {
System.out.println("o(n)算法:");
long k = 0;
for (long i = 0; i < n; i++) {
k++;
}
}
// o(n^2)
public static void function2(long n) {
System.out.println("o(n^2)算法:");
long k = 0;
for (long i = 0; i < n; i++) {
for (long j = 0; j < n; j++) {
k++;
}
}
}
// o(nlogn)
public static void function3(long n) {
System.out.println("o(nlogn)算法:");
long k = 0;
for (long i = 0; i < n; i++) {
for (long j = 1; j < n; j = j * 2) {
k++;
}
}
}
public static void main(String[] args) {
while (true) {
Scanner scan = new Scanner(System.in);
System.out.println("输入n:");
int n = scan.nextInt();
long startTime = System.currentTimeMillis();
function1(n);
// function2(n);
// function3(n);
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
System.out.println("算法耗时==" + costTime + "ms");
}
}
}
递归算法复杂度
递归算法的时间复杂度本质上是要看: 递归的次数 * 每次递归中的操作次数。
那再来看代码,这里递归了几次呢?
每次n-1,递归了n次时间复杂度是O(n),每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项O(1),所以这份代码的时间复杂度是 n × 1 = O(n)。
//o(n)
int function2(int x, int n) {
if (n == 0) {
return 1; // return 1 同样是因为0次方是等于1的
}
return function2(x, n - 1) * x;
}
//o(logn)
int function4(int x, int n) {
if (n == 0) {
return 1;
}
int t = function4(x, n / 2);// 这里相对于function3,是把这个递归操作抽取出来
if (n % 2 == 1) {
return t * t * x;
}
return t * t;
}
空间复杂度
空间复杂度是考虑程序(可执行文件)的大小么?
很多同学都会混淆程序运行时内存大小和程序本身的大小。这里强调一下空间复杂度是考虑程序运行时占用内存的大小,而不是可执行文件的大小。
空间复杂度是准确算出程序运行时所占用的内存么?
不要以为空间复杂度就已经精准的掌握了程序的内存使用大小,很多因素会影响程序真正内存使用大小,例如编译器的内存对齐,编程语言容器的底层实现等等这些都会影响到程序内存的开销。
所以空间复杂度是预先大体评估程序内存使用的大小。
说到空间复杂度,我想同学们在OJ(online judge)上应该遇到过这种错误,就是超出内存限制,一般OJ对程序运行时的所消耗的内存都有一个限制。
为了避免内存超出限制,这也需要我们对算法占用多大的内存有一个大体的预估。
同样在工程实践中,计算机的内存空间也不是无限的,需要工程师对软件运行时所使用的内存有一个大体评估,这都需要用到算法空间复杂度的分析。
来看一下例子,什么时候的空间复杂度是 O ( 1 ) O(1) O(1)呢,代码如下:
int j = 0;
for (int i = 0; i < n; i++) {
j++;
}
第一段代码可以看出,随着n的变化,所需开辟的内存空间并不会随着n的变化而变化。即此算法空间复杂度为一个常量,所以表示为大O(1)。
什么时候的空间复杂度是O(n)?
当消耗空间和输入参数n保持线性增长,这样的空间复杂度为O(n),来看一下这段代码
int* a = new int(n);
for (int i = 0; i < n; i++) {
a[i] = i;
}
我们定义了一个数组出来,这个数组占用的大小为n,虽然有一个for循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,随着n的增大,开辟的内存大小呈线性增长,即 O(n)。
空间复杂度是logn的情况确实有些特殊,其实是在递归的时候,会出现空间复杂度为logn的情况。
int binary_search( int arr[], int l, int r, int x) {
if (r >= l) {
int mid = l + (r - l) / 2;
if (arr[mid] == x)
return mid;
if (arr[mid] > x)
return binary_search(arr, l, mid - 1, x);
return binary_search(arr, mid + 1, r, x);
}
return -1;
}
都知道二分查找的时间复杂度是O(logn),那么递归二分查找的空间复杂度是多少呢?
递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度
我们依然看 每次递归的空间复杂度和递归的深度
每次递归的空间复杂度可以看出主要就是参数里传入的这个arr数组,但需要注意的是在C/C++中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入的数组首元素地址。
也就是说每一层递归都是公用一块数组地址空间的,所以 每次递归的空间复杂度是常数即:O(1)。
再来看递归的深度,二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * logn = O(logn)。
大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)。