TimeComplexityAndSpaceComplexity - 时间复杂度和空间复杂度- Java

x33g5p2x  于2021-11-22 转载在 Java  
字(5.4k)|赞(0)|评价(0)|浏览(310)

算法效率

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被
    称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额
    外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的
    迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复
    杂度。

 

时间复杂度

时间复杂度的概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个
    算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但
    是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,且不现实。所以才有了时间复杂度这个分析方
    式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复
    杂度。

大O 的渐进表示法

让我们通过代码,来了解它

当我们看到func1方法时,首先,要找到运行次数最多的语句

public class TimeComplexityAndSpaceComplexity {
    public static  void func1(int N){
        int count = 0;
        for (int i = 0; i < N ; i++) {
            for (int j = 0; j < N ; j++) {
                count++;// 第一个就是 这个 count++;,它执行了多少次?
 // 两个for嵌套,两个for循环N次,最外围的for循环,每遍历一个数据,嵌套在内部的for循环,就要循环 N 次。
                //即 count++; 语句 被循环执行 N*N 次,
                // 小技巧,找执行次数最多的语句,你就看哪里有循环就行了。
            }
        }
        for (int k = 0; k < 2 * N ; k++) {
            count++;// 这里的 count++; 被执行了 2*N
        }
        int M = 10;
        while ((M--) > 0) {
            count++;// 这里count++; 被执行了 10次
        }
        System.out.println(count);
    }
}
经过我们分析,这句代码在程序中 一共被执行了 F(N) = N^2 + 2N + 10
 在座的各位,跟着我思考一个问题:
如果我们的 N 越来越大
比如:
N = 10 F(N) = 10^2 + 20 + 10 == 100 + 30 == 130
N = 100 F(N) = 100^2 + 200 + 10 == 10000 + 210 == 10210
N = 1000 F(N) = 1000^2 + 2000 + 10 == 1000000 + 2010 == 1002010
 --- ---- ---
---- ----  ---

 按照这样想法,后面的 2N + 10,随着N增大,就显得微不足道。

在实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里 我们使用大O的渐进表示法。

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

推导大O阶方法:

以func1的时间复杂度为例: F(N) = N^2 + 2N + 10
    
    1、用常数1取代运行时间中的所有加法常数。{F(N)= N^2 +2N +1}
    2、在修改后的运行次数函数中,只保留最高阶项。{F(N)=N^2}
    
    3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶
   假设 使用完方法 1 和 2,把该做的都做了,还剩 3* N^2,
 此时,按照方法3的规则去操作(去掉3*),最终的时间复杂度为 O(N^2),
即分析这个代码的时间复杂度,在使用大O的渐进表示法以后

Func1的时间复杂度为 O(N^2).

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

另外有些算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)
    平均情况:任意输入规模的期望运行次数(根据代码的情况给出平均时间复杂度)
    最好情况:任意输入规模的最小运行次数(下界)
    例如:在一个长度为N数组中搜索一个数据x
    最好情况:1次找到
    最坏情况:N次找到
    平均情况:N/2次找到(可以这么理解,在中间的位置找到的。)

&ensp;

在实际中一般情况关注的是算法的最坏运行情况,

举个例子:

假设数组中有N个元素,现在我们要在数组中查找一个元素,最坏的情况,该元素处在数组末尾,
 也就是说 需要遍历整个数组元素。
 故:数组中搜索数据时间复杂度为O(N)

再来看几个代码案例

案例1

public class TimeComplexityAndSpaceComplexity {
    public static  void func(int N){
            int count = 0;
            for (int k = 0; k < 2 * N ; k++) {
                count++;// 2*N
            }
            int M = 10;
            while ((M--) > 0) {
                count++;// 10
            }
            System.out.println(count);
        }
    }
  时间复杂度为 F(N) = 2*N + 10.使用大0渐进法来表示时间复杂度为 O(N)

案例 2

public class TimeComplexityAndSpaceComplexity {
    public static  void func(int N,int M) {
        int count = 0;
        for (int k = 0; k < M; k++) {
            count++;// M
        }
        for (int k = 0; k < N; k++) {
            count++;// N
        }
        System.out.println(count);
    }
}
  时间复杂度为 F(N) = N + M.使用大0渐进法来表示时间复杂度为 O(N + M)

&ensp;

案例3

public class TimeComplexityAndSpaceComplexity {
    public static  void func(int N) {
        int count = 0;
        for (int k = 0; k < 100; k++) {
            count++;// 100
        }
        System.out.println(count);
    }
}
  时间复杂度为 F(N) = 100.使用大0渐进法来表示时间复杂度为 O(1)

大家在分析时间复杂度时,一定要结合思想,不能光看代码。

接下来,我们来分析 几个复杂 的 时间复杂度 的 程序

冒泡排序

public class TimeComplexityAndSpaceComplexity {
    public  void bubbleSort(int[] array) {
        for (int end = array.length; end > 0; end--) {// 执行 length 次
            boolean sorted = true;// 暂不考虑优化(数组已经是有序的, 也就是说 程序在遍历一次判断一下,就结束了)
            // 最好情况:时间复杂度为 O(N)
            for (int i = 1; i < end; i++) { //length -1 次
                if (array[i -1]> array[i]) {
                Swap(array, i - 1, i);
                sorted = false;
                }
            }
            if (sorted == true) {
                break;
            }
        }
    }
}

时间复杂度是考虑最坏情况(每一个元素都需排序),不考虑优化情况和平均情况:
两个for循环嵌套length * (length - 1) == N(N-1) = N^2 - N == N^2
故 冒泡排序的空间复杂度为 O(N^2)

二分查找

public class TimeComplexityAndSpaceComplexity {
    int binarySearch(int[] array, int value) {
        int left = 0;
        int right = array.length - 1;
        while (left <= right) {
            int mid = (right + left) / 2;
            if (array[mid] < value) {
                left = mid + 1;
            } else if (array[mid] > value) {
                right = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
} 图1
图1

递归

时间的复杂度 = 递归的次数 * 每次递归执行的次数
比如说:我这个递归,递归了N次(N)。每次下去(N) 就是 一个循环,循环是循环了 N 次
 那么递归的时间的复杂度 为 N * N^2 = N^3

现在我们来看下面的程序(阶乘)

public class TimeComplexityAndSpaceComplexity {
    long factorial(int N) {
        return N < 2 ? N : factorial(N-1) * N;
    }
}

虽然我们不知道该程序的递归次数,但是程序每次递归下去的时候,遇到是三目运算符。
三目运算符 不是循环,所以 每次递归的次数为1次
再来看看 程序,如果我们求的是 4 的阶乘,也就是 N==4,

N<2,那么只有 N==1时,返回 1(终止递归),然后在看后面 factorial(N-1)
每次递归减一,那么 4,3,2,1 一共 4次
也就是说 递归的次数 为 N 次,
所以 时间的复杂度 = 递归的次数 * 每次递归执行的次数 == N * 1 == N
即 O(N)

计算斐波那契递归fibonacci的时间复杂度?

public class TimeComplexityAndSpaceComplexity {
    int fibonacci(int N) {
        return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
    }
}
还是一样,每次进来就是一个 三目运算符,每次递归执行的次数为1,
再来看 递归的次数
假设我们想求第四个斐波那契数,那么就需要我们去计算 第3 和 第4 个斐波那契数
因为斐波那契数,从第三位数开始,该数 等于 自身前两个数之和。

那么 第3个斐波那契数 ,需要 第1和第2个斐波那契数
第2个斐波那契数 需要 第1个斐波那契数,(因为N<2,是终止条件)
至于,第1个斐波那契数,就不需要再计算了(终止了)

第4个斐波那契数,需要 第3 和 第2个斐波那契数
第3个斐波那契数需要 第2 和 第1 个斐波那契数,第2个斐波那契数,需要第1个斐波那契数,(因为N<2,是终止条件)
至于,第2个斐波那契数,需要第一个斐波那契数 (因为N<2,是终止条件)
见 图 2

 斐波那契数的 时间复杂度 见图3
图2

图3

空间复杂度

空间复杂度是对一个算法在运行过程中 临时 占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间
因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度,类似,也使用大O渐进表示法。

冒泡排序 的 空间复杂度

public class TimeComplexityAndSpaceComplexity {
    public void bubbleSort(int[] array) {
        for (int end = array.length; end > 0; end--) {
            boolean sorted = true;
            for (int i = 1; i < end; i++) {
                if (array[i - 1] > array[i]) {
                    Swap(array, i - 1, i);
                    sorted = false;
                }
            }
            if (sorted == true) {
                break;
            }
        }
    }
}
 冒泡排序的整个运行的过程,没有说随着问题的规模,我们定义的变量也在增多。
 也就说 冒泡排序的空间复杂度为 O(1)
 有的人可能会说 sorted 在每次循环的时候,都会创建。
 但是请注意 sorted 变量只有1个,你不能说我们循环10次,定义了10个 sorted
 每次循环结束 变量空间是会被回收的,循环开始再创建一个。
 也就是说 sorted 自始至终都是一个。
 至于数组,不算。注意空间复杂度解释的题干中的 “临时” ,你可以理解为另外消耗的,或者说括号里的变量
 这么说吧,数组是冒泡排序必须品,而 sorted 只是临时创建的,为了辅助这个程序的运行
 所以”临时“的变量个数,只有sorted一个
 故 该冒泡排序的空间复杂度为 O(1)

计算fibonacci的空间复杂度

public class TimeComplexityAndSpaceComplexity {
    int[] fibonacci(int n) {// 计算斐波那契数的第N项
        long[] fibArray = new long[n + 1];
         n如果越大,数据就越大,你的结果要存入这个数组里(这里的n+1,是因为下标,自己品)
         下面的数组元素,最后都是要存入数组的,也就是说 下面生成的临时变量,都是存入数组的
         即元素个数,就是 临时变量的个数,即求斐波那契数 的 空间复杂度 为 O(N+1)
         再根据大O渐进法,空间复杂度的最终结果: O(N)
        fibArray[0] = 0;
        fibArray[1] = 1;
        for (int i = 2; i <= n ; i++) {
            fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
        }
        return fibArray;
    }
}

计算阶乘递归Factorial的时间复杂度?

public class TimeComplexityAndSpaceComplexity {
    long factorial(int N) {
        return N < 2 ? N : factorial(N-1)*N;
    }
}
 递归的空间复杂度有点不一样。
 每递归一次,函数就要在栈上开辟一块内存
 也就是说递归一次,空间复杂度为 O(1)
 递归N次,空间复杂度为 O(N)
 即 阶乘递归函数的 空间复杂度为 O(N)

想要熟练的分析时间和空间的复杂度还是需要多刷题,多练习。

本文结束

相关文章

微信公众号

最新文章

更多