数据结构与算法(一) 数据结构和算法
算法效率的度量方法;
函数调用的时间复杂度分析;
常见的时间复杂度;
算法的空间复杂度;
一、数据结构
数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机出来的符号集合
不仅包括整型、实型等数值类型。还包括字符、声音、图像、视频等非数值类型
成为数据具备的两个前提:
1.可以输入到计算机中
2. 能被计算机程序处理
数据元素:是组成数据的、有一定意义的基本单位,在计算机中作为整体处理。也被称为记录
数据项:一个数据元素可以由若干个数据项组成;
数据项是数据不可分隔的最小单位
数据对象:是性质相同的数据元素的集合,是数据的子集
性质相同是指数据元素具有相同数量和类型的数据项
结构:不同数据元素之间不是独立的,而是存在特定关系的,将这种关系称为结构;
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合
数据元素并不是孤立的、杂乱无序的,而是具有内在联系的数据集合 数据元素之间存在一种或多种特定关系,也就是数据的组织形式。
什么是一种或多种的特定关系
逻辑结构和物理结构
逻辑结构:数据对象中数据元素之间的相互关系
分为四种:
- 集合结构:集合结构中的数据元素除了同属于一个集合外,他们之间没有其他关系
- 线性结构:线性结构中的数据元素之间是一对一对应的关系
- 树形结构:树形结构中的数据元素之间存在一种一对多的层次关系
- 图形结构:图形结构的数据元素是多对多的关系
当用示意图表示数据的逻辑结构时,要注意:
- 将每个数据元素看成一个结点,用圆圈表示。
- 元素之间的逻辑关系用结点之间的连线表示,如果这个关系是有方向的,就用带箭头的连线表示。
物理结构(存储结构):是数据的逻辑结构在计算机中的存储形式。
数据是数据元素的集合,根据物理结构的定义,就是如何把数据元素存储到计算机的存储器中,存储器主要是针对内存而言
数据的存储结构应正确反映数据元素之间的逻辑关系,如何存储数据元素之间的逻辑关系,便是物理结构的重点和难点
数据元素的存储结构分为两种:
- 顺序存储结构:把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的,比如排队占位
- 链式存储结构:把数据元素存放在任意的存储单元里,这组存储单元可以是连续的也可以是不连续
数据元素的存储关系并不能反映其逻辑关系,因此需要一个指针存放数据元素的地址,这样通过地址就可以找到相关数据元素的位置
显然,数据存储在哪里不重要,只要有一个指针存放了相同的地址就能找到他了
抽象数据类型
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称
抽象是指取出事物具有普遍性的本质
抽线数据类型:(Abstract Data Type ,ADT):是指一个数学模型及定义在该模型上的一组操作
二、算法
算法效率的度量方法:
1. 事后统计方法:通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
缺陷:是根据以及编制好的程序去测试,如果测试的是糟糕的算法,会功亏一篑。
2. 事前分析估算方法:在计算机程序编写前,依据统计方法对算法进行估算。
高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
- 算法采用的策略,方案。
- 编译产生的代码质量
- 问题的输入规模
- 机器执行指令的速度
所以:一个程序的运行时间依赖于算法的好坏和问题的输入规模。
在编写程序的时候,我们不关心语言、所用的计算机只关心它所实现的算法。
在分析程序的运行时间的时候,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤,把基本操作的数量和输入模式进行关联。
函数渐进增长:
n=1时,算法A1效率不如算法B1;当n=2时,两者效率相等;当n>2时,算法A1开始优于算法B1,随着n的继续增加,算法A1比算法B1逐渐拉大差距。所以总体上算法A1比算法B1优秀
定义:给定2个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐进快于g(n)
例如如下算法的增长率:
通过这组数据可以看出,当n的值非常大的时候,3n+1已经不能和2n^2的结果进行比较,最终几乎是可以忽略不计的,算法G在跟算法I基本已经重合了,
结论:判断一个算法的时候,函数中的常数和其他次要项常常可以忽略,关注主项(最高项)的阶数。
注意:不能通过少量的数据来判断一个算法的好坏。
算法时间复杂度:
定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级,算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。他表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度,其中f(n)是问题规模n的某个函数。
用大O()来体现算法时间复杂度的记法,一般情况下随着输入规模n的增大,T(n)增长最慢的算法为最优算法。
推导大O阶的方法
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行函数中,只保留最高项。
- 如果最高项存在且不是1,则除这个项相乘的常数。
- 得到的最后的结果就是O()阶
例如:
1.常数阶
int sum=0,n=100; printf("hello word\n"); printf("hello word\n"); printf("hello word\n"); printf("hello word\n"); sum=(1+n)*n/2
所有常数项均可以看做是1,时间复杂度为O(1);
2.线性阶:
含有非嵌套循环涉及线性阶,就是随着问题规模n的扩大,对应计算次数呈直线增长。
int i,n=100,sum=0; for(i=0;i<n;i++){ sum=sum+i; }
循环中的代码需要执行n次,所以时间复杂度为O(n)
3.平方阶:
int i,j,n=100; for(i=0;i<n;i++)
{ for(j=0;j<n;j++){ printf("hello word") } }
外层执行一次,内层执行100次,需要执行100*100次,n的平方次,所以时间复杂度为O(n^2),
如果有三个这样的循环,则时间复杂度为O(n^3)
分析下,由于当i=0时,内循环执行了n次,当i=1时,内循环则执行n-1次…..当i=n-1时,内循环执行1次,所以总的执行次数应该是:- n+(n-1)+(n-2)+…+1 = n(n+1)/2,用我们推导大O的攻略,第一条忽略,因为没有常数相加。第二条只保留最高项,所以n/2这项去掉。第三条,去除与最高项相乘的常数,最终得O(n^2)。
4.对数阶
int i=1,n=100; while(i<n){ i=i*2; }
由于2^x=n得到x=log(2)n,所以这个循环的时间复杂度为O(log(2)n)
函数调用的时间复杂度分析:
例1:
int i,j; for(i=0;i<n;i++){ function(i); } void function(int count){ printf("%d",count); }
函数体是打印这个参数,function函数的时间复杂度是O(1),所以整体的时间复杂度就是循环的次数O(n)。
例2:
void function(int count){ int j; for(j=count;j<n;j++){ printf("%d",j); }
count++;
}
function内部的循环次数随count的增加而减少,所以它的时间复杂度为O(n^2)。
例3:
n++; ##1 function(n);##一个内部循环的函数 O(N^2)
for(i=0;i<n;i++){ function(i);##内部循环的函数 } ## O(N^2) for(i=0;i<n;i++){ for(j=i;j<n;j++){ printf("%d",j); }}##O(n^2)
时间复杂度为O(n^2)
常见的时间复杂度:
常用时间复杂度所耗费的时间从小到大依次是:
O(1)<O(logn)<(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
最坏情况与平均情况:
算法的分析也是类似,我们查找一个有n个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为O(1),但也有可能这个数字就在最后一个位置,它的时间复杂度为O(n),平均运行时间是期望的运行时间,最坏的运行时间是一种保证,在应用中,这是一种最重要的需求,通常除非特别的指定,我们提到的运行时间都是最坏情况的运行时间。
算法的空间复杂度:
算法的空间复杂度通过计算算法所需的存储空间实现,算法的空间复杂度的计算公式为:S(n)=O(f(n)),其中n为问题的规模,f(n)为语句关于n所占空间的函数。
一般情况”复杂度”指的是时间复杂度