• 数据结构和算法(二)--算法分析


    目录

    一、算法分析

    1、算法的时间复杂度分析

    事后分析估算方法:

    事前分析估算方法:

    1.1、函数渐近增长

    1.2、大O记法

    1.3、常见的大O阶

    1.4、函数调用的时间复杂度

    1.5、最坏情况

    2、算法的空间复杂度分析

    2.1、Java中常见内存占用

    2.2、算法的空间复杂度


    一、算法分析

            研究算法的最终目的就是如何花更少的时间,如何占用更少的内存去完成相同的需求。

            有关算法时间耗费分析,我们称之为算法的时间复杂度分析,有关算法的空间耗费分析,我们称之为算法的空间复杂度分析

    1、算法的时间复杂度分析

    事后分析估算方法:

    1. public static void main(String[] args) {
    2. long start = System.currentTimeMillis();
    3. int sum = 0;
    4. int n = 100;
    5. for (int i = 1; i <= n; i++) {
    6. sum += i;
    7. }
    8. System.out.println("sum=" + sum);
    9. long end = System.currentTimeMillis();
    10. System.out.println(end - start);
    11. }

    事前分析估算方法:

            在计算机程序编写前,依据统计方法对算法进行估算,经过总结,程序在计算机上运行所消耗的时间取决于下列因素:

            1、算法采用的策略和方案;

            2、编译产生的代码质量;

            3、问题的输入规模(所谓的问题输入规模就是输入量的多少);

            4、机器执行指令的速度;

            由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。如果算法固定,那么该算法的执行时间就只和问题的输入规模有关系了。

    需求:计算1到100的和。

    第一种解法:

    1. //如果输入量n为1次,则需要计算1
    2. //如果输入量n为1亿次,则需要计算1亿次
    3. public static void main(String[] args) {
    4. int sum = 0;//执行1
    5. int n = 100;//执行1
    6. for (int i = 1; i <= n; i++) {//执行n+1
    7. sum += i;//执行n次
    8. }
    9. System.out.println("sum=" + sum);
    10. }

    第二种解法:

    1. //如果输入量n为1次,则需要计算1
    2. //如果输入量n为1亿次,则需要计算1
    3. public static void main(String[] args) {
    4. int sum = 0;//执行1
    5. int n = 100;//执行1
    6. sum = (n + 1) * n / 2;//执行1
    7. System.out.println("sum=" + sum);
    8. }

    因此,当输入规模为n时,第一种算法执行了1+1+(n+1)+n=2n+3次;第二种算法执行1+1+1=3次。如果我们把第一种算法的循环体看做是一个整体,忽略结束条件的判断,那么其实这两个算法运行时间的差距就是n和1的差距。

    需求:计算100个1+100个2+100个3+....100个100的结果

    1. public static void main(String[] args) {
    2. int sum = 0;
    3. int n = 100;
    4. for (int i = 1; i <= n; i++) {
    5. for (int j = 1; j <= n; j++) {
    6. sum += i;
    7. }
    8. }
    9. System.out.println("sum=" + sum);
    10. }

    最重要的就是把核心操作的次数和输入规模关联起来。

    1.1、函数渐近增长

    概念:给定两个函数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=15243
    n=27476
    n=396109
    n=1023203130
    n=100203200301300

    结论:

    当输入规模n>2时,算法A1的渐近增长小于算法B1的渐近增长。

    随着输入规模的增大,算法的常数操作可以忽略不计。

    测试二:

    规模算法C1(4n+8)执行次数算法C2(n)执行次数算法D1(2n^2+1)执行次数算法D2(n^2)....
    n=112131
    n=216294
    n=3203199
    n=104810201100
    n=1004081002000110000
    n=10004008100020000011000000

    结论:

    随着输入规模的增大,与最高次项相乘的常数可以忽略。

    测试三:

    规模算法E1(2n^2+3n+1)执行次数算法E2(n^2)执行次数算法F1(2n^3+3n+1)..算法F2(n^3)..
    n=16161
    n=2154238
    n=32896427
    n=1023110020311000
    n=100203011000020003011000000

    结论:

    最高次项指数大的,随着n的增长,结果也会变得增长特别快。

    测试四:

    规模算法G(n^3)执行次数算法H(n^2)执行次数算法I(n)..算法J(logn)..算法K(1)..
    n=284211
    n=46416421

    结论:

    算法函数中n最高次幂越小,算法效率越高。

    1、算法函数中的常数可以忽略;

    2、算法函数中最高次幂的常数因子可以忽略;

    3、算法函数中最高次幂越小,算法效率越高;

    1.2、大O记法

    定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规律n的函数,进而分析T(n)随着n的变化情况并确定T(n)的量级。算法的时间复杂度,就是算法的时间量度,记作:T(n)=O(f(n))。它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模n的某个函数。

    在这里,我们需要明确一个事情:执行次数=执行时间

    用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法。

    算法一:

    1. public static void main(String[] args) {
    2. int sum = 0;//执行1
    3. int n = 100;//执行1
    4. sum = (n + 1) * n / 2;//执行1
    5. System.out.println("sum=" + sum);
    6. }

    算法二:

    1. public static void main(String[] args) {
    2. int sum = 0;//执行1
    3. int n = 100;//执行1
    4. for (int i = 1; i <= n; i++) {
    5. sum += i;//执行n次
    6. }
    7. System.out.println("sum=" + sum);
    8. }

    算法三:

    1. public static void main(String[] args) {
    2. int sum = 0;//执行1
    3. int n = 100;//执行1
    4. for (int i = 1; i <= n; i++) {
    5. for (int j = 1; j <= n; j++) {
    6. sum += i;//执行n^2
    7. }
    8. }
    9. System.out.println("sum=" + sum);
    10. }

    以上算法执行次数:

    算法一:3次

    算法二:n+2次

    算法三:n^2+2次

    如果用大O记法表示上述每个算法的时间复杂度,应该如何使用呢?基于我们对函数渐近增长分析,推导大O阶的表示法有以下几个规则可以使用:

    1、用常数1取代运行时间中的所有加法常数;

    2、在修改后的运行次数中,只保留最高阶项;

    3、如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数;

    所以上述算法的大O记法分别为:

    算法一:O(1)--常数阶

    算法二:O(n)----线性阶

    算法三:O(n^2)----平方阶

    1.3、常见的大O阶

    1、线性阶--O(n)

    2、平方阶--O(n^2)2次for循环

    3、立方阶--O(n^3)--3次for循环

    4、对数阶--O(logn)

    1. int i=1,n=100;
    2. while(i<n){
    3. i=i*2;
    4. }

    由于每次i*2之后,就距离n更近一步,假设有x个2相乘后大于n,则会退出循环。由于是2^x=n,得到x=log(2)n,所以这个循环的时间复杂度为O(logn);

    对于对数阶,由于随着输入规模n的增大,不管底数为多少,它们的增长趋势是一样的,所以我们会忽略底数。

    规模规模log(2)nlog(4)nlog(8)n
    831.51
    64632
    51294.53
    40961264
    1677721624128
    1342177282713.59
    2.815E+14482416
    7.923E+28964832
    6.28E+571929664
    3.94E+115384192128
    1.55E+231768384256

    5、常数阶--O(1)

    描述增长的数量级说明举例
    常数级别1普通语句两个数相加
    对数级别logn二分策略二分查找
    线性级别n循环找出最大元素
    线性对数级别nlogn分治思想归并排序
    平方级别n^2双层循环检查所有元素对
    立方级别n^3三层循环检查所有三元组
    指数级别2^n穷举查找检查所有子集

    它们的时间复杂度从低到高依次为:O(1)

    从平方阶开始,随着输入规模的增大,时间成本会急剧增大;所以,我们的算法,尽可能的追求是

    O(1)

    1.4、函数调用的时间复杂度

    案例一:

    1. public static void main(String[] args) {
    2. int n = 100;
    3. for (int i = 0; i < n; i++) {
    4. show(i);
    5. }
    6. }
    7. private static void show(int i) {
    8. System.out.println(i);
    9. }

    时间复杂度:O(n)

    案例二:

    1. public static void main(String[] args) {
    2. int n = 100;
    3. for (int i = 0; i < n; i++) {
    4. show(i);
    5. }
    6. }
    7. private static void show(int i) {
    8. for (int j = 0; j < i; j++) {
    9. System.out.println(i);
    10. }
    11. }

    时间复杂度:O(n^2)

    案例三:

    1. public static void main(String[] args) {
    2. int n = 100;
    3. show(n);
    4. for (int i = 0; i < n; i++) {
    5. show(i);
    6. }
    7. for (int i = 1; i <= n; i++) {
    8. for (int j = 1; j <= n; j++) {
    9. System.out.println(i);
    10. }
    11. }
    12. }
    13. private static void show(int i) {
    14. for (int j = 0; j < i; j++) {
    15. System.out.println(i);
    16. }
    17. }

    时间复杂度:O(n^2)-----n+n^2+n^2=2n^2+n根据大O推导规则,最终时间复杂度:O(n^2)

    1.5、最坏情况

    1. //有一个存储n个随机数字的数组,请从中查找出指定的数字的索引下标
    2. public int search(int num) {
    3. int[] arr = { 11, 10, 8, 9, 7, 22, 33, 23, 0 };
    4. for (int i = 0; i < arr.length; i++) {
    5. if (num == arr[i]) {
    6. return i;
    7. }
    8. }
    9. return -1;
    10. }

    最好情况:O(1)

    最坏情况:O(n)

    平均情况:O(n/2)

    2、算法的空间复杂度分析

    早期计算机软硬件的发展漫长过程中,512K-->1M-->2M-->4M...等.发展到现在8G,16G,32G。早期,算法在运行过程中对内存的占用情况是需要考虑的问题,我们可以用算法的空间复杂度来描述算法对内存的占用。

    2.1、Java中常见内存占用

    1、基本数据类型内存占用情况

    数据类型内存占用字节数
    byte1
    short2
    int4
    long8
    float4
    double8
    boolean1
    char2

    2、计算机访问内存的方式都是一次一个字节

    3、一个引用(机器地址)需要8个字节表示

    4、创建一个对象,比如new Date();除了Date对象内存的数据(年月日等信息占用的内存),该对象本身也有内存开销,每个对象自身开销是16个字节,用来保存对象的头信息。

    5、一般内存的使用,如果不够8个字节,都会被自动填充为8字节

    1. public class A {
    2. public int a=1;
    3. }
    4. 通过new A();创建一个对象的内存占用如下:
    5. 1、整型成员变量a占用4个字节
    6. 2、对象本身占用16个字节
    7. 那么创建该对象总共需要20个字节,但由于不是以8为单位,会自动填充为24个字节。

    6、Java中数组被限定为对象,它们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组,一般需要24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存。

    2.2、算法的空间复杂度

    算法的空间复杂度计算公式记作:S(n)=O(f(n)),其中n为输入规模,f(n)为语句关于n所占存储空间的函数。

    案例:对指定的数组元素进行反转,并返回反转的内容。

    解法一:

    1. public static int[] reverse(int[] arr) {
    2. int n = arr.length;// 申请4个字节
    3. int tmp;// 申请4个字节
    4. for (int start = 0, end = n - 1; start <= end; start++, end--) {
    5. tmp = arr[start];
    6. arr[start] = arr[end];
    7. arr[end] = tmp;
    8. }
    9. return arr;
    10. }

    解法二:

    1. public static int[] reverse(int[] arr) {
    2. int n = arr.length;// 申请4个字节
    3. int[] tmp = new int[n];// 申请n*4个字节+数组自身开销24字节
    4. for (int i = n - 1; i >= 0; i--) {
    5. tmp[n - 1 - i] = arr[i];
    6. }
    7. return tmp;
    8. }

    解法一:O(1)----4+4=8

    解法二:O(n)----4+4n+24=4n+28

    数据结构和算法(一)

    数据结构和算法(三)--排序

    天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。

  • 相关阅读:
    day 10.4
    抖音小店:庞大用户基数与强大商业化能力的未来发展
    Maven的常用命令
    MATLAB绘图合集:fcontour绘制隐函数等高线图
    基于springboot教学管理系统设计与实现
    Docker 安装 MySQL5.7
    Mybatis主配置—Configuration
    Android——ScrollView与ConstraintLayout约束布局失效场景以及方案
    秋招每日一题T30——每个元音包含偶数次的最长子字符串
    FFmpeg 中 Filters 使用文档介绍
  • 原文地址:https://blog.csdn.net/weixin_42472027/article/details/127605888