前面我们介绍了算法分析的一些姿势,那么现在我们就来学以致用,一起来解决几道一线互联网企业有关于算法分析的面试/笔试题。
【腾讯】下面算法的时间复杂度是____
int foo(int n) {
if (n <= 1) {
return 1;
}
return n * foo(n – 1);
}
看到这道题要我们分析算法时间复杂度后,我们要做的第一步便是确定关键操作,这里的关键操作显然是if语句,那么我们只需要判断if语句执行的次数即可。首先我们看到这是一个递归过程:foo会不断的调用自身,直到foo的实参小于等于1,foo就会返回1,之后便不会再执行if语句了。由此我们可以知道,if语句调用的次数为n次,所以时间复杂度为O(n)。
【京东】以下函数的时间复杂度为____
void recursive(int n, int m, int o) {
if (n <= 0) {
printf(“%d, %d\n”, m, o);
} else {
recursive(n – 1, m + 1, o);
recursive(n – 1, m, o + 1);
}
}
这道题明显要比上道题难一些,那么让我们来按部就班的解决它。首先,它的关键操作时if语句,因此我们只需判断出if语句的执行次数即可。以上函数会在n > 0的时候不断递归调用自身,我们要做的是判断在到达递归的base case(即n <= 0)前,共执行了多少次if语句。我们假设if语句的执行次数为T(n, m, o),那么我们可以进一步得到:T(n, m, o) = T(n-1, m+1, o) + T(n-1, m, o+1) (当n > 0时)。我们可以看到base case与参数m, o无关,因此我们可以把以上表达式进一步简化为T(n) = 2T(n-1),由此我们可得T(n) = 2T(n-1) = (2^2) * T(n-2)……所以我们可以得到以上算法的时间复杂度为O(2^n)。
【京东】如下程序的时间复杂度为____(其中m > 1,e > 0)
x = m;
y = 1;
while (x – y > e) {
x = (x + y) / 2;
y = m / x;
}
print(x);
以上算法的关键操作即while语句中的两条赋值语句,我们只需要计算这两条语句的执行次数即可。我们可以看到,当x – y > e时,while语句体内的语句就会执行,x = (x + y) / 2使得x不断变小(当y<<x时,执行一次这个语句会使x变为约原来的一半),假定y的值固定在1,那么循环体的执行次数即为~logm,而实际情况是y在每次循环体最后都会被赋值为m / x,这个值总是比y在上一轮循环中的值大,这样一来x-y的值就会更小,所以以上算法的时间复杂度为O(logm)。
【搜狗】假设某算法的计算时间可用递推关系式T(n) = 2T(n/2) + n,T(1) = 1表示,则该算法的时间复杂度为____
根据题目给的递推关系式,我们可以进一步得到:T(n) = 2(2T(n/4) + n/2) + n = … 将递推式进一步展开,我们可以得到该算法的时间复杂度为O(nlogn),这里就不贴上详细过程了。
时间复杂度:
时间复杂度按n越大算法越复杂来排的话:常数阶O(1)、对数阶O(logn)、线性阶O(n)、线性对数阶O(nlogn)、平方阶O(n²)、立方阶O(n³)、……k次方阶O(n的k次方)、指数阶O(2的n次方)。
示例代码(1):
1 decimal Factorial(int n) 2 { 3 if (n == 0) 4 return 1; 5 else 6 return n * Factorial(n - 1); 7 }
阶乘(factorial),给定规模 n,算法基本步骤执行的数量为 n,所以算法复杂度为 O(n)。
示例代码(2):
1 int FindMaxElement(int[] array) 2 { 3 int max = array[0]; 4 for (int i = 0; i < array.Length; i++) 5 { 6 if (array[i] > max) 7 { 8 max = array[i]; 9 } 10 } 11 return max; 12 }
这里,n 为数组 array 的大小,则最坏情况下需要比较 n 次以得到最大值,所以算法复杂度为 O(n)。
示例代码(3):
1 long FindInversions(int[] array) 2 { 3 long inversions = 0; 4 for (int i = 0; i < array.Length; i++) 5 for (int j = i + 1; j < array.Length; j++) 6 if (array[i] > array[j]) 7 inversions++; 8 return inversions; 9 }
这里,n 为数组 array 的大小,则基本步骤的执行数量约为 n*(n-1)/2,所以算法复杂度为 O(n2)。-----统计逆序数
示例代码(4):
1 long SumMN(int n, int m) 2 { 3 long sum = 0; 4 for (int x = 0; x < n; x++) 5 for (int y = 0; y < m; y++) 6 sum += x * y; 7 return sum; 8 }
给定规模 n 和 m,则基本步骤的执行数量为 n*m,所以算法复杂度为 O(n2)。
示例代码(5):
1 decimal Sum3(int n) 2 { 3 decimal sum = 0; 4 for (int a = 0; a < n; a++) 5 for (int b = 0; b < n; b++) 6 for (int c = 0; c < n; c++) 7 sum += a * b * c; 8 return sum; 9 }
这里,给定规模 n,则基本步骤的执行数量约为 n*n*n ,所以算法复杂度为 O(n3)。
示例代码(6):
1 decimal Calculation(int n) 2 { 3 decimal result = 0; 4 for (int i = 0; i < (1 << n); i++) 5 result += i; 6 return result; 7 }
这里,给定规模 n,则基本步骤的执行数量为 2n,所以算法复杂度为 O(2n)。
示例代码(7):
斐波那契数列:
- Fib(0) = 0
- Fib(1) = 1
- Fib(n) = Fib(n-1) + Fib(n-2)
F() = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ...
1 int Fibonacci(int n) 2 { 3 if (n <= 1) 4 return n; 5 else 6 return Fibonacci(n - 1) + Fibonacci(n - 2); 7 }
这里,给定规模 n,计算 Fib(n) 所需的时间为计算 Fib(n-1) 的时间和计算 Fib(n-2) 的时间的和。
T(n<=1) = O(1)
T(n) = T(n-1) + T(n-2) + O(1)
fib(5) / \ fib(4) fib(3) / \ / \ fib(3) fib(2) fib(2) fib(1) / \ / \ / \
通过使用递归树的结构描述可知算法复杂度为 O(2n)。
示例代码(8):
1 int Fibonacci(int n) 2 { 3 if (n <= 1) 4 return n; 5 else 6 { 7 int[] f = new int[n + 1]; 8 f[0] = 0; 9 f[1] = 1; 10 11 for (int i = 2; i <= n; i++) 12 { 13 f[i] = f[i - 1] + f[i - 2]; 14 } 15 16 return f[n]; 17 } 18 }
同样是斐波那契数列,我们使用数组 f 来存储计算结果,这样算法复杂度优化为 O(n)。
示例代码(9):
1 int Fibonacci(int n) 2 { 3 if (n <= 1) 4 return n; 5 else 6 { 7 int iter1 = 0; 8 int iter2 = 1; 9 int f = 0; 10 11 for (int i = 2; i <= n; i++) 12 { 13 f = iter1 + iter2; 14 iter1 = iter2; 15 iter2 = f; 16 } 17 18 return f; 19 } 20 }
同样是斐波那契数列,由于实际只有前两个计算结果有用,我们可以使用中间变量来存储,这样就不用创建数组以节省空间。同样算法复杂度优化为 O(n)。
示例代码(10):
通过使用矩阵乘方的算法来优化斐波那契数列算法。
1 static int Fibonacci(int n) 2 { 3 if (n <= 1) 4 return n; 5 6 int[,] f = { { 1, 1 }, { 1, 0 } }; 7 Power(f, n - 1); 8 9 return f[0, 0]; 10 } 11 12 static void Power(int[,] f, int n) 13 { 14 if (n <= 1) 15 return; 16 17 int[,] m = { { 1, 1 }, { 1, 0 } }; 18 19 Power(f, n / 2); 20 Multiply(f, f); 21 22 if (n % 2 != 0) 23 Multiply(f, m); 24 } 25 26 static void Multiply(int[,] f, int[,] m) 27 { 28 int x = f[0, 0] * m[0, 0] + f[0, 1] * m[1, 0]; 29 int y = f[0, 0] * m[0, 1] + f[0, 1] * m[1, 1]; 30 int z = f[1, 0] * m[0, 0] + f[1, 1] * m[1, 0]; 31 int w = f[1, 0] * m[0, 1] + f[1, 1] * m[1, 1]; 32 33 f[0, 0] = x; 34 f[0, 1] = y; 35 f[1, 0] = z; 36 f[1, 1] = w; 37 }
优化之后算法复杂度为O(log2n)。
示例代码(11):
在 C# 中更简洁的代码如下。
1 static double Fibonacci(int n) 2 { 3 double sqrt5 = Math.Sqrt(5); 4 double phi = (1 + sqrt5) / 2.0; 5 double fn = (Math.Pow(phi, n) - Math.Pow(1 - phi, n)) / sqrt5; 6 return fn; 7 }
示例代码(12):
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的有序数据。算法适用于少量数据的排序,时间复杂度为 O(n2)。
1 private static void InsertionSortInPlace(int[] unsorted) 2 { 3 for (int i = 1; i < unsorted.Length; i++) 4 { 5 if (unsorted[i - 1] > unsorted[i]) 6 { 7 int key = unsorted[i]; 8 int j = i; 9 while (j > 0 && unsorted[j - 1] > key) 10 { 11 unsorted[j] = unsorted[j - 1]; 12 j--; 13 } 14 unsorted[j] = key; 15 } 16 } 17 }