您的位置:首页 > 理论基础 > 数据结构算法

数据结构与算法简介

2016-11-15 18:45 197 查看
一、算法定义:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。

二、算法特性:

五个基本特性:输入、输出、有穷性、确定性、可行性。

(1)输入输出:算法具有零个或多个输入,至少有一个或多个输出。

(2)有穷性:指算法在执行有效步骤后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。

(3)确定性:算法的每一步骤都具有确定的含义,不会出现二义性。

(4)可行性:算法的每一步都必须是可行的,每一步都能通过执行有限次完成。

三、算法设计的要求:

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

主要分为以下四个层次:

1、算法程序没有语法错误;

2、算法程序对于合法的输入数据能够产生满足要求的输出结果;

3、算法程序对于非法的输入数据能够得到满足规格说明的结果;

4、算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。

上述四层含义中,1层要求最低,4层是最困难的。一般情况下,我们把层次3作为一个算法是否正确的标准。

(2)、可读性:算法设计的另一目的是为了便于阅读、理解和交流。

(3)、健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名奇妙的结果。

(4)、时间效率高

(5)、存储量低。

四、算法效率的度量方法

事后统计方法:主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低,但这种方法有很大缺陷,一般不予采纳。

事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。

一个用高级语言编写的程序在计算机上运行时所消耗的时间取决于以下因素:

1、算法采用的策略,方法;(算法好坏的根本)

2、编译产生的代码质量;(由软件来支持)

3、问题的输入规模;

4、机器执行指令的速度。(看硬件的性能)

因此,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。

函数的渐近增长:给定两个函数f(n)和g(n),若存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,则f(n)的增长渐近快于g(n).

判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。

五、算法时间复杂度

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

根据定义,求解算法的时间复杂度的具体步骤是:

⑴ 找出算法中的基本语句;

  算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。

  ⑵ 计算基本语句的执行次数的数量级;

  只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。

  ⑶ 用大Ο记号表示算法的时间性能。

  将基本语句执行次数的数量级放入大Ο记号中。

如何推导大o阶呢?我们给出了下面 的推导方法:

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

2.在修改后的运行次数函数中,只保留最髙阶项。

3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。

简单的说,就是保留求出次数的最高次幂,并且把系数去掉。 如T(n)=2n^2+n+1 =O(n^2)

举个例子:

[cpp] view
plain copy

#include "stdio.h"

int main()

{

int i, j, x = 0, sum = 0, n = 100; /* 执行1次 */

for( i = 1; i <= n; i++) /* 执行n+1次 */

{

sum = sum + i; /* 执行n次 */

for( j = 1; j <= n; j++) /* 执行n*(n+1)次 */

{

x++; /* 执行n*n次 */

sum = sum + x; /* 执行n*n次 */

}

}

printf("%d", sum); /* 执行1次 */

}

按照上面推导“大O阶”的步骤,我们来看

第一步:“用常数 1 取代运行时间中的所有加法常数”,

则上面的算式变为:执行总次数 =3n^2 + 3n + 1

(直接相加的话,应该是T(n) = 1 + n+1 + n + n*(n+1) + n*n + n*n + 1 = 3n^2 + 3n + 3。现在用常数 1 取代运行时间中的所有加法常数,就是把T(n) = 3n^2 + 3n + 3中的最后一个3改为1. 就得到了 T(n) = 3n^2 + 3n + 1)

第二步:“在修改后的运行次数函数中,只保留最高阶项”。

这里的最高阶是 n 的二次方,所以算式变为:执行总次数 = 3n^2

第三步:“如果最高阶项存在且不是 1 ,则去除与这个项相乘的常数”。

这里 n 的二次方不是 1 所以要去除这个项的相乘常数,算式变为:执行总次数 = n^2

因此最后我们得到上面那段代码的算法时间复杂度表示为: O( n^2 )

下面我把常见的算法时间复杂度以及他们在效率上的高低顺序记录在这里,使大家对算法的效率有个直观的认识。

O(1) 常数阶 < O(logn) 对数阶 < O(n) 线性阶 < O(nlogn) < O(n^2) 平方阶 < O(n^3) < { O(2^n) < O(n!) < O(n^n) }

最后三项用大括号把他们括起来是想要告诉大家,如果日后大家设计的算法推导出的“大O阶”是大括号中的这几位,那么趁早放弃这个算法,在去研究新的算法出来吧。因为大括号中的这几位即便是在 n 的规模比较小的情况下仍然要耗费大量的时间,算法的时间复杂度大的离谱,基本上就是“不可用状态”。

好了,原理就介绍到这里了。下面通过几个例子具体分析下时间复杂度计算过程。

一、计算 1+2+3+4+.....+100

常规算法,代码如下:

[cpp] view
plain copy

#include "stdio.h"

int main()

{

int i, sum = 0, n = 100; /* 执行1次 */

for( i = 1; i <= n; i++) /* 执行 n+1 次 */

{

sum = sum + i; /* 执行n次 */

//printf("%d \n", sum);

}

printf("%d", sum); /* 执行1次 */

}

从代码附加的注释可以看到所有代码都执行了多少次。那么这写代码语句执行次数的总和就可以理解为是该算法计算出结果所需要的时间。该算法所用的时间(算法语句执行的总次数)为: 1 + ( n + 1 ) + n + 1 = 2n + 3

而当 n 不断增大,比如我们这次所要计算的不是 1 + 2 + 3 + 4 + ...... + 100 = ? 而是 1 + 2 + 3 + 4 + ...... + n = ?其中 n 是一个十分大的数字,那么由此可见,上述算法的执行总次数(所需时间)会随着 n 的增大而增加,但是在 for 循环以外的语句并不受 n 的规模影响(永远都只执行一次)。所以我们可以将上述算法的执行总次数简单的记做: 2n 或者简记 n

这样我们就得到了我们设计的算法的时间复杂度,我们把它记作: O(n)



再来看看高斯的算法,代码如下:

[cpp] view
plain copy

#include "stdio.h"

int main()

{

int sum = 0, n = 100; /* 执行1次 */

sum = (1 + n) * n/2; /* 执行1次 */

printf("%d", sum); /* 执行1次 */

}

这个算法的时间复杂度: O(3),但一般记作 O(1)。

从感官上我们就不难看出,从算法的效率上看,O(1) < O(n) 的,所以高斯的算法更快,更优秀。

二、求两个n阶方阵C=A*B的乘积其算法如下:



void MatrixMultiply(int A

,int B

,int C

)

{

(1) for(int i=0; i <n; i++) //n+1

{

(2) for (j=0;j < n; j++) //n*(n+1)

{

(3) C[i][j]=0; //n^2

(4) for (k=0; k<n; k++) //n^2*(n+1)

{

(5) C[i][j]=C[i][j]+A[i][k]*B[k][j]; //n^3

}

}

}

}

则该算法所有语句的执行次数之和为:

T(n) = 2n^3+3n^2+2n+1; 利用大O表示法,该算法的时间复杂度为O(n^3)。

三、分析下列时间复杂度

[cpp] view
plain copy

void test_(int n)

{

i = 1, k = 100;

while (i<n)

{

k = k + 1;

i += 2;

}

}

设for循环语句执行次数为T(n),则 i = 2T(n) + 1 <= n - 1, 即T(n) <= n/2 - 1 = O(n)

四、分析下列时间复杂度

void test_2(int b[], int n)

{

int i, j, k;

for (i=0; i<n-1; i++)

{

k = i;

for (j=i+1; j<n; j++)

{

if (b[k] > b[j])

{

k = j;

}

}

x = b[i];

b[i] = b[k];

b[k] = x;

}

}

其中,算法的基本运算语句是

if (b[k] > b[j])

{

k = j;

}

其执行次数T(n)为:



五、分析下列时间复杂度

[cpp] view
plain copy

void test_3(int n)

{

int i = 0, s = 0;

while (s<n)

{

i++;

s = s + i;

}

}

其中,算法的基本运算语句即while循环内部分,

设while循环语句执行次数为T(n),则



六、递归算法时间复杂度分析

[cpp] view
plain copy

void hanoi(int n, char a, char b, char c)

{

if (n==1)

{

printf("move %d disk from %c to %c \n", n, a, c); //执行一次

}

else

{

hanoi(n-1, a, c, b); //递归n-1次

printf("move %d disk from %c to %c \n", n, a, c); //执行一次

hanoi(n-1, b, a, c); //递归n-1次

}

}

对于递归函数的分析,跟设计递归函数一样,要先考虑基情况(比如hanoi中n==1时候),这样把一个大问题划分为多个子问题的求解。

故此上述算法的时间复杂度的递归关系如下:













六、最坏情况与平均情况

最坏情况运行时间是一种保证,那就是运行时间不会再坏了。在应用中,这是一种最重要的需求。通常,我们提到的运行时间都是最坏情况的运行时间。

平均时间是所有情况中最有意义的,是期望运行时间。

七、算法空间复杂度

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,这种算法是节省存储的算法;有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元。
如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(10g2n);当一个算法的空间复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: