赞
踩
我们需要先熟悉以下大学中我们学习过的函数图像长成啥样:
大家看到这张图有没有泛起一点点对数学的厌恶,但是别慌我们主要研究的是这些函数的趋势,换种说法来说就是我们用n表示程序核心代码执行的次数,当n趋近于无穷大的时候,纵轴的走势是什么样的就可以了。从上图以及一些基础的数学推演我们可以得到这样一些结论,当n趋近于无穷大的时候:
我们熟悉了上方的一些结论之后,再来学习一下复杂度是怎么表示的,这个复杂度即表示不论是时间复杂度还是空间复杂度,都可以直接使用 O(f(n)) 进行表示,而 f(n) 就是我们程序需要运行的次数的表达式,我们再根据上方总结出来的3点规律对表达式进行微调,就可以先得到我们的时间复杂度表达式了。
我们就可以进行一些实操,看看如何从一份代码看出其时间复杂度。首先来看一个最简单的for循环
public static void doSth(int n) {
for (int i = 0; i < n; i++) {
System.out.println(n); // 这一段代码会运行n次
}
}
上方程序中核心代码需要执行的次数f(n) = n
因此上方程序的时间复杂度就是 O(f(n)) = O(n)
。
怎么样,是不是还蛮轻松的,接下来慢慢升级咯。
public static void doSth(int n) {
System.out.println(n); // 这一段代码会运行1次
for (int i = 0; i < n; i++) {
System.out.println(n); // 这一段代码会运行n次
}
System.out.println(n); // 这一段代码会运行1次
}
上方程序中核心代码需要执行的次数f(n) = n+2
因此上方程序的时间复杂度就是 O(f(n)) = O(n+2) = O(n)
。
为啥呢, 因为上方结论第一点:表达式中的常数项可以进行忽略。明白了吗,就是这个套路没错,让我们继续瞎搞。
public static void doSth(int n) {
System.out.println(n); // 这一段代码会运行1次
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println(n); // 这一段代码会运行n*n次
}
}
System.out.println(n); // 这一段代码会运行1次
}
上方程序中核心代码需要执行的次数f(n) = n²+2
因此上方程序的时间复杂度就是 O(f(n)) = O(n²+2) = O(n²)
。
同样还是和前一个例子一样的,删去常数项,再来!虽然我承认没有哪个傻子会像下边这样子写代码,但是为了能说明问题,好吧 我就是那个傻子:
public static void doSth(int n) {
System.out.println(n); // 这一段代码会运行1次
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println(n); // 这一段代码会运行n*n次
}
for (int j = 0; j < n; j++) {
System.out.println(n); // 这一段代码会运行n*n次
}
}
for (int i = 0; i < n; i++) {
System.out.println(n); // 这一段代码会运行n次
}
System.out.println(n); // 这一段代码会运行1次
}
上方程序中核心代码需要执行的次数f(n) = 2n²+n+2
因此上方程序的时间复杂度就是 O(f(n)) = O(2n²+n+2) = O(n²)
。
为啥呢,看看上方的结论1+2+3就明白啦!虽然这个代码有点呆,但是理儿就是这个理儿,最后再来个凑数的。
public static void doSth(int n) {
System.out.println(n); // 这一段代码会运行1次
System.out.println(n); // 这一段代码会运行1次
System.out.println(n); // 这一段代码会运行1次
System.out.println(n); // 这一段代码会运行1次
System.out.println(n); // 这一段代码会运行1次
System.out.println(n); // 这一段代码会运行1次
}
上方程序中核心代码需要执行的次数f(n) = 6
因此上方程序的时间复杂度就是 O(f(n)) = O(6) = O(1)
。
还是祭出这张图,直接看图丢结论
[2, +∞)
的常整数有时候我们的程序执行的次数不固定,比如像下边这个程序:
public static void doSth(int n) { // 确定 0 <= n <= 9
int[] arr = {1,1,2,3,4,5,6,7,8,9};
for (int i = 0; i < n; i++) {
if (arr[n] == 3) return;
System.out.println(n); // 核心代码执行次数不定
}
}
当n = 2 程序需要执行 n 次,此时时间复杂度为 O(n)
当n = 5 程序需要执行 3 次,此时时间复杂度为 O(1)
但是我们常说一个程序时间复杂度取的一般都是最糟糕的结果,本例中也就是 O(n)
。
但是在了解空间复杂度之前,我们同样的需要有一些前置知识需要进行准备。
数据类型——内存占用 |
---|
int——4字节——32bit |
short——2字节——16bit |
long——8字节——64bit |
double——8字节——64bit |
float——4字节——32bit |
char——2字节——16bit |
boolean——1字节——8bit |
byte——1字节——8bit |
Java中创建引用数据类型的引用需要8个字节来表示引用的地址。例如 Date dd = new Date();
那么dd这个引用就需要申请8字节的内存空间。
同时引用数据类型在创建对象的时候又需要16字节来表示保存创建的对象的头信息。例如new Date()
这个操作。并且对于一般内存的使用,不满8字节的会自动被填充成8字节(内存对齐原理)。
举例:
class Person { // 16字节存储基础信息
char familyName; // 2字节
int age; // 4字节
}
// p1需要8字节 + 16字节保存对象头部 + (2+4)内部成员占用,由于不到8的倍数自动填充2个字节
Person p1 = new Person(); // 因此这一行语句总共需要申请32字节的内存空间
for (int i = 0; i < n; i++) {
Person person = new Person(); // 需要n字节
}
因此上方的代码总计需要申请f(n) = 32n
的内存空间,对于空间复杂度,上方的三个结论依旧成立,即:
因此上方代码的空间复杂度就是O(n)
Java中的数组被限定为对象,一个最原始长度为0的数组一般需要24字节的头信息(包括16字节的对象头部开销 + 4字节的表示数组长度 + 4个填充字节) + 保存的值的长度
空间复杂度对于现代的设备来说,其实并没有太大的压力,市场上最小内存的智能设备都有2G的内存空间,因此并不用太过于纠结几个字节的空间大小,甚至有时候我们会选择采用空间换时间的概念去进行程序设计。
当然如果你面对的是嵌入式的编程,通常一些传感器上的内存非常有限,空间复杂度的控制就显得尤为重要了。
所谓的排序稳定性指的是将一个数组进行排序,原数组中存在两个值相同的元素,如果排序之后这两个元素的前后位置顺序不发生变化,则称该排序算法是稳定的;如果位置顺序变化了,则称该排序算法不稳定。
上文的排序稳定性咋一听好像没啥用,两个元素既然都相等,那么前后顺序有什么影响呢?谁前谁后不是都一样的吗?
其实不然,当我们只对数组进行一次排序的话,那么所谓稳定性确实是无稽之谈,但是如果是多次排序呢?
假如对于一些商品,我们希望先按照价格降序,再按照销量升序,这时候稳定性的问题就被提上日程了,如果是不稳定的算法,可能按照销量进行第二次排序之后,前一次按照价格拍好的顺序中,销量相同的记录就会被打乱掉。
因此,我们需要根据使用的场景去选择排序的算法,一般来说选择高效的算法,对于稳定性有要求的话就需要选择稳定性好的算法。
函数图形在线绘制工具:https://zh.numberempire.com/graphingcalculator.php?functions=&xmin=-781.857417&xmax=3004.675376&ymin=-512.031295&ymax=2012.323091&var=n
资料学习参考:https://www.bilibili.com/video/BV1iJ411E7xW
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。