算法时间复杂度、空间复杂度(大O表示法)
什么是算法?
计算机是人的大脑的延伸,它的存在主要是为了帮助我们解决问题。
而算法在计算机领域中就是为了解决问题而指定的一系列简单的指令集合。不同的算法需要不同的资源,例如:执行时间或消耗内存。
如果一个算法执行时间需要好几年或者需要占用非常大的内存,那么这算法几乎毫无用处,即使有价值使用场景也非常有限。
因此,一般上我们讨论一个算法的优劣的时候可以通过时间和空间两个维度来衡量,也就是常说的:
1、时间复杂度;
2、空间复杂度;
我们当然希望执行时间和消耗内存都越少越好,但很多时候其实我们无法同时兼顾,需要在时间和空间之间做一定的取舍达到平衡。
时间复杂度
一般上,如果我们要衡量一个程序片段的执行时间,我们会把程序运行一次并打印时间,这是最常见也是最简单的方式。
这种方式存在一些问题:
1、不同的计算机会产生不同的执行时间,甚至于相同的计算机也会产生不同的时间,根据计算机当前的情况而定;
2、通常可能我们使用很小的数据量来测量,但一个算法随着数据量的不同性能变化是不同的,所以小数据量衡量的时间不见得适用于大数据量;
3、甚至于有时候一个算法压根无法直接通过运行来测试时间。
为了解决这些问题,引入了数学领域中的 “大O标记法”
大O标记法
数学概念:如果存在正常数c和n,使得当N≥n的时候T(N)≤cf(N),则标记为T(N)=O(f(N))
数学概念看起来有些费解,我们可以把T(N)=1000N把f(N)=N2,当N=1000,c=1的时候,1000N=N2。而当N>1000的时候,N2>1000N。
也就是说,当N无限大的时候,N2的值将必定大于1000N的值,也就是说1000N这个函数的值不会超过N2,或者说N2是1000N的上界,它限定了1000N的最大值。
如果我们考虑一个算法最糟糕的时候会执行多久,则大O标记法很轻易就能表示出来。
下面我来看一个例子,关于大O标记法怎么衡量算法时间:
1 int total = 0; 2 for (int i = 0; i < n ; i++ ) { 3 total += i; 4 }
我们计算一下代码的执行时间:
1、第1行有一个赋值操作记1个单位时间;
2、第2行有一个赋值操作记1个单位时间,一个比较操作记n+1个单位时间,自增运算记n个单位时间,合计:2n+2个时间;
3、第3行在循环体中执行n次,我们记为2*n个单位时间;
以上代码合计时间为:4n+3,也就是 T(n) = O(4n+3),O(4n+3)表示程序的运行时间上界。如果n无限大的时候,我们忽略倍数4和常数3,则T(n)的运行时间为O(n);
我们也可以说,以上代码的时间复杂度为O(n)。
上面的例子中,我们分析了每一行的代码运行时间,并最终得出时间复杂度为O(n),但一个算法的复杂度有时候让我们难以像上面这样每一步都去计算并合计时间,由此我们可以得出一些简单的原则来计算时间
1、常数阶O(1)
无论代码执行多少行,只要代码中没有for等循环结构,那么复杂度就是O(1),如:
1 int i = 1; 2 int j = 2; 3 ++i; 4 j++; 5 int m = i + j;
无论程序中数据量有多少,都不影响代码的运行时间,则为常数阶。
2、线性阶O(N)
如果存在一个循环体,那么循环n次,则复杂度为O(N),如:
1 for(i=1; i<=n; ++i) 2 { 3 j = i; 4 j++; 5 }
3、对数阶O(logN)
线性阶O(N)的情况是循环了N次,所以对数阶的情况就是循环了logN次,如:
1 int i = 1; 2 while(i<n) 3 { 4 i = i * 2; 5 }
我们假设while循环体在循环了x次之后退出循环,那么也就是i*2x≥n,时间复杂度的上界也就是log2n=x,我们标记为O(logN)。
4、线性对数O(NlogN)
对数阶是logN,那么nlogN即使把对数阶循环n次,如:
1 for(m=1; m<n; m++) 2 { 3 i = 1; 4 while(i<n) 5 { 6 i = i * 2; 7 } 8 }
5、平方阶O(N2)
平方阶很容易理解,嵌套循环即是,如:
1 for(x=1; i<=n; x++) 2 { 3 for(i=1; i<=n; i++) 4 { 5 j = i; 6 j++; 7 } 8 }
根据平方阶可以推出,立方阶O(N3),k次方阶O(Nk),或者O(n * m)
空间复杂度
时间复杂度粗略估计了执行时间的上界,空间复杂度也是类似的,我们看几个常见的示例:
1、空间复杂度O(1)
和时间复杂度一样,一个随着数据量的变化内存消耗不变化的时候,我们认为空间复杂度为常数也就是O(1),如:
1 int i = 1; 2 int j = 2; 3 ++i; 4 j++; 5 int m = i + j;
2、空间复杂度O(N)
随着数据量变化,内存消耗呈线性变化的时候,我们称之为O(N),如:
1 int[] m = new int[n] 2 for(i=1; i<=n; ++i) 3 { 4 j = i; 5 j++; 6 }
这里的数组m随着数据量n的变化线性增长。
3、空间复杂度O(N2)
随着数据量的变化,内存消耗为平方变化,如:
1 int[][] arr = new int[n][n]; 2 for (int i = 0; i < n; i++) { 3 for (int j = 0; j < n; j++) { 4 arr[i][j] = new Random().nextInt(); 5 } 6 }
以上二维数组中,当n+1的时候,arr数组大小从n*n变为了(n+1)*(n+1),其它空间复杂度以此类推
总结
其实无论是时间复杂度还是空间复杂度,都是在考虑当n变化的时候时间或者空间会呈什么样的变化,并最终确定时间和空间的上界问题。
考虑时间复杂度的时候,我们简化为思考n+1的时候,循环次数如何变化,如果不变则O(1),线性则O(N),对数则log(N)...以此类推。也就是说把n当作问题规模,当n变化的时候,执行次数的变化呈现什么规律。
考虑空间复杂度的时候,我们简化为思考n+1的时候,内存消耗的数量如何变化,如果不变则为O(1),线性则为O(N),平方则为O(N2)...以此类推。也就是说当n变化的时候,内存消耗的变化呈现什么规律。
本文讨论的是在n不断增长到无限大最糟糕的情况下时间与空间复杂度的问题,但我们程序中不是每个算法都需要考虑n无限大问题,也就是说如果我们的n是有限的且是很小的值我们甚至完全可以不考虑它的执行时间或者空间问题。
参考文章:https://blog.csdn.net/jsjwk/article/details/84315770