虽然写完程序在计算机上跑一遍。就知道代码执行的效率,但这都属于事后统计法,不同配置的平台处理效率各不相同,局限性太大,要想编写出能高效运行的程序,我们就需要考虑到算法的效率,分析算法的复杂度。算法复杂度包括时间复杂度,和空间复杂度,下面我们重点看下时间复杂度。
T(n)=O(f(n))
T(n):代码执行时间(估计)
f(n): 代码执行的总次数
T(n)=O(f(n)) 表示代码执行的时间和代码执行的总次数成正比。
下面来看一个例子:
int print(int n) {
for(int i = 0; i<n; i++) { // 需要执行 (n + 1) 次
System.out.println("Hello, Algorithm!"); // 需要执行 n 次
}
return 0; // 需要执行 1 次
}
上面方法中代码执行总次数f(n) = (n+1+n+1),执行总时间T(n)=O(2n+2),这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模n增长的变化趋势。
大 O 这种复杂度表示方法只是表示一种变化趋势。我们通常会忽略掉公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了。所以下面看看常见时间复杂度的分析方法。
1.加法法则:总复杂度等于量级最大的那段代码的复杂度
如果 T1(n)=O(f(n)),T2(n)=O(g(n)),那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).
void print(int n) {
// 第一部分时间复杂度为 O(n^2)
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
System.out.println("Hello, Algorithm!");
}
}
// 第二部分时间复杂度为 O(n)
for(int j = 0; j < n; j++) {
System.out.println("Hello, Algorithm!");
}
}
此例时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。
2. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))
void print(int n) {
for(int i = 0; i < n; i++) { //时间复杂度为 O(n)
f(i)
}
}
void f(int n) {
for(int i = 0; i < n; i++) { //时间复杂度为 O(n)
System.out.println("Hello, Algorithm!");
}
}
我们单独看 print() 函数。假设 f() 只是一个普通的操作,那print()函数的时间复杂度就是,T1(n) = O(n)。但 f() 函数本身不是一个简单的操作,它的时间复杂度是 T2(n) = O(n),所以,整个 print() 函数的时间复杂度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n^2)
3. 只关注循环执行次数最多的一段代码
我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。
void print(int n) {
if (n >= 0) {
// 第一条路径时间复杂度为 O(n^2)
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
System.out.println("输入数据大于等于零\n");
}
}
} else {
// 第二条路径时间复杂度为 O(n)
for(int j = 0; j < n; j++) {
System.out.println("输入数据小于零\n");
}
}
}
此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)
时间复杂度分析的基本策略是:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。
需要注意的点:
看到是双层循环,不一定都是O(n^2)级别的算法。
public class Main {
// O(1)
/*下面这个方法是交换一个数组中的两个元素,是一个常数阶算法,
因为没有涉及到数据规模的变化*/
private static void swap(Object[] arr, int i, int j){
if(i < 0 || i >= arr.length)
throw new IllegalArgumentException("i is out of bound.");
if(j < 0 || j >= arr.length)
throw new IllegalArgumentException("j is out of bound.");
Object temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// O(n)
/*下面这个方法是一个典型的O(n)级别的算法,
O(n)级别的算法特征是包含一个循环,且循环的次数是和n相关的,
代码执行次数可以表示为cn次,其中c是一个常数。*/
private static int sum(int n){
if(n < 0)
throw new IllegalArgumentException("n should be greater or equal to zero.");
int ret = 0;
for(int i = 0 ; i <= n ; i ++)
ret += i;
return ret;
}
//下面这个方法是将以一个字符串数组翻转过来,
//就是将第一个元素和最后一个元素交换位置,只需执行n/2次。
//所以也是O(n)级别的算法
private static void reverse(Object[] arr){
int n = arr.length;
for(int i = 0 ; i < n / 2 ; i ++ )
swap(arr, i, n - 1 - i);
}
// O(n^2) Time Complexity
/*下面这个方法是选择排序,是一个O(n^2)级别的算法,
O(n^2)级别的算法的特征是包含一个双重循环
执行次数分析:(n-1)+(n-2)+(n-3)+(n-4)=1/2*n^2-1/2*n,
结合前面的分析。得出这是一个O(n^2)级别的算法*/
private static void selectionSort(Comparable[] arr, int n){
for(int i = 0 ; i < n ; i ++){
int minIndex = i;
for(int j = i + 1 ; j < n ; j ++)
if(arr[j].compareTo(arr[minIndex]) < 0)
minIndex = j;
swap(arr, i, minIndex);
}
}
// O(n) Time Complexity
/*但双重循环未必是O(n^2)级别的算法,如下面这个方法,
内循环就是和n无关的,是一个固定次数的循环。
总的执行次数为30n次,所以是O(n) 级别的算法*/
private static void printInformation(int n){
for( int i = 1 ; i <= n ; i ++ )
for( int j = 1 ; j <= 30 ; j ++ )
System.out.println("Class " + i + " - " + "No. " + j);
}
// O(logn) Time Complexity
/*二分查找法
在有序数组中,我们要找到一个目标值,数组长度为n,
我们首先在n个元素中查找,比较数组中间位置的那个元素,
之后在n/2个元素中查找,再到n/4个元素中查找...每次扔掉一半的元素,
最终在1个元素中查找。那么最多执行多少次呢,
即就是n经过几次“除以2”操作等于1.由高中数学可知需要经过log2n次。
所以时间复杂度O(log2n),即为O(logn)。
注意:不管是以2为底、以3为底,还是以10为底,
我们可以把所有对数阶的时间复杂度都记为 O(logn)。为什么呢?
因为他们之间通过一个常数系数便可互相转化。
而我们采用大O法标记时间复杂度时忽略系数。(ps:注释中的log2n,2为底数)*/
private static int binarySearch(Comparable[] arr, int n, int target){
int l = 0, r = n-1;
while( l <= r ){
int mid = l + (r-l)/2;
if(arr[mid].compareTo(target) == 0) return mid;
if(arr[mid].compareTo(target) > 0) r = mid - 1;
else l = mid + 1;
}
return -1;
}
private static String intToString(int num){
StringBuilder s = new StringBuilder("");
String sign = "+";
if(num < 0){
num = -num;
sign = "-";
}
while(num != 0){
s.append(Character.getNumericValue('0') + num % 10);
num /= 10;
}
if(s.length() == 0)
s.append('0');
s.reverse();
if(sign == "-")
return sign + s.toString();
else
return s.toString();
}
// O(nlogn)
private static void hello(int n){
for( int sz = 1 ; sz < n ; sz += sz )
for( int i = 1 ; i < n ; i ++ )
System.out.println("Hello, Algorithm!");
}
// O(sqrt(n)) Time Complexity
//经过sqrt(x)次操作等于num,所以时间复杂度是O(sqrt(n))
private static boolean isPrime(int num){
for(int x = 2 ; x*x <= num ; x ++)
if( num % x == 0 )
return false;
return true;
}
private static boolean isPrime2(int num){
if( num <= 1 ) return false;
if( num == 2 ) return true;
if( num % 2 == 0 ) return false;
for(int x = 3 ; x * x <= num ; x += 2)
if( num%x == 0 )
return false;
return true;
}
public static void main(String[] args) {
System.out.println(intToString(123));
System.out.println(intToString(0));
System.out.println(intToString(-123));
System.out.println();
if(isPrime2(137)) System.out.println("137 is a prime.");
else System.out.println("137 is not a prime.");
if(isPrime2(121)) System.out.println("121 is a prime.");
else System.out.println("121 is not a prime.");
}
}
ps(关注公众号,回复:黑客帝国,获取彩蛋)