数据结构(二) — 算法

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

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

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

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

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

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

原文地址:https://www.cnblogs.com/ZWOLF/p/10551888.html

时间: 2024-08-04 22:26:11

数据结构(二) — 算法的相关文章

【Java数据结构学习笔记之二】Java数据结构与算法之队列(Queue)实现

  本篇是数据结构与算法的第三篇,本篇我们将来了解一下知识点: 队列的抽象数据类型 顺序队列的设计与实现 链式队列的设计与实现 队列应用的简单举例 优先队列的设置与实现双链表实现 队列的抽象数据类型   队列同样是一种特殊的线性表,其插入和删除的操作分别在表的两端进行,队列的特点就是先进先出(First In First Out).我们把向队列中插入元素的过程称为入队(Enqueue),删除元素的过程称为出队(Dequeue)并把允许入队的一端称为队尾,允许出的的一端称为队头,没有任何元素的队列

数据结构与算法二

1.课程安排表: 1. 线性表 2. 字符串 3. 栈和队列 4.树 5.查找 6.排序 7.暴力枚举法 8.广度优先搜索 9.深度优先搜索 10.分治 11.贪心 12.动态规划 13.图 14.数学方法与常见模型 15.大整数运算 16. 基础功能 2.   编程技巧: 1.把较大的数组放在main 函数(全局变量)外,作为全局变量,这样可以防止栈溢出,因为栈的大小是有限制的.GCC (C编译器) 段错误 2.如果能够预估栈,队列的上限,则不要用stack,queue,使用数组来模拟,这样速

数据结构与算法系列研究二——栈和队列

栈和队列的相关问题分析 一.栈和队列定义 栈和队列是两种重要的数据结构.从结构特性角度看,栈和队列也是线性表,其特殊性在于它们的基本操作是线性表的子集,是操作受限的线性表,可称为限定性的数据结构:从数据类型角度看,其操作规则与线性表大不相同,是完全不同于线性表的抽象数据类型.                    图1 栈的结构                                                 图2 队列的结构   1.1.栈是限定在表的一端进行插入和删除操作的线性

php面试题之二——数据结构和算法(高级部分)

二.数据结构和算法 1.使对象可以像数组一样进行foreach循环,要求属性必须是私有.(Iterator模式的PHP5实现,写一类实现Iterator接口)(腾讯) <?php class Test implements Iterator{ private $item = array('id'=>1,'name'=>'php'); public function rewind(){ reset($this->item); } public function current(){

Java数据结构和算法(二)树的基本操作

Java数据结构和算法(二)树的基本操作 一.树的遍历 二叉树遍历分为:前序遍历.中序遍历.后序遍历.即父结点的访问顺序 1.1 前序遍历 基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树即根-左-右.图中前序遍历结果是:1,2,4,5,7,8,3,6. // 递归实现前序遍历 public void preOrder() { System.out.printf("%s ", value); if (left != null) { left.preOrder1(); }

算法与数据结构(二):链表

上一篇简单的开了一个头,简单介绍了一下所谓的时间复杂度与空间复杂度,从这篇开始将陆陆续续写一下常用的数据结构:链表.队列.栈.树等等. 链表当初是我在学校时唯一死磕过的数据结构,那个时候自己还算是一个好学生,虽然上课没怎么听懂,但是课后还是根据仔细调试过老师给的代码,硬是自己给弄懂了,它是我离校时唯一能够写出实现的数据结构,现在回想起来应该是它比较简单,算法也比较直来直去吧.虽然它比较简单,很多朋友也都会链表.但是作为一个系列,如果仅仅因为它比较简单而不去理会,总觉得少了点什么,所以再这仍然将其

数据结构与算法系列二(复杂度分析)

1.引子 1.1.为什么要学习数据结构与算法? 有人说,数据结构与算法,计算机网络,与操作系统都一样,脱离日常开发,除了面试这辈子可能都用不到呀! 有人说,我是做业务开发的,只要熟练API,熟练框架,熟练各种中间件,写的代码不也能“飞”起来吗? 于是问题来了:为什么还要学习数据结构与算法呢? #理由一: 面试的时候,千万不要被数据结构与算法拖了后腿 #理由二: 你真的愿意做一辈子CRUD Boy吗 #理由三: 不想写出开源框架,中间件的工程师,不是好厨子 1.2.如何系统化学习数据结构与算法?

Java数据结构和算法(二)——数组

数组的用处是什么呢?--当你需要将30个数进行大小排列的时候,用数组这样的数据结构存储是个很好的选择,当你是一个班的班主任的时候,每次要记录那些学生的缺勤次数的时候,数组也是很有用.数组可以进行插入,删除,查找等. 1)创建和内存分配 Java中有两种数据类型,基本类型和对象类型,也有人称为引用类型,Java中把数组当成对象,创建数组时使用new操作符. int array[] = new int[10]; 既然是对象,那么array便是数组的一个引用,根据Java编程思想(一) -- 一切都是

Java数据结构和算法(二)——数组

上篇博客我们简单介绍了数据结构和算法的概念,对此模糊很正常,后面会慢慢通过具体的实例来介绍.本篇博客我们介绍数据结构的鼻祖——数组,可以说数组几乎能表示一切的数据结构,在每一门编程语言中,数组都是重要的数据结构,当然每种语言对数组的实现和处理也不相同,但是本质是都是用来存放数据的的结构,这里我们以Java语言为例,来详细介绍Java语言中数组的用法. 1.Java数组介绍 在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型. ①.数组的声明 第一种方式: 数据类型 []

数据结构与算法 3:二叉树,遍历,创建,释放,拷贝,求高度,面试,线索树

[本文谢绝转载,原文来自http://990487026.blog.51cto.com] 树 数据结构与算法 3:二叉树,遍历,创建,释放,拷贝,求高度,面试,线索树 二叉树的创建,关系建立 二叉树的创建,关系建立2 三叉链表法 双亲链表: 二叉树的遍历 遍历的分析PPT 计算二叉树中叶子节点的数目:使用全局变量计数器 计算二叉树中叶子节点的数目:不使用全局变量计数器 无论是先序遍历,中序遍历,后序遍历,求叶子的数字都不变;因为本质都是一样的,任何一个节点都会遍历3趟 求二叉树的高度 二叉树的拷