什么是算法
按照某种逻辑关系组织起来的一批数据,按一定的方式把它存放在计算机的内存里,在这个基础上为了实现某个功能(比如查找某个元素,删除某个元素,给所有元素排序等等)而进行的一些列操作,我们把这一些列的操作步骤描述出来就是算法。
算法也可以解释为:计算机求解一个问题所需的一系列步骤。
算法的基本特性
1.输入:一个算法有0个或者多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身给出了初始条件;
2.输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
3.有穷性:算法必须能在执行有限个步骤之后终止;
4.确切性:算法的每一步骤必须有确切的定义;
5.可行性:算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步,即每个计算步都可以在有限时间内完成。
算法的设计要求
1.正确性:设计的算法能满足具体问题的需求,并且任何合法的输入都会得出正确的输出;
2.可读性:是指算法被写好之后,该算法理解的难易程度,一个算法可读性的好坏十分重要。如果一个算法比较抽象且难以理解,那么这个算法就不利于交流和推广使用,对于修改、扩展、维护来说都十分不方便,因此,在追求高效的同时,也应是算法尽量简明易懂。
3.健壮性:当输入数据非法时,算法也会做出相应的判断,而不会因为输入的错误而造成瘫痪。
4.时间效率高(时间复杂度)和需要的存储空间少(空间复杂度)
时间复杂度和空间复杂度是衡量算法优劣的重要指标:
时间复杂度:是用程序执行的次数来衡量,不是程序执行的时间。
空间复杂度:用程序执行所需要的最大内存。
**算法问题实质上就是数学问题,计算时间复杂度和空间复杂度远远没这么简单!
基本的数学概念的复习:
函数的定义:
给定一个数集A,假设其中的元素为x。现对A中的元素x施加对应法则f,记作f(x),得到另一数集B。假设B中的元素为y。则y与x之间的等量关系可以用y=f(x)表示。我们把这个关系式就叫函数关系式,简称函数。函数概念含有三个要素:定义域A、值域C和对应法则f。其中核心是对应法则f,它是函数关系的本质特征。
极限:
初等数学研究的对象是不变的量,高等数据研究的对象是变动的量。比如极限。
“极限”是数学中的分支——微积分的基础概念,广义的“极限”是指“无限靠近而永远不能到达”的意思。极限是一种“变化状态”的描述。此变量永远趋近的值A叫做“极限值”
对数:
在数学中,对数是对求幂的逆运算,正如除法是乘法的倒数,反之亦然。
如果a的x次方等于N(a>0,且a不等于1)a^x=N,那么数x叫做以a为底N的对数(logarithm),记作x=logaN。其中,a叫做对数的底数,N叫做真数。
下面举几个例子来理解时间复杂度
基本操作执行次数
关于代码的基本操作执行次数,我们用四个生活中的场景,来做一下比喻:
场景1:给小灰一条长10寸的面包,小灰每3天吃掉1寸,那么吃掉整个面包需要几天?
答案自然是 3 X (10/1) = 30天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 3 X (n/1) = 3n 天。
如果用一个函数来表达这个相对时间,可以记作 T(n) = 3n。
场景2:给小灰一条长16寸的面包,小灰每5天吃掉面包剩余长度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸…那么小灰把面包吃得只剩下1寸,需要多少天呢?
这个问题翻译一下,就是数字16不断地除以2,除几次以后的结果等于1?这里要涉及到数学当中的对数,以2位底,16的对数,可以简写为log16。
因此,把面包吃得只剩下1寸,需要 5 X log16 = 5 X 4 = 20 天。
如果面包的长度是 N 寸呢?
需要 5 X logn = 5logn天,记作 T(n) = 5logn。
场景3:给小灰一条长10寸的面包和一个鸡腿,小灰每2天吃掉一个鸡腿。那么小灰吃掉整个鸡腿需要多少天呢?
答案自然是2天。因为只说是吃掉鸡腿,和10寸的面包没有关系 。
如果面包的长度是 N 寸呢?
无论面包有多长,吃掉鸡腿的时间仍然是2天,记作 T(n) = 2。
场景4:给小灰一条长10寸的面包,小灰吃掉第一个一寸需要1天时间,吃掉第二个一寸需要2天时间,吃掉第三个一寸需要3天时间…每多吃一寸,所花的时间也多一天。那么小灰吃掉整个面包需要多少天呢?
答案是从1累加到10的总和,也就是55天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 1+2+3+…+ n-1 + n = (1+n)*n/2 = 0.5n^2 + 0.5n。
记作 T(n) = 0.5n^2 + 0.5n。
上面所讲的是吃东西所花费的相对时间,这一思想同样适用于对程序基本操作执行次数的统计。刚才的四个场景,分别对应了程序中最常见的四种执行方式:
场景1:T(n) = 3n,执行次数是线性的。
void eat1(int n){
for(int i=0; i<n; i++){;
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("吃一寸面包");
}
}
场景2:T(n) = 5logn,执行次数是对数的。
void eat2(int n){
for(int i=1; i<n; i*=2){
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("等待一天");
System.out.println("吃一半面包");
}
}
场景3:T(n) = 2,执行次数是常量的。
void eat3(int n){
System.out.println("等待一天");
System.out.println("吃一个鸡腿");
}
场景4:T(n) = 0.5n^2 + 0.5n,执行次数是一个多项式。
void eat4(int n){
for(int i=0; i<n; i++){
for(int j=0; j<i; j++){
System.out.println("等待一天");
}
System.out.println("吃一寸面包");
}
}
渐进时间复杂度
有了基本操作执行次数的函数 T(n),是否就可以分析和比较一段代码的运行时间了呢?还是有一定的困难。
比如算法A的相对时间是T(n)= 100n,算法B的相对时间是T(n)= 5n^2,这两个到底谁的运行时间更长一些?这就要看n的取值了。
所以,这时候有了渐进时间复杂度(asymptotic time complectiy)的概念。
官方的定义如下:
若存在函数 f(n),使得当n趋近于无穷大时,T(n)/ f(n)的极限值为不等于零的常数,则称 f(n)是T(n)的同数量级函数。
记作 T(n)= O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。渐进时间复杂度用大写O来表示,所以也被称为大O表达式。
我们一般情况下就是用大O表达式来表达程序的时间复杂度的,从而衡量程序那个好那个更好那个不好。
如何从基本操作执行次数推导出时间复杂度呢?有如下几个原则:
1.如果程序的运行次数和要处理的量n的大小没有关系,用常数1表示;O(1)
2.如果程序的运行次数和要处理的量n的大小有关系,只保留关系函数中的最高阶项; O(n^2)
3.如果最高阶项存在,则省去最高阶项前面的系数。 O(n^2)
让我们回头看看刚才的四个场景。
场景1:
T(n) = 3n
最高阶项为3n,省去系数3,转化的时间复杂度为:
T(n) = O(n) 大O线性阶
场景2:
T(n) = 5logn
最高阶项为5logn,省去系数5,转化的时间复杂度为:
T(n) = O(logn) 大O对数阶
场景3:
T(n) = 2
只有常数量级,转化的时间复杂度为:
T(n) = O(1) 大O常数阶
场景4:
T(n) = 0.5n^2 + 0.5n
最高阶项为0.5n^2,省去系数0.5,转化的时间复杂度为:
T(n) = O(n^2) 大O平方阶
大O表达式 算法的好坏
O(1) 最好
O(logn) 比较好
O(n) 良好
O(n^2) 不好
O(n^3) 很不好
O(2^n) 很很不好
O(n!) 最不好
举一个实例:
算法A的相对时间规模是T(n)= 100n*100,时间复杂度是O(n)
算法B的相对时间规模是T(n)= 5n^2, 时间复杂度是O(n^2)
随着输入规模 n 的增长,两种算法谁运行更快呢?
从表格中可以看出,当n的值很小的时候,算法A的运行用时要远大于算法B;当n的值达到1000左右,算法A和算法B的运行时间已经接近;当n的值越来越大,达到十万、百万时,算法A的优势开始显现,算法B则越来越慢,差距越来越明显。
空间复杂度
空间复杂度和时间复杂度很类似,当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);
当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(10g2n);当一个算法的空I司复杂度与n成线性比例关系时,可表示为O(n)…
了解了判断算法好坏的两个指标:需要的内存空间(可以 理解为运行代码需要的内存空间),代码运行的时间(可以简单的理解为代码需要执行的步数)。
程序的设计中要不就是时间换空间,要不就是用空间去换时间。
并且时间和空间是可以进行相互转化的:
对于执行的慢的程序,可以通过消耗内存(即构造新的数据结构)来进行优化。而消耗内存的程序,也可以多消耗时间来降低内存的消耗。
下面举个简单的例子:比如要写一个两个值互换的算法
//时间换空间
int a = 5;
int b = 10;
a = a+b;//得到a值为15
b = a-b;//得到b值为5
a = a-b;//得到a值为10
//空间换时间
int c = 5;
int d = 10;
int e = c;//得到e为5
c= d;//得到c值为10
d= e;//得到d值为
结论:
第一个a和b互换值的算法:总共进行了3次加减运算和三次赋值运算,能够把a和b的值进行互换,没有开辟多余的内存空间。
第二个c和d互换的时候,多开辟了一个内存空间存储e,但是这样只需要进行三次赋值运算就可以把c和d的值进行互换。
所以第一个算法空间效率高,时间效率低,第二个算法空间效率低,时间效率高。
我们在程序当中,请求分页,请求分段,都属于用时间去换空间。在项目当中使用各种缓存技术,都属于利用空间去换时间。