初识算法之时间复杂度、空间复杂度以及排序稳定性

一、 时间复杂度

  • 概念:所谓时间复杂度,就是运行某个程序所需要消耗的时间,然后我们使用了数学的函数表示方法对消耗的时间量级做了一个分类。

我们需要先熟悉以下大学中我们学习过的函数图像长成啥样:
在这里插入图片描述
大家看到这张图有没有泛起一点点对数学的厌恶,但是别慌我们主要研究的是这些函数的趋势,换种说法来说就是我们用n表示程序核心代码执行的次数,当n趋近于无穷大的时候,纵轴的走势是什么样的就可以了。从上图以及一些基础的数学推演我们可以得到这样一些结论,当n趋近于无穷大的时候

  • 表达式中的常数项可以进行忽略。举例:f(n) = n + 50000 与 f(n) = n + 1 在n等于五十万亿的时候差别几乎可以忽略不记。
  • 表达式中除了n的最高次幂的项,其余都可以忽略。举例: f(n) = n²+50n 与 f(n) = n² 在n等于五十万亿的时候差别几乎可以忽略不记,不信的话看下边这个图看看仅仅当n=200的时候差别就几乎肉眼不可见了,更何况n趋近无穷大。
    在这里插入图片描述- 表达式中n的最高次幂的项中的系数可以忽略为1。举例:f(n) = 10*n² 与 f(n) = n2在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)

时间复杂度效率

还是祭出这张图,直接看图丢结论
在这里插入图片描述

  • 当n趋近于无穷大的时候,复杂度排序如下:O(1) < O(log(n)) < O(n) < O(n*log(n)) < O(n²) < O(nk) < O(kn) ,这里的k为[2, +∞) 的常整数
  • 一般来说,当一个程序的时间复杂度超过 O(n2),则说明这个程序可能存在一定的问题,我们可以尝试着去优化他。

次数不定取最糟

有时候我们的程序执行的次数不固定,比如像下边这个程序:

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)




二、 空间复杂度

  • 概念:其实有了时间复杂度的概念之后,空间复杂度的推断方法也比较类似。所谓空间复杂度,就是描述程序执行中需要申请的资源大小的一个量级。

但是在了解空间复杂度之前,我们同样的需要有一些前置知识需要进行准备。

Java基础数据类型所占空间

数据类型——内存占用
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引用数据类型所占空间

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的内存空间,对于空间复杂度,上方的三个结论依旧成立,即:

  • 表达式中的常数项可以进行忽略
  • 表达式中除了n的最高次幂的项,其余都可以忽略
  • 表达式中n的最高次幂的项中的系数可以忽略为1

因此上方代码的空间复杂度就是O(n)

Java数组所占空间

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

啪!~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值