打个比方,如果你在图书馆想要寻找指定的一本书,一本一本找的话不知要找到猴年马月,所以这时候就要有一个关键词,先找到这一类书,再从这类书籍去找,这样就会快很多。这里的如何查找就是所谓的算法。
算法是解决问题的有限集合,用某种计算机语言来描述,通过时间复杂度和空间复杂度来衡量算法的好与坏。
算法具有零个或多个输入尽管对于绝大多数算法来讲,输入参数都是必要的,但对于个别情况,例如打印**"hello world ! "**这样的代码,不需要任何输入参数,因此算法的输入可以是零个。
算法至少有一个或多个输出算法是一定需要输出的,若不需要输出,那你要这个算法干嘛?输出的形式可以是打印输出,也可以返回一个或多个值。
有穷性:指的是算法在执行有限的步骤之后,自动结束而不会出现死循环,并且一个步骤可以在可接受的时间内完成。 当然 ,这里的有穷性的概念并不是纯数学意义的,如果说你写了一个算法,计算机需要花十年二十年才能计算出来,这里结束了,那么他在数学的意义上就是有穷了。
确定性:算法的每一步都具有确定的含义,不会出现二义性,保证算法的每个步骤被精确定使用而无歧义。
可行性:算法的每一步都必须是可行的,也就是说,每一步都能通过执行有限次数完成。
算法不是唯一的,也就是说同一个问题,可以有多种解决问题的算法。
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性,能正确反映问题的需求,能够得到问题的正确答案。
但是算法的“正确”通常在用法上有很大的差别,大体分为以下四个层次
要求的
算法程序没有语法错误。(要求低,但语法没错并不能证明是个好算法)
算法程序对于合法的输入数据能够产生满足要求的输出结果。
算法程序对于非法的输入数据能够得出满足规格说明的结果。(作为算法是否正确的标准)
算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。(这个才是最困难的)
算法设计的另一个目的是为了便于阅读、理解和交流。可读性高有利于他人去理解,而那些晦涩难懂的算法往往隐含错误,因为不容易被发现,而且难于调试和修改。
当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。一个好的算法还应该对数据不合法的情况做出适当的处理。
好的算法同时还应该具备时间效率高和存储量低的特点。
时间效率指的是算法的执行时间
存储量需求值得是算法在执行过程中需要的最大存储空间
这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序运行时间进行比较,从而确定不同算法效率的高低。
但这种方法显然是有很大缺陷的
必须依据算法事先编制好程序了,这需要花费大量的时间和精力。
不同的处理器、操作系统、编译器、运行框架等的不同,都会影响他们的结果。
算法的测试数据设计困难,并且程序的运行时间往往还与测试数据的规模有很大的关系,效率高的算法在小的测试数据面前往往得不到体现。
在计算机程序编制前,依据统计方法对算法进行估算。
一个高级程序语言编写的程序在计算机运行时所消耗的时间取决于下列因素:
(1)算法采用的策略、方法。
(2)编译产生的代码质量。
(3)问题的输入规模。
(4)机器执行指令的速度。
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的量级。算法的时间复杂度,也就是算法的时间量度,记作T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
(1)用常数1取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且其系数不是1,则去除与这个项相乘的系数。
得到的结果就是大O阶。
O(1)—常数阶
O(N)—线性阶
O(logN)—对数阶
O(nlongn)—线性对数阶
O(n^2)—平方阶
O(1)—常数阶
// 计算Func1的时间复杂度?
void Func1(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
这个算法的运行次数函数是f(n)=100,根据我们推导大O阶的方法,第一步就是把常数项100改为1.在保留最高项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1);
注意:不管这个常数是多少,我们都记作O(1),而不能是O(100)等其他任何数字
O(N)—线性阶
// 计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}//一层循环,执行n次
int M = 10;
while (M--)
{
++count;
}//执行10次
printf("%d\n", count);
}
基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)
O(longN)—对数阶
//计算func3的时间复杂度
void func3(int n)
{
int i=l;
while(i<=n)
i=i*2;
}
此函数有一个循环,但是循环没有被执行n次,i每次都是2倍进行递增,所以循环只会被执行log2(n)次,时间复杂度为O(logn)。
O(nlogn)—线性对数阶
for (m = 1; m < n; m ++){
i = 1;
while (i < n){
i = i * 2;
}
}
线性对数阶将时间复杂度为 O(logn)的代码循环n遍,就是n*O(logn),所以它的时间复杂度为O(nlogn)
O(n^2)—平方阶
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}//嵌套循环,执行了N*N次
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}//一层循环,执行2*N次
int M = 10;
while (M--)
{
++count;
}//循环执行10次
printf("%d\n",count);
}
函数执行了N*N+2N+10次,即时间复杂度为O(N^2)
实际中我们计算时间复杂度时,并不要求计算精确的执行次数,只需要大概的执行次数,我们可以用大O渐进表示法,也就是抓"大头",取对结果又决定性的那一项。
许多算法题,包括校招的考察题目都会有小要求:时间复杂度为O(N)或者O(1)等等,所以时间复杂度也是本章的重点。
一个算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。渐近空间复杂度也常常简称为空间复杂度。空间复杂度(SpaceComplexity)是对一个算法在运行过程中临时占用存储空间大小的量度。一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。
首先要明白一个概念,变量的内存分配发生在定义的时候
递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程
//例题1
int a=0;
int b=1;
printf("%d",a+b);
这里有两个局部变量分配了存储空间,所以f(n)=1+1=2,忽略常数用O(1)表示,它的空间复杂度为O(1)。
int func(int N)
{
int k=10;
if(N==k)
return n;
else
return func(++N);
}
递归实现,调用func函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1)=O(n)。
# 总结
1. 算法的定义:算法是解决特定问题求解步骤的描述,在计算机中为指令的有限程序列,并且每条指令表示一个或多个操作。
2. 算法的特性:输入、输出、有穷性、确定性、可行性。
3. 算法的设计要求:正确性、可读性、健壮性、高效率和低存储量需求。
(算法的特性和设计要求容易混淆,需要对比记忆)
4. 算法的度量方法:事后统计法(不科学、不准确)、事后分析估算法。
5. 推导大O阶
(1)用常数1取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且其系数不是1,则去除与这个项相乘的系数。
6. 常见的时间复杂度所耗时间大小排序
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(n!)