数据结构与算法(二)—— 算法基础知识与效率度量

注:本篇内容参考了《Java常用算法手册》和《大话数据结构》。

本人水平有限,文中如有错误或其它不妥之处,欢迎大家指正!

目录

1,算法概念及特征

1.1 概念

1.2 特征

1.3 算法的设计要求

2 算法分类

2.1 按照应用分类

2.2 按照确定性分类

2.3 按算法的思路分类

3 算法相关概念及区别

3.1算法和公式的关系

3.2 算法和程序的关系

3.3 算法和数据结构的关系

4 算法的表示

4.1 自然语言表示

4.2 流程图表示

4.3 N-S图表示

4.4 伪代码表示

5 算法效率的度量

5.1 时间复杂度

5.2 求解时间复杂度

5.3 空间复杂度

6 算法的新进展

6.1 并行算法

6.2 遗传与进化算法

6.3 量子算法


1,算法概念及特征

1.1 概念

算法(Algorithm)是模型分析的一组可行的、确定的和有穷的规则。通俗地说,可以认为是一个完整的解题步骤,之前上学在题目时,解题分为几步,第一步做什么,第二步做什么。所以简单理解就是分步骤解决一个问题。这一看有点不太好理解,下面举个例子白话一下。

小明中午准备做饭,需要进行买菜洗菜切菜烧水和准备其它的配料等工作,假设洗菜做用到热水。小明可以有几种做法:

  1. 做法A:买菜——>洗菜——>切菜——>烧水——>准备配料...;
  2. 做法B:烧水——>买菜——>洗菜——>切菜——>准备配料...;
  3. 做法C:...

当然也可以有其它做法,有多少种做法就可以认为是有多少种算法,不同的做法就相当于是不同的算法。

目前算法的应用很广泛,常用的算法有递推、递归、穷举、贪婪、分治、动态规划和迭代等。

 

1.2 特征

一个算法一般都可以从其中抽象出5个特征:有穷性、确切性、输入、输出和可行性

有穷性

算法的指令或步骤的执行次数是有限的,执行时间也是有限的。指令或步骤、时间都不是无限的执行下去。

确切性

算法的每一个指令或步骤都必须有明确的定义和描述,即每一步做什么都是明确的。

输入

一个算法应该相应的输入条件,用来刻画运算对象的初始情况。比如解数据题也是有已知条件和输入的信息。

输出

一个算法应该有明确的输出。没有得到结果的算法是没有意义的,这里不要简单的理解是函数没有返回,这两个是不同的。

可行性

算法的执行步骤必须是可行的,且可以在有限时间内完成。如果某个步骤无法执行,那是没有意义的。

 

1.3 算法的设计要求

算法在设计时要求正确性、可读性、健壮性、高效率和低存储量

正确性

算法至少应该有输入、输出和加工处理无歧义性,能正确反映问题的需求,能得到问题的正确答案。

可读性

算法要便于阅读,好理解,方便交流。可读性的算法有助于人们对算法的理解,容易发现可能存在的错误和问题,且易于调试和修改。

健壮性

当输入数据不合法时,算法也能做出相应的处理,而不是产生异常或莫名其妙的结果。

时间效率高和存储量低

算法执行所花的时间应该短,且占用的存储资源要少。即设计的算法应该尽量满足时间效率高和存储量低的需求。

 

2 算法分类

按照不同的应用和特性,算法可以分为不同的类别。

2.1 按照应用分类

按照算法的应用领域,也就是解决的问题,可以为基本算法、数据结构相关算法、几何算法、图论算法、规划算法、数值分析算法、加/解密算法、排序算法、查找算法、并行算法和数论算法等。

 

2.2 按照确定性分类

可分为确定性算法和不确定性算法。

  • 确定性算法:这类算法在有限的时间内完成计算,得到的结果是唯一的,且经常取决于输入值;
  • 不确定性算法:这类算法在有限的时间内完成计算,但得到的结果往往不是唯一的,也就是存在多值性。

 

2.3 按算法的思路分类

可分为递推算法、递归算法、穷举算法、贪婪算法、分治算法、动态规划算法和迭代算法等。

 

 

3 算法相关概念及区别

3.1算法和公式的关系

上学时经常会用到一些公式,比如数字物理化学等方面的。公式也是用来解决某类问题的。有特定的输入条件和输出结果,能在有限时间内完成计算,并且公式都是完全可操作计算的。

其实公式确实是提供了一种算法,但算法绝不完成等于公式。公式是一种高度精简的计算方法,可以认为就是一种算法。而算法并不一定就是公式,算法的形式可以比公式理工复杂,解决的问题更加广泛。

 

3.2 算法和程序的关系

如今信息发展程度较高,很多计算已经不再靠传统的绝笔来解决了,而是靠一些计算机程序来解决。

算法和程序设计语言是不同的。程序设计语言是算法实现的一种形式,也就是一种工具。如果我们要用程序来实现一个算法,往往要先熟悉程序设计语言的语法格式,然后才能使用这种程序语言来编写合适的算法实现程序。

 

3.3 算法和数据结构的关系

数据结构是数据的组织形式,可以用来表征特定的数据对象。上一篇数据结构与算法(一)——数据结构基础知识专门说明了数据结构的基础知识,这里不再赘述。在计算机程序设计中,操作的对象是各式各样的数据,这些数据往往拥有不同的数据结构,比如数组、链表等。不同的数据结构所采用的处理方法也不同,计算的复杂程度也不同,因此算法往往是依赖于某种数据结构的。换句话说,就是数据结构是算法实现的基础

上一篇数据结构与算法(一)——数据结构基础知识也提到,计算机科学家尼克劳斯·沃思(Nikiklaus Wirth)提出了“数据结构+算法=程序”的著名公式。

对程序、算法、数据结构、程序设计语言,给出一个公式的话,可以这样表述:

数据结构 + 算法 + 程序设计语言 = 程序数据结构 + 算法 + 程序设计语言 = 程序

数据结构往往表示的是处理的对象,算法是计算和处理的核心方法,程序设计语言是算法的实现方法。算法是解决问题的一个抽象方法和步骤,同一个算法在不同的语言中有不同的实现形式,这依赖于数据结构的形式和程序设计语言的语法格式。

 

 

4 算法的表示

为了便于交流和算法处理,往往需要将算法进行表示和描述,不能只是自己清楚。能够很好的表示一个算法,才能更好的进行交流和算法的处理。一般算法可采用自然语言、流程图、N-S图、伪代码表示。

4.1 自然语言表示

自然语言就是自然地随文化演化的语言,如汉语、英语等。通俗一点就是我们平时交流口头所说的语言。对于一些简单的算法,可以采用自然语言来口头进行描述,这样方便也快捷,利于交流和发展。但不适合于复杂的算法。

 

4.2 流程图表示

流程图一种用图形来表示算法流程的方法,由一些图标和连接线组成。流程图表示算法的优点是简单直观、便于理解,所以在计算机领域有广泛的应用。但也是相对麻烦。

在实际应用中一般采用顺序结构、分支结构和循环结构三种流程结构来表示。如果对流程图有所认识或者画过流程图的话,就容易明白。常用的流程图工具有微软的Visio、在线processon等,工作流Activity也有其绘图插件。

其中,顺序结构是最简单的一种,一个接一下往下处理。适用于简单的算法。

数据结构与算法(二)—— 算法基础知识与效率度量_第1张图片

分支结构常用来根据某个条件来决定算法的走向,比如Java语言中的if-else就有这样的作用。

循环结构常用于需要反复执行的算法操作。按循环方式可分为当型循环结构和直到型循环结构。这两种的区别如下:

  • 当型循环结构:先对条件进行决断,然后再执行,,一般采用while语言来实现;
  • 直到型循环结构:循环先执行,然后再对条件进行判断,一般采用until、do...while语言来实现。

需要注意的是,不论哪种循环都要进行适合的处理,以保证能跳出循环,否则容易造成死循环。

 

4.3 N-S图表示

N-S图也称为盒图或CHAPIN图,于1973年由美国学者I.Nassi和B.Shneiderman提出。他们发现采用流程图可以清楚的表示算法或程序的运行过程,但其中的流程线并不是必须的,因此创立了N-S图。

在N-S图中,把整个程序写在一个大框图内,这个大框由若干个小的基本框图构成。采用N-S图可以方便的表示流程图的内容。

数据结构与算法(二)—— 算法基础知识与效率度量_第2张图片

 

4.4 伪代码表示

伪代码(Pseudocode)是另一种算法描述的方式。伪代码并不是真正的程序代码,是介于自然语言和编程语言之间的代码,所以伪代码无法在计算机上运行。使用伪代码的目的是将算法描述成一种类似于编程语言的形式,如C、C++、Java等。这样程序员便可以很容易理解算法的结构,再根据编程语言的语法特点,稍加修改就可以实现一个真正的算法程序了。

之所以能够使用伪代码的一个重要原因是C语言的广泛使用,其它语言大都借鉴了C语言的语法特点。这些编程语言在很大程度上都和C语言类似,比如都采用if表示条件分支和判断等,可以利用这些共性来描述算法,而忽略编程语言之间的差异。

在使用伪代码表示算法时,程序员可以使用自然语言来进行表述,也可以采用简化的编程语句来表示,相当灵活。不过为了编程代码的交流和重利用,程序员还是应该尽可能地表述清楚。下面举例说明。

使用伪代码时,描述应该结构清晰、代码简单、可读性好,这样才能更有利于算法的表示,否则将适得其反,让人看不懂,就失去伪代码表示的意义了。

 

 

5 算法效率的度量

解决一件的方法有多种,算法很多时候也是有多种的。好的算法执行效率高,所花的时间短;差的算法往往需要花更多的时间,导致效率低下。

算法的一个重要任务就是找到合适的、效率最高的解决的办法,也就是最好的算法。理论上就需要对算法的性能有一个合理的评价。一个算法优劣往往通过算法复杂度衡量,算法的复杂度包括时间复杂度和空间复杂度两个方面。

 

5.1 时间复杂度

时间复杂度,简单理解就是算法执行所需要花的时间,时间越短算法越好。但一个算法执行的时间往往无法精确估计,只在实际运行了才知道。但有时候是不可能一个个去试验再决定用哪种算法,虽然有些时候是可以这样的,实际中可能也会这样去做。所以我们需要对算法代码进行评估,从而了解算法的时间复杂度。

语句频度(时间频度)

算法执行的时间和算法代码中语句的执行次数有关。由于每条语句执行都需要时间,所以语句执行的次数越多,整个程序所花的时间就越长。因此,简短精悍的算法程序往往执行速度较快。所以一个算法所花的时间与算法中语句的执行次数成正比,一个算法中语句执行次数称为语句频度或时间频度,记为T(n)。在《大话数据结构》中是这样说的

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

常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),k次方阶O(nk),指数阶O(2n)。这里用O来体现算法时间复杂度的标记方法,称为大O表示法。一般情况下,随着n的增大,T(n)增长最慢的算法为最优的算法。

算法的时间复杂度还与问题的规模有关。比如说一个算法代码中处理的1+1的去处,而另一个算法中处理的是亿和亿的相乘,那肯定就不一样量级的问题了。可能这个例子不太适合,那看了解下天气预报这样算法的数据计算量是非常大的,是10以内的加减法完全不能比的。

平均时间复杂度是所有可能的输入均以相等概念出现的情况下,执行算法所花的时间。平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。最理想情况下的时间复杂度是较少的,且比较困难。最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度就是最坏时间复杂度,特殊指定的除外。之所以这样做,是因为平均情况大多和最坏情况比较持平,这样可以保证算法执行的时间不会比最坏情况下还要长。

 

5.2 求解时间复杂度

推导大O阶方法

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

常数阶

首先看下顺序结构的时间复杂度。下面的代码,为什么时间复杂度不是O(3)而是O(1)?

这个算法的运行次数函数是f(n)= 3。根据推导大O阶的方法,第一步就是把常数3改为1。在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)。试想一下,若这个算法中sum = n * n + 5有10句,事实上无论n为多少,执行时间并没有什么变化,这种与问题的大小无关(n的多少),执行时间恒定的算法,称之为具有O(1)的时间复杂度,又叫常数阶

需要注意的是,不管这个常数是多少,都记作O(1),而不能记作O(3)或其它括号内不是1的数字。

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

虽然有n,也有n*n,但n是一个常数,不管外界传参如何或怎么样,这个n还是10。下面的三行代码都是各执行一次,算法的执行时间并不会随着问题规模n的增加而增加。即使算法中有几千上万条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度为常数阶O(1)。

public static void testConstant () {
        int n = 10, sum = 0;       // 执行1次
        sum = n*n + 5;             // 执行1次
        System.out.println(sum);   // 执行1次
    }

线性阶

线性阶的循环结构要复杂很多。要确定某个算法的阶次,就是我们常常需要确定某个特定语句或某个语句集运行的次数。因此,要分析算法的复杂度,关键就是要分析循环结构的运行情况

下面的这段代码,其时间复杂度为O(n),因为循环体中的代码需要执行n次。

 int i;
 for (i = 0; i < n; i ++) {
      // ...
 }

对数阶

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

int count = 1;
while (count < n) {
     count = count * 2;
}

平方阶

下面的例子是一个嵌套循环,内循环的时间复杂度上面已经分析过了为O(n)。对于外层循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为平方阶O(n²)。

int i, j;
for (i = 0; i < n; i ++) {
    for (j = 0; j < n; j ++) {
          // 时间复杂度为O(1)的程序步骤序列
    }
}

如果外层循环次数由n变成了m,那时间复杂度就变为O(m*n)。所以得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数

那下面的这个嵌套循环,它的时间复杂度是多少呢?

int i, j;
for (i = 0; i < n; i ++) {
    for (j = i; j < n; j ++) { // 这里j=i
          // 时间复杂度为O(1)的程序步骤序列
    }
}

上面的代码稍微做了改变,j=0换成了j=i。当i=0时,内循环执行n次,当i=1时,执行n-1次,...当i=n-1时,执行1次。所以总的执行次数为:

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

从这个例子中,可以得出一个经验,其实理解大O推导不算难,难的是对数列的一些相关运算,这更多的是考察数学知识和能力。所以要能这一块研究的好,数学知识是不能弱的。

再看下下面这段相对复杂的代码:

数据结构与算法(二)—— 算法基础知识与效率度量_第3张图片

数据结构与算法(二)—— 算法基础知识与效率度量_第4张图片

它的执行次数f(n)= ,根据推导大O阶方法,这段代码的时间复杂度也是O(n²)。

常见的时间复杂度如下表:

数据结构与算法(二)—— 算法基础知识与效率度量_第5张图片

它们所花费的时间从小到大依次是:

 

5.3 空间复杂度

空间复杂度,简单理解就是算法程序在计算机中执行所占用的存储空间。可分两个方面:

  • 程序保存所需要的存储空间:
  • 程序在执行过程中需要消耗的存储空间资源。

一般来讲,程序越小,执行过程中消耗的资源越少,这个程序越好。算法的空间复杂度通过计算算法所需要的存储空间来实现,算法空间复杂度的计算公式记作:S(n) = O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数

一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入的数据所占空间只取决于问题本身,和算法无关,那这时只需要分析算法在实现时所需要的辅助单元即可。若算法执行时所需要的辅助空间相对于数据量而言是个常数,则称此算法为原地工作,空间复杂度记为O(1)。

通常都使用时间复杂度来指运行时间的需求,使用空间复杂度指空间的需求。当不用限定词地使用复杂度时,通常指的是时间复杂度。这里重点分析的也是时间复杂度。

 

6 算法的新进展

算法是一门随着历史不断发展的学科。在计算机及程序设计出现之前,算法还停留在演算和手工计算的层面。在计算机出现后,算法在计算机编程领域再次获得了大发展,很多以前不可能实现的算法,现在都可以实现了。

算法也是一个扎根于数学和物理的科学。数学和物理学上的发展往往能激发一些新的算法应用的产生。下面展示一些近现代算法的新进展。

 

6.1 并行算法

我们经常接触到的算法模式都是单线程顺序计算的,即使采用了if分支语句或其它跳转语言,也都是按照单线程每次仅完成一步操作,这就是串行的意思。现在计算领域正在向并行计算方向发展,例如多核处理器、多台计算机的分布式并行计算等。在并行计算中,一个任务可以分布在多个线程中同时计算,这大大提升了计算的速度。

但并行计算的思路不同于传统的串行计算,因此需要一些新的算法来展示并行计算的优势。目前,划分法、分治法、平衡树法、倍增法/指针跳跃法、流水线法、破对称法等都是常用的并行算法。而且,由于并行计算是一个蓬勃发展的全新计算领域,相应的并行算法还在不断发展中。

其实,在实际应用中已经有很多领域开始应用并行算法了,主要体现在以下几个方面:

  • 常用的计算机已经全面进入双核多核时代;
  • 显卡的CPU是基于并行计算的思路;
  • 硬件电路设计领域,可编程逻辑器件FPGA/CPLD等都是基于并行处理的思想,可通过VHDL、Verilog等语言来实现并行算法的处理。

 

6.2 遗传与进化算法

遗传算法(Genetic Algorithm)和进化算法(Evloutionary Algorithms)是学科交叉的结果。遗传与进化算法根据生物的遗传、进化和变异的特性,通过模拟自然演化的方法来得到最优解。目前在组合优化、机器学习、信号处理、自适应控制、人工生命和人工智能等领域得到了广泛应用。

 

6.3 量子算法

量子物理学的发展是近现代物理学领域的最大突破,其提出了一系列颠覆性的概念和方法。量子物理学的发展,使其迅速与信息论和计算相结合,产生了量子信息技术和量子计算。

量子计算是一种依照量子力学理论进行的新型计算,它的基础和原理使能能够大大超越传统的图灵机模型的计算机。

量子计算(quantum computation)的概念最早由IBM的科学家R.Landauer及C.Bennett于20世纪70年代提出。1985年,牛津大学的D.Deutsch提出了量子图灵机的概念,量子计算才开始具备了数学的基本形式。采用量子计算机和相应的量子算法,可以实现超高速并行计算,现在为各国科学家的重要研究方向。

目前,已经发展的量子算法包括量子Shor算法、Grover搜索算法、Hogg搜索算法等。量子算法需要依赖于量子计算机来实现。

你可能感兴趣的:(数据结构与算法,算法)