目录
研究算法的最终目的就是如何花更少的时间,如何占用更少的内存去完成相同的需求。
有关算法时间耗费分析,我们称之为算法的时间复杂度分析,有关算法的空间耗费分析,我们称之为算法的空间复杂度分析。
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
-
- int sum = 0;
- int n = 100;
- for (int i = 1; i <= n; i++) {
- sum += i;
- }
- System.out.println("sum=" + sum);
-
- long end = System.currentTimeMillis();
- System.out.println(end - start);
- }
在计算机程序编写前,依据统计方法对算法进行估算,经过总结,程序在计算机上运行所消耗的时间取决于下列因素:
1、算法采用的策略和方案;
2、编译产生的代码质量;
3、问题的输入规模(所谓的问题输入规模就是输入量的多少);
4、机器执行指令的速度;
由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。如果算法固定,那么该算法的执行时间就只和问题的输入规模有关系了。
需求:计算1到100的和。
第一种解法:
- //如果输入量n为1次,则需要计算1次
- //如果输入量n为1亿次,则需要计算1亿次
- public static void main(String[] args) {
- int sum = 0;//执行1次
- int n = 100;//执行1次
- for (int i = 1; i <= n; i++) {//执行n+1次
- sum += i;//执行n次
- }
- System.out.println("sum=" + sum);
- }
第二种解法:
- //如果输入量n为1次,则需要计算1次
- //如果输入量n为1亿次,则需要计算1次
- public static void main(String[] args) {
- int sum = 0;//执行1次
- int n = 100;//执行1次
- sum = (n + 1) * n / 2;//执行1次
- System.out.println("sum=" + sum);
- }
因此,当输入规模为n时,第一种算法执行了1+1+(n+1)+n=2n+3次;第二种算法执行1+1+1=3次。如果我们把第一种算法的循环体看做是一个整体,忽略结束条件的判断,那么其实这两个算法运行时间的差距就是n和1的差距。
需求:计算100个1+100个2+100个3+....100个100的结果
- public static void main(String[] args) {
- int sum = 0;
- int n = 100;
- for (int i = 1; i <= n; i++) {
- for (int j = 1; j <= n; j++) {
- sum += i;
- }
- }
- System.out.println("sum=" + sum);
- }
最重要的就是把核心操作的次数和输入规模关联起来。

概念:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐近快于g(n)。
测试一:
| 规模 | 算法A1(2n+3)执行次数 | 算法A2(2n)执行次数 | 算法B1(3n+1)执行次数 | 算法B2(3n)执行次数 |
| n=1 | 5 | 2 | 4 | 3 |
| n=2 | 7 | 4 | 7 | 6 |
| n=3 | 9 | 6 | 10 | 9 |
| n=10 | 23 | 20 | 31 | 30 |
| n=100 | 203 | 200 | 301 | 300 |
结论:
当输入规模n>2时,算法A1的渐近增长小于算法B1的渐近增长。
随着输入规模的增大,算法的常数操作可以忽略不计。
测试二:
| 规模 | 算法C1(4n+8)执行次数 | 算法C2(n)执行次数 | 算法D1(2n^2+1)执行次数 | 算法D2(n^2).... |
| n=1 | 12 | 1 | 3 | 1 |
| n=2 | 16 | 2 | 9 | 4 |
| n=3 | 20 | 3 | 19 | 9 |
| n=10 | 48 | 10 | 201 | 100 |
| n=100 | 408 | 100 | 20001 | 10000 |
| n=1000 | 4008 | 1000 | 2000001 | 1000000 |
结论:
随着输入规模的增大,与最高次项相乘的常数可以忽略。
测试三:
| 规模 | 算法E1(2n^2+3n+1)执行次数 | 算法E2(n^2)执行次数 | 算法F1(2n^3+3n+1).. | 算法F2(n^3).. |
| n=1 | 6 | 1 | 6 | 1 |
| n=2 | 15 | 4 | 23 | 8 |
| n=3 | 28 | 9 | 64 | 27 |
| n=10 | 231 | 100 | 2031 | 1000 |
| n=100 | 20301 | 10000 | 2000301 | 1000000 |
结论:
最高次项指数大的,随着n的增长,结果也会变得增长特别快。
测试四:
| 规模 | 算法G(n^3)执行次数 | 算法H(n^2)执行次数 | 算法I(n).. | 算法J(logn).. | 算法K(1).. |
| n=2 | 8 | 4 | 2 | 1 | 1 |
| n=4 | 64 | 16 | 4 | 2 | 1 |
结论:
算法函数中n最高次幂越小,算法效率越高。
1、算法函数中的常数可以忽略;
2、算法函数中最高次幂的常数因子可以忽略;
3、算法函数中最高次幂越小,算法效率越高;
定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规律n的函数,进而分析T(n)随着n的变化情况并确定T(n)的量级。算法的时间复杂度,就是算法的时间量度,记作:T(n)=O(f(n))。它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模n的某个函数。
在这里,我们需要明确一个事情:执行次数=执行时间
用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法。
算法一:
- public static void main(String[] args) {
- int sum = 0;//执行1次
- int n = 100;//执行1次
- sum = (n + 1) * n / 2;//执行1次
- System.out.println("sum=" + sum);
- }
算法二:
- public static void main(String[] args) {
- int sum = 0;//执行1次
- int n = 100;//执行1次
- for (int i = 1; i <= n; i++) {
- sum += i;//执行n次
- }
- System.out.println("sum=" + sum);
- }
算法三:
- public static void main(String[] args) {
- int sum = 0;//执行1次
- int n = 100;//执行1次
- for (int i = 1; i <= n; i++) {
- for (int j = 1; j <= n; j++) {
- sum += i;//执行n^2次
- }
- }
- System.out.println("sum=" + sum);
- }
以上算法执行次数:
算法一:3次
算法二:n+2次
算法三:n^2+2次
如果用大O记法表示上述每个算法的时间复杂度,应该如何使用呢?基于我们对函数渐近增长分析,推导大O阶的表示法有以下几个规则可以使用:
1、用常数1取代运行时间中的所有加法常数;
2、在修改后的运行次数中,只保留最高阶项;
3、如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数;
所以上述算法的大O记法分别为:
算法一:O(1)--常数阶
算法二:O(n)----线性阶
算法三:O(n^2)----平方阶
1、线性阶--O(n)
2、平方阶--O(n^2)2次for循环
3、立方阶--O(n^3)--3次for循环
4、对数阶--O(logn)
- int i=1,n=100;
- while(i<n){
- i=i*2;
- }
由于每次i*2之后,就距离n更近一步,假设有x个2相乘后大于n,则会退出循环。由于是2^x=n,得到x=log(2)n,所以这个循环的时间复杂度为O(logn);
对于对数阶,由于随着输入规模n的增大,不管底数为多少,它们的增长趋势是一样的,所以我们会忽略底数。
| 规模规模 | log(2)n | log(4)n | log(8)n |
| 8 | 3 | 1.5 | 1 |
| 64 | 6 | 3 | 2 |
| 512 | 9 | 4.5 | 3 |
| 4096 | 12 | 6 | 4 |
| 16777216 | 24 | 12 | 8 |
| 134217728 | 27 | 13.5 | 9 |
| 2.815E+14 | 48 | 24 | 16 |
| 7.923E+28 | 96 | 48 | 32 |
| 6.28E+57 | 192 | 96 | 64 |
| 3.94E+115 | 384 | 192 | 128 |
| 1.55E+231 | 768 | 384 | 256 |
5、常数阶--O(1)
| 描述 | 增长的数量级 | 说明 | 举例 |
| 常数级别 | 1 | 普通语句 | 两个数相加 |
| 对数级别 | logn | 二分策略 | 二分查找 |
| 线性级别 | n | 循环 | 找出最大元素 |
| 线性对数级别 | nlogn | 分治思想 | 归并排序 |
| 平方级别 | n^2 | 双层循环 | 检查所有元素对 |
| 立方级别 | n^3 | 三层循环 | 检查所有三元组 |
| 指数级别 | 2^n | 穷举查找 | 检查所有子集 |
它们的时间复杂度从低到高依次为:O(1)
从平方阶开始,随着输入规模的增大,时间成本会急剧增大;所以,我们的算法,尽可能的追求是
O(1) 案例一: 时间复杂度:O(n) 案例二: 时间复杂度:O(n^2) 案例三: 时间复杂度:O(n^2)-----n+n^2+n^2=2n^2+n根据大O推导规则,最终时间复杂度:O(n^2) 最好情况:O(1) 最坏情况:O(n) 平均情况:O(n/2) 早期计算机软硬件的发展漫长过程中,512K-->1M-->2M-->4M...等.发展到现在8G,16G,32G。早期,算法在运行过程中对内存的占用情况是需要考虑的问题,我们可以用算法的空间复杂度来描述算法对内存的占用。 1、基本数据类型内存占用情况 2、计算机访问内存的方式都是一次一个字节 3、一个引用(机器地址)需要8个字节表示 4、创建一个对象,比如new Date();除了Date对象内存的数据(年月日等信息占用的内存),该对象本身也有内存开销,每个对象自身开销是16个字节,用来保存对象的头信息。 5、一般内存的使用,如果不够8个字节,都会被自动填充为8字节 6、Java中数组被限定为对象,它们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组,一般需要24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存。 算法的空间复杂度计算公式记作:S(n)=O(f(n)),其中n为输入规模,f(n)为语句关于n所占存储空间的函数。 案例:对指定的数组元素进行反转,并返回反转的内容。 解法一: 解法二: 解法一:O(1)----4+4=8 解法二:O(n)----4+4n+24=4n+28 天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。1.4、函数调用的时间复杂度
1.5、最坏情况
2、算法的空间复杂度分析
2.1、Java中常见内存占用
数据类型 内存占用字节数 byte 1 short 2 int 4 long 8 float 4 double 8 boolean 1 char 2 2.2、算法的空间复杂度