• 数据结构(二) — 算法


     一、数据结构与算法的关系

    上一次我大致说了数据结构的一些基本概念,应该还蛮通俗易懂的吧(大概吧!!!)。数据结构与算法这两个概念其实是可以单独拿出来讲的,毕竟我们大学有数据结构课,有算法课,单独拿出来讲好像没什么问题,但是数据结构就那么一些(数组、队列、树、图等结构),单独拿出来很快就说完了,但是说完之后,一脸懵逼,两脸茫然,感觉数据结构没什么用啊,但是,注意了啊,但是引入算法,变成程序之后你就会发觉某些特别困难的问题,原来可以用程序这么简单的解决。

    所以在我们用程序解决问题看来,程序 = 数据结构 + 算法,数据结构和算法两个概念间的逻辑关系贯穿了整个程序世界,二者不可分割。数据结构是底层,算法高层。数据结构为算法提供服务。算法围绕数据结构操作。

    但是我们这个毕竟是数据结构系列嘛,所以算法涉及就比较少,好了,我们接下来进入正题。

    二、算法的定义
    算法 (Algorithm) 这个单词最早出现在波斯数学家阿勒·花刺子密在公元 825 年(相当于我们中国的唐朝时期)所写的 《印度数字算术》中。 如今普遍认可的对算法的定义是:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。

    三、算法的特征
    算法具有五个基本特性: 输入、输出、 有穷性、确定性和可行性。
    1、输入:算法可以有零个或者多个输入。
    2、输出:算法至少有一个或者多个输出,算法必须要输出的,输入你可以没有,输出都没有,那算法就没有意义了。
    3、有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每个步骤在可接受的时间内完成。可接受的时间就是比如你计算100内的加减法,然后需要个1年时间,这个就肯定无法接受了,虽然有穷了,但是算法的意义就没有了。
    4、确定性:算法的每一步骤都具有确定的含义, 不会出现二义性。
    5、可行性:算法的每一步都必须是可行的, 也就是说,每一步都能够通过执行有限次数完成。

    四、算法设计的要求
    好的算法的设计应该要满足以下几个要求:
    1、正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、 能正确反映问题的需求、能够得到问题的正确答案。
    2、可读性:算法设计的另一目的是为了便于阅读、 理解和交流
    3、健壮性:当输入数据不合法时,算法也能做出相关处理, 而不是产生异常或莫名其妙的结果
    4、时间效率高:对于同一个问题,如果有多个算法能够解决, 执行时间短的算法效率高,执行时间长的效率低,好的算法时间效率高。
    5、存储量底:存储量指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间,好的算法占用的存储空间尽量低

    五、算法效率的度量方法
    对算法效率的度量一般有两种方法,事后统计法与事前分析估算法
    1、事后统计法;这种方法主要是通过设计好的测试程序和数据,利用计算机计时 器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
    事后统计法有很大的缺陷:先设计算法,然后实施,最后测量,软硬件不同,或许结果不同,同时测试结果测算也不好统计,最重要的是统计出来结果不理想,又花费了那么的多的人力物力,吃力不讨好,所以我们一般不会选这种方法。

    2、事前分析估算法:在计算机程序编制前,依据统计方法对算法进行估算。
    事前分析估算法较好,那我们怎么估算呢,一般依靠下面两点:时间复杂度与空间复杂度


    六、算法的时间复杂度
    先说时间复杂度的定义:在进行算法分析时, 语句总的执行次数 T ( n )是关于问题规模 n 的函数,进而分析 T ( n )随 n 的变化情况并确定T(n)的数量级。 算法的时间复杂度,也就是算法的时间量度,记作: T ( n ) = O(f(n))。 它表示随问题规模 n 的增大,算法执行时间的增长率和 f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。 其中 f ( n) 是问题规模 n 的某个函数。这样用大写 O()来体现算法时间复杂度的记法,我们称之为大 O 记法。

    是不是没看懂,没关系,其实很简单的,九个字概括:估算算法运行的次数。也就是说我们将一个算法程序看作一个整体,然后估算程序运行过程中执行的次数,然后以数量级(比如常数阶1;线性阶n;平方阶n^2等)来表示,并用O()这种形式来记录。还不懂,那么接下来看例子就应该懂了:

    还说一点,那么大O阶的表示规则是怎么样的呢(这个是重点):

    1、用常数 1 取代运行时间中的所有加法常数。
    2、在修改后的运行次数函数中,只保留最高阶项。
    3、如果最高阶项存在且不是 1 ,则去除与这个项相乘的常数。
    得到的结果就是大O阶。

    1、常数阶

    首先顺序结构的时间复杂度。看个程序:

    int sum = 0; /*执行一次*/
    int n = 100; /*执行一次*/
    sum = (1+n)*n/2;/*执行一次*/
    system.out.print("sum:"+sum);/*执行一次*/

    这个算法的运行次数函数是 f(n)= 4,根据我们大O阶的方法,第一步就是把常数项4改为1。在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)。
    另外,我们试想一下,如果这个算法当中的语句 sum= ( 1+n)*n/2 有 10 句,
    即:

    int sum = 0; /*执行一次*/
    int n = 100; /*执行一次*/
    sum = (1+n)*n/2;/*执行第一次*/
    sum = (1+n)*n/2;/*执行第二次*/
    sum = (1+n)*n/2;/*执行第三次*/
    sum = (1+n)*n/2;/*执行第四次*/
    sum = (1+n)*n/2;/*执行第五次*/
    sum = (1+n)*n/2;/*执行第六次*/
    sum = (1+n)*n/2;/*执行第七次*/
    sum = (1+n)*n/2;/*执行第八次*/
    sum = (1+n)*n/2;/*执行第九次*/
    sum = (1+n)*n/2;/*执行第十次*/
    system.out.print("sum:"+sum);/*执行一次*/

    事实上无论 n 为多少,上面的两段代码就是 4次和 13 次执行的差异。这种与问题的大小无关 (n 的多少) ,执行时间恒定的算法,我们称之为具有 O(1)的时间复杂度,又叫常数阶

    注意: 不管这个常数是多少,我们都记作O(1),而不能是O(4)、O(13)等其他任何数字,这是我们这种初学者常常犯的错误。

    对于分支结构(可理解为非循环结构)而言,无论是真,还是假,执行的次数都是恒定的,不会随着 n 的变大而发生变化,所以单纯的分支结构(不包含在循环结构中) ,其时间复杂度都是O(1)。

    2、线性阶:
    线性阶的循环结构会复杂很多。要确定某个算法的阶次,我们常常需要确定某个特定语句或某个语句集运行的次数。因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。
    下面这段代码,它的循环的时间复杂度为 O(n) , 因为循环体中的代码须要执行 n次。

    int i;
    for( i = 0;i < n;i++){/*总执行次数是n*/
      /*时间复杂度O(1)的程序步骤序列 */ 
    }

    3、对数阶:
    下面的这段代码,时间复杂度又是多少呢?

    int count = 1;
    while(count < n){
    count = count*2;
      /*时间复杂度O(1)的程序步骤序列 */ 
    }

    由于每次 count 乘以 2 之后,就距离 n 更近了一分。 也就是说,有多少个2 相乘后大于 n ,则会退出循环。 由 2^x=n 得到 x=log2^n。 所以这个循环的时间复杂度为 O(log^n)。

    4、平方阶:
    下面例子是一个循环嵌套,它的内循环刚才我们已经分析过,时间复杂度为 O(n)。

    int i, j;
    for(i = 0; i < n; i++){/*总执行次数是n*/
    for(j = 0; j < n; j++){/*总执行次数是n*/
      /*时间复杂度O(1)的程序步骤序列 */ 
      }
    }

    而对于外层的循环,不过是内部这个时间复杂度为 O(n)的语句,再循环 n 次。 所以这段代码的时间复杂度为 O(n^2).

    如果外循环的循环次数改为了m时间复杂度就变为 O(m*n)。

    int i, j;
    for(i = 0; i < m; i++){/*总执行次数是m*/
    for(j = 0; j < n; j++){/*总执行次数是m*/
      /*时间复杂度O(1)的程序步骤序列 */ 
        }
    }

    所以我们可以总结得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。
    那么下面这个循环嵌套,它的时间复杂度是多少呢?

    int i, j;
    for(i = 0; i < m; i++){/*总执行次数是m*/
    for(j = i; j < n; j++){/*注意这里是i,不是0*/
        /*时间复杂度O(1)的程序步骤序列 */ 
        }
    }

    由于当 i= 0 时,内循环执行了 n 次,当 i = 1 时,执行了 n-1 次,……当 i=n —1 时,执行了 1 次。所以总的执行次数为:

    用我们推导大 0 阶的方法,第一条:没有加法常数不予考虑; 第二条:只保留最高阶项,因此保留时n^2/2;第三条:去除这个项相乘的常数,也就是去除 1/2 ,最终这段代码的时间复杂度为O(n^2)。

    从这个例子,我们也可以得到一个经验,其实理解大O阶推导不算难,难的是对数列的一些相关运算,这更多的是考察你的数学知识和能力。

    5、常见的时间复杂度:

    执行次数函数 非正式术语
    13 O(1) 常数阶
    2n+3 O(n) 线性阶
    3n^2+2n+1 O(n^2)  平方阶
    2log2^n+3 O(log^n) 对数阶
    2n+3nlog2^n+4 O(nlog^n) nlong^n阶
    4n^3+2n+1 O(n^3) 立方阶
    2^n O(2^n) 指数阶



    常用的时间复杂度所耗费的时间从小到大依次是:

     七、算法的空间复杂度

    算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作: S(n)= O(f(n)),其中,O 为问题的规模, f(n)为语句关于 n 所占存储空间的函数。
    一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、 变量和输入数据外,还需要存储对数据操作的存储单元,若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为 0(1)。
    通常, 我们都使用"时间复杂度"来指运行时间的需求,使用"空间复杂度"指空间需求。当不用限定词地使用"复杂度'时,通常都是指时间复杂度。
    一般来说我们最关心的是时间复杂度


    八、总结回顾
    算法的定义:算法是解决特定问题求解步骤的描述,在计算机中为指令的有限序列,并且每条指令表示一个或多个操作。
    算法的特性: 有穷性、确定性、可行性、输入、输出。
    算法的设计的要求: 正确性、可读性、健壮性、 高效率和低存储量需求。

    算法特性与算法设计容易混,需要对比记忆。
    算法的度量方法: 事后统计方法(不科学、不准确)、 事前分析估算方法。
    推导大 O 阶:
    1、用常数 1 取代运行时间中的所有加法常数。
    2、在修改后的运行次数函数中,只保留最高阶项。
    3、如果最高阶项存在且不是 1 ,则去除与这个项相乘的常数。**
    得到的结果就是大 O阶。

    通过这个步骤,我们可以在得到算法的运行次数表达式后,很快得到算法的时间复杂度,即大O阶。同时我也提醒了大家,其实推导大 O 阶很容易,但如何得到运行次数的表达式却是需要数学功底的。
    接着我们给出了常见的时间复杂度所耗时间的大小排列:


    最后,我们给出了算法空间复杂度的概念。

     

  • 相关阅读:
    html 知识
    mysql use mysql hang
    微信机器人 简化版
    Tk::Table
    好友消息和群消息区别
    完整的微信登陆 接收消息流程
    Python OOP知识积累
    Python OOP知识积累
    JSTL EL 详解
    JSP中的EL表达式详细介绍
  • 原文地址:https://www.cnblogs.com/ZWOLF/p/10551888.html
Copyright © 2020-2023  润新知