一、什么是算法
算法定义:为解决一个问题而采取的方法和步骤,称为“算法”。
二、算法有哪些特征?
①有穷性(Finiteness)—— 算法必须能在执行有限个步骤之后终止;
②确切性(Definiteness)—— 算法的每一步骤必须有确切的定义;
③输入项(Input)—— 一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
④输出项(Output)
一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
⑤可行性(Effectiveness)
算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性)。
一个问题可以通过多种算法来解决,那么如何衡量哪个算法更好呢?显然一个算法执行时间越短,占用的内存空间越小,这样的算法就越好。通常有两种方法来比较:事前分析和事后统计。
三、事后统计法
如果单从执行效率上进行评估,可能会想到比较不同算法对同一组输入的执行处理时间
这种方案叫做:事后统计法
缺点:
①执行时间严重依赖硬件以及运行时各种不确定的环境因素② 必须编写相应的测算代码
③测试数据的选择比较难保证公正性
我们以上一章的斐波那契数列为例:
//根据定义来实现
public static int fib(int n) {
//退出递归的条件
if (n == 0 || n == 1) return 1;
return fib(n-1) + fib(n-2);
}
//迭代
public static int fib2(int n) {
if (n<=1) return 1;
int first = 1;
int second =1;
for (int i = 0; i < n - 1; i++) {
int sum = first + second;
first = second;
second = sum;
}
return second;
}
这是主函数的测试代码
public static void main(String[] args) {
int n = 45;
//迟迟出不来结果
//System.out.println(fib(64));
//立刻出结果
// System.out.println(fib2(64));
TimeTool.check("fib", new Task() {
@Override
public void execute() {
// 需要测试的代码
System.out.println(fib(n));
}
});
TimeTool.check("fib2", new Task() {
@Override
public void execute() {
// 需要测试的代码
System.out.println(fib2(n));
}
});
}
1.1执行时间严重依赖硬件以及运行时各种不确定的环境因素
当在主函数里面进行测试时,如果执行fib()函数时,后台打开的软件比较少;而执行fib2()时,后台打开的软件比较多,即CPU处于繁忙状态;或者此时网络状况不佳,都会影响最终的执行时间
1.2必须编写相应的测算代码
因为想要根据对同一个数据两种算法的执行时间进行算法的比较,那我们即必须得花时间写一个测试时间的代码
1.3测试数据的选择比较难保证公正性
就斐波那契数列而言,当n很小的时候,两个算法的时间都为零,随着n的增大,我们才发现迭代算法消耗的时间更短,几乎为0(斐波那契数列那章有具体的时间截图);但是如果现在有两个算法,刚开始算法1的时间短,达到某个数值后,算法2的时间更短,那么这个数据我们该如何选择?一直测试数据观察变化是不是太浪费时间了?
四、五个方面评估算法的好坏
4.1正确性
算法的正确性是评价一个算法优劣的最重要的标准。正确的算法能够满足具体问题的需求,程序运行正常,无语法错误,能过通过典型的软件测试,达到预期需求规格。正确性也是一个最基本的要求
4.2可读性
可读性:算法的可读性是指一个算法可供人们阅读的容易程度。好的算法应该遵循标识符命名规则,简洁,易懂,注释语句恰当、适量,方便自己和他人阅读,便于后期调试和修改。
4.3健壮性
健壮性:健壮性是指一个算法对不合理数据输入的反应能力和处理能力,也称为容错性23。一个好的算法应该具有较高的鲁棒性,能够在各种情况下都能正确地执行任务,输出清晰的信息
4.4时间复杂度(time complexity)
这是衡量算法执行速度的主要指标,表示随着输入数据规模的增大,算法执行时间的增长趋势。一般来说,时间复杂度越小,算法的执行速度越快
4.5空间复杂度(space complexity)
这是衡量算法内存消耗的指标,表示随着输入数据规模的增大,算法所需内存空间的增长趋势。一般来说,空间复杂度越小,算法所需的内存空间越少
五、事前分析法
主要通过分析算法的时间复杂度和空间复杂度来进行
5.1时间复杂度的计算
计算时间复杂度需要考虑以下几个因素:
- 算法中基本操作的执行次数
- 输入数据的规模
- 算法的执行时间与机器性能无关,即计算机语言、硬件等不影响算法的时间复杂度
具体的计算步骤如下:
- 找到算法的基本操作
- 统计基本操作的执行次数
- 建立基本操作执行次数与输入数据规模的关系
- 确定时间复杂度的最高次项,即最慢增长项
通常用大O符号表示时间复杂度,例如常数阶O(1)、对数阶 O(logn)、线性阶 O(n)、线性对数阶 O(nlogn)、平方阶 O(n²)等。其中,O表示算法的渐进时间复杂度,它描述的是算法的时间复杂度随着输入规模的增大所呈现的增长趋势。如果算法中的基本操作执行次数与输入规模n成线性关系,那么时间复杂度就是O(n)。如果基本操作执行次数是n的平方,那么时间复杂度就是O(n²)。
5.1.1案例分析
注意:大O表示法忽略常数、系数、低阶
时间复杂度O(1)
public static void test1(int n) {
// 执行次数是一个确定数字,与输入数据无关,称 时间复杂度为 O(1)
if (n > 10) {
System.out.println("n > 10");//判断1+输出1
} else if (n > 5) { // 2
System.out.println("n > 5");//判断2+输出1
} else {
System.out.println("n <= 5"); //判断3+输出1
}
// 赋值1 + 判断5 + i++ 4 + 输出4
for (int i = 0; i < 4; i++) {
System.out.println("test");
}
}
时间复杂度O(n)
public static void test2(int n) {
// O(n) 与输入的数据成正比
// 赋值1 + 判断n+1 + i++ n + 输出n = 3n+2
//省去系数,只看最高阶
for (int i = 0; i < n; i++) {
System.out.println("test");
}
}
public static void test3(int n) {
/*
*抓内层循环体
*外层n次,外层每执行一次, 内层15次
*所以估算 15n
*大O估算法省去系数
*/
// O(n)
for (int i = 0; i < n; i++) {
for (int j = 0; j < 15; j++) {
System.out.println("test");
}
}
}
时间复杂度O(n²)
public static void test4(int n) {
/*
* 抓主干 只看执行次数最多的 当然是循环体
* 这里其实不够严谨,j<n 判断语句的执行次数要比输出语句多
* 因为每次内层循环结束的时候,都会在执行一次判断
* 而j++的次数与循环体的执行次数是一样
* 因为是估算,最后只留下最高阶,所以不必关心系数和常数
*/
//很明显外层每执行一次,内层执行n次,外层总共执行n次
// O(n²) 与输入 数据的平方成正比
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
}
时间复杂度O(logn)
public static void test5(int n) {
// 输出语句执行次数 = log2(n)
//无论是log2(n)还是log5(n)统称为O(logn)
// O(logn) 表示与输入数据的对数成正比
while ((n = n / 2) > 0) {
System.out.println("test");
}
}
public static void test6(int n) {
// log5(n)
// O(logn)
while ((n = n / 5) > 0) {
System.out.println("test");
}
}
时间复杂度O(nlogn)
public static void test7(int n) {
// 外层循环每执行一次,内层循环执行n次,外层循环总共执行log2(n)
// O(nlogn) 表示与输入数据nlogn成正比
for (int i = 1; i < n; i = i * 2) {
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
}
5.2空间复杂度的计算
空间复杂度是指算法在执行过程中所需的存储空间,通常指算法所使用的内存空间大小。空
根据算法的空间复杂度计算结果,可以用大O符号表示算法的空间复杂度。
5.2.1案例分析
空间复杂度O(1)
public static void test1(int n) {
if (n > 10) {
System.out.println("n > 10");
} else if (n > 5) { // 2
System.out.println("n > 5");
} else {
System.out.println("n <= 5");
}
// 只在这里定义了一个整型变量
for (int i = 0; i < 4; i++) {
System.out.println("test");
}
}
空间复杂度O(n)
public static void test2(int n) {
// 空间复杂度 O(n) 开辟空间的大小跟输入数据n成正比
//int i = 0 一个整型变量的空间,属于常数项,还是抓大头
//int[] array = new int[n]; 开辟了n个整型大小的空间
int a = 10;
int b = 20;
int c = a + b;
int[] array = new int[n];
for (int i = 0; i < array.length; i++) {
System.out.println(array[i] + c);
}
}
同样的,如何我们把数组的大小改成 logn、nlogn 、n²、n³,那么空间复杂度就是 O(logn) 、O(nlogn) 、 O(n²) ;
需要注意的是,空间复杂度和时间复杂度不同,时间复杂度是指算法执行的时间与问题规模n之间的关系,而空间复杂度是指算法所需的空间与问题规模n之间的关系。
5.3 大O表示法的大小比较
O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
O(1)表示常量时间算法,不管输入的大小是多少,运行时间都是恒定的。(大O表示法规定只要是常数项,哪怕是10000000也是O(1),但是如果常数项能达到10000000这么大,那我们就应该考虑一下是不是写错了)
O(logn)表示对数时间算法,运行时间随输入的增加而增加,但不是线性的,例如二分查找算法。
O(n)表示线性时间算法,运行时间与输入的大小成正比,例如遍历数组算法。
O(nlogn)表示近似线性时间算法,通常出现在快速排序、归并排序等算法中。
O(n²)表示平方时间算法,运行时间与输入的大小的平方成正比,例如嵌套循环算法。
O(n³)表示立方时间算法,运行时间与输入的大小的立方成正比。
O(2ⁿ)表示指数时间算法,运行时间随着输入的增加呈指数级增长,例如暴力枚举算法。
O(n!)表示阶乘时间算法,运行时间随着输入的增加呈指数级增长,例如旅行商问题算法。
因此,在设计算法时,我们应该尽可能地避免使用大O符号表示更高级别的时间复杂度算法。
注意:大O表示法仅仅是一种粗略的分析模型,是一种估算,能帮助我们短时间了解一个算法的执行效率