1. 时间复杂度O(n)
O(n)衡量了算法的增长曲线,也就是程序运行时间和输入大小的关系。
- 设f(n)和g(n)为两个函数,f(n)=O(g(n))成立的条件是:
如果存在一个**常数c**>0, n>=n0>=1,使得f(n)<=c*g(n),且n>=n0
证明2n+10的时间复杂度是O(n): 2n+10 <= cn,存在c=3,n0=10使得等式成立
- 简单程序分析:
public static double max(double[] nums) {// array size = n
int size = nums.length; // 2 ops
double max = nums[0]; // 2 ops
for (int i = 1; i < size; i++) { // 2 * (n - 1) ops
if (nums[i] > max) { // 2 * (n - 1) ops
max = nums[i]; // 0 to (n - 1) ops
}
}
return max; // 1 op
} // total: 5n ---> runs in O(n)
- 简单递归程序分析:
public int fac(int n) { // n >= 0
if (n == 0) return 1;
return n * fac(n - 1);
}
T(n) = T(n - 1) + b
T(0) = c
推出通项公式T(n) = c + nb
,所以时间复杂度为O(n)
其他简单推论:递归输入T(n) = T(n/2) + c
的时间复杂度为O(logn)
, T(n) = 2T(n - 1) + c
时间复杂度为O(2^n)
,如前面的汉诺塔
2. 测量时间代码
纳秒
long startTime = System.nanoTime();
Arrays.sort(arr);
long timeElapsed = System.nanoTime() - startTime;
System.out.println("Execution time in nanosecond: " + timeElapsed);
微秒
long startTime = System.currentTimeMillis();
Arrays.sort(arr);
long timeElapsed = System.currentTimeMillis() - startTime;
// convert to seconds
// long timeElapsedSecond = TimeUnit.MILLISECONDS.toSeconds(timeElapsed);
System.out.println("Execution time in nanosecond: " + timeElapsed);
>>> timeit('x.append(1)','x=[]', number = 10000)
0.0014090538024902344
3. 时间复杂度和input size的关系
简单加法O(n)级别不同input size的运行时间测试:
public class runtimeTest {
public static double sum(double n) {
long start = System.currentTimeMillis();
long sum = 0;
for (long j = 0; j < n; j++) { // 注意这边j是long,否则会造成死循环(整数溢出)
sum += 1;
}
return (System.currentTimeMillis() - start) / 1000.0;
}
public static void main(String[] args) {
for (int i = 1; i < 11; i++) {
double n = Math.pow(10, i);
double time = 0;
double count = 1;
for (int j = 0; j < 10; j++) {
time += sum(n);
}
System.out.println("10^" + i + ": " + time / count + " s");
}
}
}
// 10^1: 0.0 s
// 10^2: 0.0 s
// 10^3: 0.0 s
// 10^4: 0.001 s
// 10^5: 0.002 s
// 10^6: 0.013000000000000003 s
// 10^7: 0.13499999999999998 s
// 10^8: 1.366 s
// 10^9: 11.797 s
// 10^10: 119.626 s
结论:从10^5开始,时间10倍增加。
程序运行时间1s以下的输入数据规模:
O(n): < 10 ^ 8
O(n^2) < 10 ^ 4 (x ^ 2 = 10 ^ 8)
O(n*logn) < 10 ^ 7 (x * log_7^x = 10 ^ 8)
O(n^2)算法参见454. 4Sum II
实际问题的数据规模需要在这个基础上再除以10或2
算法的时间复杂度和输入规模的关系(假设运行时间上限为1s):
- 如果输入规模为1000以下,那么设计O(n^2)级别的算法没有问题。所以排序算法在输入规模较小时可以改用O(n^2)的排序算法进行优化。
- 如果输入规模在10^8左右,那么需要设计O(n)或者O(nlogn)级别的算法
4. 关于O(log(n))的区别
log_2 : 二叉树
log_10 : 求整数位数
根据log换底公式log_b^N = log_b^a * log_a^N
,log底数的转换为常数倍,所以不管底是多少,时间复杂度都是O(logn)
5. 循环的形式和时间复杂度的关系
简单循环的时间复杂度
public static void runDiffTimeComp() {
int[] arr = {1, 2, 3, 4};
reverse(arr);
arr = new int[]{1, 2, 3, 4, 5};
reverse(arr);
sort(arr);
binarySearch(arr, 2);
reverseNumber(123);
test(10);
System.out.println(isPrime(10));
}
// swap
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// O(n) reverse array
public static int[] reverse(int[] arr) {
int len = arr.length;
for (int i = 0; i < len / 2; i++) {
swap(arr, i, len - 1 - i);
}
return arr;
}
// O(n)
public static void print(int[] arr) {
int len = arr.length;
for (int i = 0; i < len; i++) {
for (int j = 0; j < 10; j++) {
System.out.println(arr[i] + " " + j);
}
}
}
// O(n^2): (n - 1) + (n - 2) + .. + 0 = (n - 1)n/2
public static int[] bsort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 1; j < arr.length - i; j++) {
if (arr[j - 1] > arr[j]) {
swap(arr, j, j - 1);
}
}
}
return arr;
}
// O(n^2): selection sort
public static int[] ssort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] > arr[i])
swap(arr, j, i);
}
}
return arr;
}
// O(logn): log_2^n
public static boolean binarySearch(int[] arr, int target) {
int l = 0;
int r = arr.length;
while (l < r) {
int mid = (l + r) >>> 1;
if (arr[mid] == target) {
return true;
} else if (arr[mid] < target)
l = mid + 1;
else
r = mid;
}
return false;
}
// O(logn): log_10^n
public static int reverseNumber(int num) {
int n = num;
int res = 0;
while (n > 0) {
int r = n % 10;
n = n / 10;
res = res * 10 + r;
}
return res;
}
// O(nlogn)
public static void test(int n) {
for (int i = 1; i < n; i += i) // O(log(n))
for (int j = 0; j < n; j++) // O(n)
System.out.print(i + " "); // 1.., 2.., 4.., 8..
}
// O(sqrt(n))
public static boolean isPrime(int n) {
for (int i = 2; i * i < n; i++) {
if (n % i == 0) return false;
}
return true;
}
6. 两倍input size增加测试时间复杂度
input size每次增加两倍,观察runtime的改变
public static int[] generateRandomArray(int len, int l, int r) {
Random rand = new Random();
int[] arr = new int[len];
for (int i = 0; i < len; i++) {
arr[i] = rand.nextInt(r - l + 1) + l;
}
return arr;
}
public static void testTwoTimesIncrement(String name, int a, int b) throws Exception {
Method method = runtimeTest.class.getMethod(name, int[].class);
for (int i = a; i < b; i++) {
int n = (int) Math.pow(2, i); // 2, 4, 8, 16, 32, 64, ...
int[] arr = generateRandomArray(n, 0, 10000000);
long start = System.currentTimeMillis();
method.invoke(null, arr);
double end = (System.currentTimeMillis() - start) / 1000.0;
System.out.println("i: " + i + ", input size: " + n + ", runtime: " + String.format("%10.6e", end) + " s");
}
}
public static void main(String[] args) throws Exception {
testTwoTimesIncrement("reverse", 15, 29);
testTwoTimesIncrement("ssort", 10, 17);
// change method param signature.
testTwoTimesIncrement1("binarySearch", 20, 28);
}
// O(n) reverse() 2 times increment
//i: 22, input size: 4194304, runtime: 1,0e-03 s
//i: 23, input size: 8388608, runtime: 5,0e-03 s
//i: 24, input size: 16777216, runtime: 8,0e-03 s
//i: 25, input size: 33554432, runtime: 1,7e-02 s
//i: 26, input size: 67108864, runtime: 2,8e-02 s
//i: 27, input size: 134217728, runtime: 5,5e-02 s
//i: 28, input size: 268435456, runtime: 1,1e-01 s
// O(n^2) ssort() 4 times increment
//i: 10, input size: 1024, runtime: 6,0e-03 s
//i: 11, input size: 2048, runtime: 9,0e-03 s
//i: 12, input size: 4096, runtime: 4,5e-02 s
//i: 13, input size: 8192, runtime: 1,3e-01 s
//i: 14, input size: 16384, runtime: 4,8e-01 s
//i: 15, input size: 32768, runtime: 1,7e+00 s
//i: 16, input size: 65536, runtime: 6,5e+00 s
//i: 17, input size: 131072, runtime: 2,7e+01 s
//O(logn) binary search slowly increment: (log2n/logn = 1 + log2/logn)
//i: 20, input size: 1048576, runtime: 6,1e+04 ns
//i: 21, input size: 2097152, runtime: 3,3e+04 ns
//i: 22, input size: 4194304, runtime: 2,9e+04 ns
//i: 23, input size: 8388608, runtime: 3,3e+04 ns
//i: 24, input size: 16777216, runtime: 4,1e+04 ns
//i: 25, input size: 33554432, runtime: 4,3e+04 ns
//i: 26, input size: 67108864, runtime: 3,8e+04 ns
//i: 27, input size: 134217728, runtime: 5,4e+04 ns
结论:O(logn)的时间复杂度比O(n)小得多,O(nlogn)相比O(n^2)时间复杂度小的多
7. 均摊时间复杂度分析(ArrayList为例)
import java.util.Arrays;
public class MyArrayList {
private int cap;
private int[] arr;
private int pointer;
public MyArrayList(int cap) {
this.cap = cap;
pointer = 0;
arr = new int[cap];
}
public void add(int e) {
if (pointer >= cap) resize(cap * 2);
arr[pointer++] = e;
}
private void resize(int size) {
System.out.println("resize");
int[] newArr = new int[size];
for (int i = 0; i < pointer; i++) {
newArr[i] = arr[i];
}
cap = size;
arr = newArr;
}
public int pop() {
assert (pointer >= 0);
pointer--;
int res = arr[pointer];
if (pointer <= cap / 4) resize(cap / 2); // change if condition from cap/2 to cap/4(刚好比1/4多一个元素的时候resize)
return res;
}
private void getArray() {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
MyArrayList list = new MyArrayList(3);
for (int i = 0; i < 9; i++) {
list.add(i);
list.getArray();
}
for (int i = 0; i < 9; i++) {
System.out.println("pointer: " + list.pointer + ", pop :" + list.pop());
list.getArray();
}
}
}
某个时间复杂度较高的方法均摊到其他操作中,如MyArrayList中的resize()方法平分到每一次操作之后,add()的时间复杂度仍然为O(1)。
注意pop的条件是pointer <= cap / 4
, 防止复杂度震荡(在同一个点不断调用resize())
output:
[0, 0, 0]
[0, 1, 0]
[0, 1, 2]
resize
[0, 1, 2, 3, 0, 0]
[0, 1, 2, 3, 4, 0]
[0, 1, 2, 3, 4, 5]
resize
[0, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0]
[0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0]
pointer: 9, pop :8
[0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0]
pointer: 8, pop :7
[0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0]
pointer: 7, pop :6
[0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0]
pointer: 6, pop :5
[0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0]
pointer: 5, pop :4
[0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0]
resize
pointer: 4, pop :3
[0, 1, 2, 0, 0, 0]
pointer: 3, pop :2
[0, 1, 2, 0, 0, 0]
resize
pointer: 2, pop :1
[0, 0, 0]
resize
pointer: 1, pop :0
[0]