2.4 时间复杂度分析 → 函数调用 && 最坏情况 && 空间复杂度分析
2.4.1 时间复杂度分析 → 函数调用
之前,我们分析的都是在单个函数内的,算法代码的时间复杂度。接下来我们 来分析一下 函数调用过程中时间复杂度。
- 案例①:
public static void main(String[] args)
{
int n = 100;
for(int i = 0;i < n;++i)
{
show();
}
}
private static void show(int i){
System.out.println(i);
}
函数调用的时间复杂度分析 与 循环和判断 的分析时一样的,它们都应该 被忽略掉 外壳,而只 考虑 内部的 核心代码。所以 当我们 调用函数的时候,不会去 考虑 调用这个 函数 需要 多长时间,只会考虑 调用这个 函数,内部的代码 执行了 多少次而已。
2.4.2 最坏情况
从心理学角度讲,每个人对发生的事情都会有一个预期,比如看到半杯水,有人会说:哇哦,还有半杯水哦!但也有人会说:天哪,只有半杯水了。一般人处于一种对未来失败的担忧,而在预期的时候趋向做最坏的打算,这样即使最糟糕的结果出现,当事人也有了心理准备,比较容易接受结果。假如最糟糕的结果并没有出现,当事人会很快乐。
- 算法分析也是类似的,假如有一个需求
一个存储了 n 个随机数字的数组,要在其中查找出指定的数字
public int search(int num)
{
int[] arr = {11,10,8,9,7,22,23,0};
for(int i = 0;i < arr.length;++i)
{
if(num == arr[i])
{
return i;
}
}
return -1;
}
上述代码会出现 以下几种情况:
- 最好情况:
查找的第一个数字就是期望的数字,那么算法的时间复杂度 为 O(1)
- 最坏情况:
查找的最后一个数字才是期望的数字,那么算法的时间复杂度为 O(n)
- 平均情况:
任何数字查找的平均成本 是 O(n/2)
最坏情况是一种保障!!!在应用中,这是一种最基本的保障!因为如果在最坏的情况下,还能够正常的提供服务的话,那就说明该算法 是可以通过的!而我们提到的 算法执行时间其实指的都是 最坏情况下的 时间。
2.4.3 空间复杂度分析基本了解
- Java 基本数据类型内存的占用情况
特别强调更正:boolean 占用的 是 一位!并非一个字节(八位)。
- 计算机访问内存的方式都是一次一个字节
- 一个引用(内存地址)需要 8 个字节来表示
例如:Date date = new Date() 则 date 这个变量 就需要 占八个字节 来做 表示。
-
创建一个对象,比如 new Date(),除了 Date 对象内部存储的数据(例如 年月日 等信息)占用的内存,该对象本身也有内存的开销,每个对象的自身开销是 16 个字节,用来 保存 对象的 头信息。
-
因为类中 会有 保存头信息的 内存开辟,所以 不够 8 个字节 开辟的成员,需要 根据 内存对齐,来补到 8 个字节。
public class A{
public int a = 1;
}
通过 new A() 创建一个 对象的内存占用 如下:
1.整型成员变量 a 占用 4 个字节
2.对象本身占用 16 个字节
所以创建上述该对象 总共 需要 24 个字节,而不是 20 个字节!!!
- Java 中的 数组被限定为 对象,即 引用类型的 变量来进行存储。它们 一般都会 记录自身的长度,所以 就会 出现 额外的内存占用,一个 基本数据类型的数组,一般需要 24 个字节的头信息(16 个 字节 保存 对象自身的开销,4 个 字节 用于 保存 长度 以及 4 个字节 补充!)
也就是说 一个 基本数据类型的数组,在不存储任何数据的时候,就有 24 个字节的开销了。
2.4.4 头信息 是什么 ?
头信息包括:
-
对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode(即该对象所在的内存地址哈希值)
-
Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例。(即指向的是 自身的类模板)
- 32 位时
Mark Word 4 位
Klass Word 4 位
- 64 位时
Mark Word 8 位
Klass Word 4 位(指针压缩)
Mark Word 8 位
Klass Word 8位
2.4.5 算法的空间复杂度
了解了 Java 的内存 最基本的机制,就能够 有效帮助我们 估计 大量程序的 内存使用情况。
算法的空间复杂度计算公式 记作: S(n) = O(f(n)) ,其中 n 为输入规模, f(n) 为语句 关于 n 所占 存储空间的函数。
- 案例:
对指定的 数组元素 进行反转,并返回反转的 内容
- 解法①
public static int[] reverse1(int[] arr)
{
int n = arr.length;//申请 4个字节
int temp;//申请 4个 字节
for(int start=0,end = n-1;start <= end;start++,end--)
{
temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
return arr;
}
f(n) = 8
O(f(n)) = 1 (空间复杂度 为 1,常数复杂度!!)
- 解法②
public static int[] reverse2(int[] arr)
{
int n = arr.length;//4
int[] temp = new int[n];//4*n + 24
for(int i = n - 1;i >= 0;++i)
{
temp[n-1-i] = arr[i];
}
return temp;
}
f(n) = 4n + 24 + 8
S(n) = O(f(n)) = n
根据大O推导法则,算法①的空间复杂度 为O(1),算法②的空格键复杂度为O(n),所以从空间占用的角度来看,算法①要比算法②要好很多。
由于 Java 中有 内存垃圾回收机制(GC),并且 JVM 对程序的内存占用 也有 优化(例如 即时编译
),我们 无法精确的 评估 一个 Java 程序的 内存占用情况,但是 了解了 Java 的基本内存占用,使我们可以对 Java 程序的内存占用情况 进行估算。
由于现在的计算机设备内存一般都比较大,基本上个人计算机都是 8G 起步,大 的 可以 达到 64G,所以 内存占用 现在已经不是我们 算法的 瓶颈了,普通的情况下,我们愿意 直接 说 算法的时间复杂度 来衡量 一个 算法的 优越性!
但是,如果你做的 程序 是 嵌入式开发,尤其是一些传感器设备上的内置程序,由于这些设备的内存很小,一般为 几 KB,这个时候 对 算法的 空间复杂度 就 有 很大的要求了!!
但是 一般做 Java 开发的,都是 基于服务器的,而 你会发现 现在 所有的东西 都离不开 服务器这方面!!(因为 数据的传输呀 ~ ~)