大O复杂度表示法

目录

复杂度和大O表示法

时间复杂度

1、只关注代码中执行次数最多的部分

2、加法法则:整体时间复杂度等于量级最大的一段的复杂度

3、乘法法则:嵌套循环的复杂度等于嵌套内外复杂度的乘积

空间复杂度

复杂度分析方法(最好、最坏、平均、均摊 时间复杂度)


复杂度和大O表示法

    数据结构和算法本身解决的问题就是,怎么让程序在执行尽量的情况下,尽量的节占用的空间,也就是研究程序执行的时间【快】复杂度、空间【省】复杂度。很多时候当我们写程序的时候,在特定的场景下已知数据规模的范围,但是更多的情况下编写程序的时候并不知道调用该方法的数据规模。比如:java中的排序操作,我们可以调用Collections.sort方法进行排序,但是真的不同的数据规模不同的排序算法,不同的排序方式执行效率完全不同;比如在数据量比较大 并且一次插入多次查询的时候,使用数组存储的ArrayList和使用链表存储的LinkedList,执行效率可能是成千上万倍的。

    复杂度研究的是数据结构和算法在不同数据规模下的影响,很多时候可能会省略部分的不会随着规模增加而自增的系数、常数、低阶等,复杂度是整个算法的精髓。那么,大O复杂度分析和表示法就出现了,代码中可以用的到的复杂度有:

         

    而常用的大O复杂度随递增关系有:

    O(1)O(logN)O(N)O(N*logN)O(N²) ,随着数据规模N的增大,执行效率(时间复杂度)的递增趋势图如下:

        

时间复杂度

    随着工作年限的增加,我们一定关注自己写的程序的时间空间复杂度,怎么让自己写的程序执行的更快。比如:有一个List存储的是用户信息的数据(集合长度为M),另一个List存储的是用户部门的数据(集合长度为N),我们需要遍历用户集合中的数据,从部门集合中获取部门名称进行填充。

1)、此时,我们完全可以用两次for嵌套进行处理,那么时间复杂度是O(M*N)

2)、我们也可以将部分数据转换成Map的数据结构【读写的时间复杂度都是接近 O(1) 】,再for遍历一次用户信息集合,那么时间复杂度就是 O(M),只是增加一点的空间,也就是空间换时间。而现在的机器一般JVM都是在2-16G等,这点空间是ok的

上面两种的伪代码分别为:

List<User> userList = new ArrayList( M );
List<Department> deptList = new ArrayList( N );
// 方法1: 时间复杂度为 O(M*N)
for (User user : userList) {
    for (Department department : deptList) {
        if (department.id == user.deptId) {
            user.setDeptName(department.name);
        }
    }
}
// 方法2:时间复杂度为 O(M)
Map<Long, String> map = deptList.stream().collect(Collectors.toMap(Department::getId,Department::getName));
for (User user : userList) {
    user.setDeptName(map.get(user.deptId));
}

 

    所以我们不仅要关心各种数据结构和算法的时间、空间复杂度,还要让自己写出复杂度更低的代码。那么,我们怎么看出代码执行的时间复杂度呢?判断原则有:

1、只关注代码中执行次数最多的部分

    大O表示法只关注数据规模与执行时间的趋势,那么与此无关的常数、系数、低阶则可以忽略。比如下面的求和代码,其中随着数组长度的增加,变量sum和i执行时间不随n的增大而变化则可以忽略, 只是关注for循环与n的关系, 所以时间复杂度为for循环的 O(N)。

public static int sum(int[] arr) {
    int n = arr.length;
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

2、加法法则:整体时间复杂度等于量级最大的一段的复杂度

    如上面,只关心执行次数最多的部分,即使是有一个方法内部有一个单层for和一个双层嵌套for,时间复杂度也是只关注双层嵌套,那么时间复杂度是O(N²)。 但是如果复杂度最高的不止一个呢,比如下面的代码:

public static void test(int[] arr1, int[] arr2) {
    for (int i = 0; i < arr1.length; i++) {
        System.out.println(arr1[i]);
    }
    for (int i = 0; i < arr2.length; i++) {
        System.out.println(arr2[i]);
    }
}

    我们不知道两个数组的规模,并且他们的时间复杂度分开看都是一样的,所以使用加法法则(假设数组1的长度为M,数组2的长度为N):时间复杂度为 O(M+N)

 

3、乘法法则:嵌套循环的复杂度等于嵌套内外复杂度的乘积

    乘法法则比较直观,比如我们经常使用的冒泡、插入、选择排序都是直接两层for循环,其时间复杂度就是O(N²)。那么,三层嵌套循环时间复杂度就是 O(N^3),N层嵌套寻循的时间复杂度就是:O(Nⁿ)

 

空间复杂度

   代码执行时占用空间(内存和磁盘)随数据规模的变化趋势,叫空间复杂度,也叫渐进式空间复杂度。对于时间复杂度而言,项目上用到的空间复杂度就简单多了,一般用到的复杂度有:O(1) 也叫原地、O(N)O(N²) ,并且空间复杂度非常好判断。不然Java代码中使用归并排序算法时空间复杂度为O(N),我们在写代码时就会显示创建(申请一片连续的内存空间)一个与排序数组大小相等的新数组。

 

复杂度分析方法(最好、最坏、平均、均摊 时间复杂度)

    先给出插入排序的代码实现,其时间复杂度算多少呢?

    如果是数组[3,2,1]需要按照正序排序,那么发现是完全逆序的时间复杂度是O(N²),如果是[1,2,3] 那么需要进行一次遍历才知道已经拍好了顺序,那么时间复杂度为 O(N)。所以具体的执行时间与数组本身的顺序程序有关,称为为顺序度,与顺序度相反的是逆序度。像上面的情况我们就可以说插入排序的最好时间复杂度为O(N),最坏时间复杂度为O(N²)。具体调用方法时传入的数组的顺序度是不定的,根据概率论的知识考虑进去后,我们就需要关注平均时间复杂度。当然概率的推断过程也不是当下研究的重点,只是我们很多时候需要关注平均时间复杂度的值。

    理解了平均时间复杂度是基于概率论,其情况可能是其中的一种。但是如果有一个程序肯定是要将时间复杂度从最坏到最好的都是执行一遍,那么我们怎么理解该时间复杂度呢?那么我们可以将最坏时间复杂度的与最好时间复杂度的做平摊,次坏时间复杂度的与次好时间复杂度的做平摊,一直到中间时间复杂度。与平均时间复杂度的非常像,只是平均时间复杂度是只可能的概率中的某一种,而这种情况是概率都发生一遍。我们将刚才平摊方法论成为 均摊时间复杂度或者叫摊还时间复杂度。

ArrayList、HashMap等需要扩容(其他容器可能需要缩容也一样)的情况,都可以使用均摊时间复杂度进行分析:

    ArrayList底层由数组实现,数组本身创建时就确定了大小,当需要存储更多的数据时则需要进行扩容,那么需要将新创建一个更大的数组,将数组依次拷贝至新的数组中(此次新增操作的时间复杂度就是O(N))。那么ArrayList的新增方法的时间复杂度是多少呢?此时我们就可以用均摊时间复杂度的方法进行分析,比如将数组扩展为原来的两倍,那么扩容完成后的N次操作时间复杂度都是O(1),那么将扩容那一次的时间复杂度均摊到后续操作上,那么我们认为ArrayList的新增操作的时间复杂度为O(1)。一般情况下,均摊的时间复杂度都等于(或者说接近)最好时间复杂度

    对应这种需要进行扩容的数据结构或者容器,我们都可以使用均摊的时间复杂度进行分析。扩容都会发生在添加方法,但是某些对存储(一般是内存)敏感的容器可能会涉及到缩容,一定是发生在有元素移除的情况,都可以用均摊时间复杂度进行分析。我们知道散列表(hash表)很重要的一个特点就是基于数组下标随机访问时间复杂度为O(1)的特点,Java的HashMap本身也是一样,扩容过程一致,也可以使用均摊时间复杂度进行分析。

    顺便扩展一下,Redis不论使用哪种数据结构,查找过程都会使用全局Hash表,并且Redis本身是基于单线程操作,存储的数据量非常大。肯定是不能接受阻塞式扩容的情况,这样新增操作的 top N 值肯定非常高,是不能接受的,其思想就是将需要扩容移动的数据均摊到后续的每次新增操作中移动(每次挪动一定数据,那么时间就分摊了)。

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值