zoukankan      html  css  js  c++  java
  • 数据结构(二) — 算法

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

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

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

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

    二、算法的定义
    算法 (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 阶很容易,但如何得到运行次数的表达式却是需要数学功底的。
    接着我们给出了常见的时间复杂度所耗时间的大小排列:


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

     

  • 相关阅读:
    react ts axios 配置跨域
    npm run eject“Remove untracked files, stash or commit any changes, and try again.”错误
    java 进程的参数和list的线程安全
    帆软报表 大屏列表跑马灯效果JS
    帆软报表 快速复用数据集,避免重复劳动
    分析云 OA中部门分级思路和实现方法
    分析云 分段器 只显示一个块的数据
    分析云 更改服务默认的端口号
    分析云U8项目配置方法新版本(2)
    Oracle 创建时间维度表并更新是否工作日字段
  • 原文地址:https://www.cnblogs.com/ZWOLF/p/10551888.html
Copyright © 2011-2022 走看看