共计 3283 个字符,预计需要花费 9 分钟才能阅读完成。
Example 1 For 循环
思考以下代码的工夫复杂度是多少,应用 Big Theta 示意
public static void printParty(int N) {for (int i = 1; i <= N; i = i * 2) {for (int j = 0; j < i; j += 1) {System.out.println("hello");
int ZUG = 1 + 1;
}
}
}
选取一项最具代表性的操作
System.out.println("hello);
记其执行次数为 c(N) , 枚举 N 从 1 到 N,C(N)的值,用以下表格示意:
察看高低两个图可知
当 N = 1 时,i = 1, j = 0;
当 N = 2 时,i = 1 , 2, j = 0, 1;
N = 2, 已知 i = 1 的后果,只需再求 i = 2 的后果再将后面算得的 i = 1 的后果相加, 即 3 = 1 + 2
当 N = 3 时,因为 i 的变动是每次变为 2i,即当 N = 2 时,2i = 4,3 比 4 小,循环依然终止在 N = 2,也就是说 N = 3 与 N = 2 的打印次数雷同
……
因而察看得悉,当 N 位于相邻的 2 的某指数幂之间时,打印次数与前一次雷同,且等于后面所有 2 的指数幂时的打印次数相加,即
$$
C(N) = 1 + 2 + 4 + 8 + …… + N
$$
假如 N 为 2 的指数幂,那么上述式子共有 \(1 + log_{2}N \)项
应用等比数列求和公式
$$
S_{n} = \frac{a_{1}(1 – q^{n})}{1 – q}
$$
可算得:
$$
C(N) = \frac{1 – 2^{1 + log_{2}N}}{1 – 2} = 2N – 1
$$
因而 \(C(N) = \Theta(N) \)
技巧 2
察看 N 的值能够发现,C(N)的值总是位于 0.5N 到 2N 之间,因而能够大抵得出 C(N)的函数图像:
不难发现 \(C(N) = \Theta(N) \)
最初,Josh 老师心愿咱们不要一看到 for 循环嵌套就认为工夫复杂度是 \(\Theta(N^{2}) \), 还是要循序渐进地剖析,对于求工夫复杂度的普遍性办法是
- 找出准确的和
- 写例子
- 画图
Example 2 递归树
思考以下函数
public static int f(int n) {if (n <= 1)
return 1;
return f(n-1) + f(n-1);
}
工夫复杂度是多少?
Solution 1
当 n = 4 的时候,f(4)的调用状况如图:
计算 f(n)的调用次数 c(n):
C(1) = 1
C(2) = 1 + 2
C(3) = 1 + 2 + 4
…….
C(N) = 1 + 2 + 4 + … + \(2^{N – 1} \)
因而依据等比数列前 N 项和可得 \(C(N) = 2^{N} – 1 \)
除了间接由等比数列前 N 项和得出后果之外,还能够由后面的公式
$$
C(Q) = 1 + 2 + 4 + … + Q = 2Q – 1
$$
此处 \(Q = 2^{N – 1} \),因而
$$
C(Q) = 2Q – 1 = 2 \cdot 2^{N – 1} – 1 = 2^{N} – 1
$$
也能够得出后果
因而工夫复杂度 \(f(N) = \Theta(2^{N}) \)
Solution 2
从直观上来看,当 f(n)中 n = 4 时,f(4)的调用状况为
当 n = 5 时,f(5)的调用状况为
可见调用次数翻倍,n 每减少 1,调用次数 x 2,因而直观上来说也是 \(2^{N} \)指数级减少,更确切来说:
C(1) = 1
C(N) = 2C(N - 1) + 1
= 2(2C(N - 2) + 1) + 1
= 2(2(2C(N - 3) + 1) + 1) + 1
......
= 2(...2C(N - (N - 1)) + 1 ).... + 1
= 2^(N - 1)C(1) + 1 + 2 + 4 + ... + 2^((N - 1) - 1)
= 2^(N - 1) + 2^(N - 2) + 2^(N - 3) + ... + 4 + 2 + 1
= 2^N - 1
工夫复杂度 \(f(N) = \Theta(2^{N}) \)
Example 3 BinarySearch
接下来咱们剖析二分查找的工夫复杂度,二分查找原理都应该比拟相熟了,不再过多介绍,具体实现过程能够查看这个 slide BinarySearch Demo。
直观剖析
咱们晓得的是每进行一次二分查找,其查找范畴都会减半,也就是以后数组的大小减半,假如数组原始大小为 N,最坏的状况下,直到数组大小缩短为 1 时,也就是只剩下一个元素时,咱们才找到想要查找的值,那么,设二分查找的调用次数为 C , 则
$$
1 = \frac{N}{2^{C}}
$$
从而解得
$$
C = log_{2}{N}
$$
而对于二分查找,其函数体内执行次数均为常数阶,因而能够用调用次数来示意工夫复杂度,从直观上来说 \(f(N) = \Theta({log_{2}N}) \)
准确剖析
思考以下问题: 对于一个大小为 6 的数组,应用二分查找,其最坏的状况下,函数的调用次数是多少?
答案是 3 次
那么这是在 N = 6 时的状况,咱们把 N 的所有状况与对应的 函数调用次数 C(N)列出:
可见是一颗递归二叉树,察看可知
$$
C(N) = ⌊log_{2}(N)⌋+1
$$
咱们失去了函数调用次数 C(N)与数组大小 N 的准确关系,那么问题是
\(C(N) = \Theta(log_{2}N) \)吗?
证实:
$$
⌊f(N)⌋=\Theta(f(N))
$$
proof:
$$
f(N) – \frac{1}{2} \le ⌊f(N) – \frac{1}{2}⌋ \le f(N) + \frac{1}{2}
$$
依据 Big Theta 的定义,可知 \(⌊f(N) – \frac{1}{2}⌋领有下界 f(N) – \frac{1}{2} 和上界 f(N) + \frac{1}{2},属于 f(N)函数簇 \),因而
$$
⌊f(N)⌋=\Theta(f(N))
$$
抛下常数阶,可知 \(C(N) = \Theta(log_{2}N) \)
不加证实地,咱们还能晓得
$$
⌈f(N)⌉=\Theta(f(N))
$$
$$
log_{P}(N) = \Theta(log_{Q}(N))
$$
因而当前无论是以任何常数为底的对数,都简写为 logN,
因而最终化简后果是 \(C(N) = \Theta(logN) \)
另一种办法计算工夫复杂度
考虑一下下面 Example 2 的一种利用式子自身的递归个性解题的办法,对于二分查找:
二分查找代码的瑕疵
当咱们的二分查找实现代码为:
static int binarySearch(String[] sorts, String x, int lo, int hi)
if (lo > hi) return -1;
int m = (lo + hi) / 2;
int cmp = x.compareTo(sorted[m]);
if (cmp < 0) return binarySearch(sorted, x, lo, m - 1);
else if (cmp > 0) return binarySearch(sorted, x, m + 1, hi);
else return m;
}
存在一个瑕疵,当计算两头地位 m 的时候:
$$
m = \frac{(lo + hi)}{2}
$$
\(当 lo + hi > 2^{31} – 1 \)时,即超过了 int 的最大值时,会产生溢出,导致 m 变成正数,从而引发数组ArrayIndexOutOfBoundsException
因而求两头地位的解决方案是
int mid = low + ((high - low) / 2);
或者
int mid = (low + high) >>> 1;
在 C 和 C ++ 中 没有 >>>,能够写成
mid = ((unsigned int)low + (unsigned int)high)) >> 1;
细节十分重要!
参考
Example 4 MergeSort
本例剖析归并排序的工夫复杂度
归并排序即把一个数组一分为二,再二分为四,四分为八,直至放大为一个数组只有 1 个元素,是一种典型的分治思维。之后再把数组从最底层的开始合并,合并的过程能够参考
MergeSortDemo
因为一次合并即相当于把大小为 N 的数组填充结束,因而以填充次数而言,合并过程的工夫复杂度为 O(N)
直观剖析
把一个大小为 N 的数组一直一分为二直至放大为一个数组只有 1 个元素,如果进行了 K 次划分,那么
\(K = log_{2}N\)
接着剖析每一层的工作量,
第一层,须要将两个大小为 N / 2 的数组合并,工作量为 N
第二层,两个 N / 2 的数组别离由两个 N / 4 的数组合并而来,即 4xN/4,工作量为 N
……
察看知每一层的工作量均为 N,共有 K 层,则总的工作量为
$$
f(N) = K\cdot N=Nlog_{2}N
$$
应用递推的数学公式
因而,归并排序的工夫复杂度为 \(\Theta(NlogN)\)