为什么选择Java编程语言?
C:非面向对象,写法复杂,大量内存管理代码。
C++:写法复杂,大量内存管理代码。
Objective-C、Swift:需要Mac系统。
JavaScript、Python:依赖于脚本解析器,同一个逻辑使用不同写法会影响代码性能,影响算法性能测评。
Java:语法丰富严谨,更多的注意力可以放到业务逻辑上,建议使用至少Java8(JDK1.8)。
Windows、Mac系统,均可轻松搭建Java开发环境。学好数据结构与算法,与编程语言无关。
一、什么是算法?
算法是用于解决特定问题的一系列的执行步骤。
使用不同的算法解决同一个问题,效率可能相差非常大。比如:求第n个斐波那契数(fibonacci number)。
斐波那契数列:从第3项开始,每一项都等于前两项之和。
0、1、1、2、3、5、8、13、21、34
1 | public class complexity { |
通过上面的两个方法可以看出,不同的算法效率相差巨大。使用递归求斐波那契数会随着数值增大最终导致程序直接卡死,但使用算法2就会非常高效。
二、算法复杂度
2.1. 如何判断一个算法的好坏?
如果单从执行效率上进行评估,可能会想到这么一种方案:比较不同算法对同一组输入的执行处理时间,这种方案也叫做“事后统计法”。
上面的方案有比较明显的缺点:执行时间严重依赖硬件以及运行时各种不确定的环境因素,而且还需要编写相应的测算代码,测试数据的选择比较难保证公正性。
1 | // 计算a+b的和 |
上面示例代码中,如果n是100,可能sum1比较快,sum2比较慢;如果n是200,可能sum1比较慢,sum2比较快。所以我们一般从以下纬度来评估算法的优劣:
- 正确性、可读性、健壮性(对不合理输入的反应能力和处理能力)
- 时间复杂度(time complexity):估算程序指令的执行次数(执行时间)
- 空间复杂度(space complexity):估算所需占用的存储空间
2.2. 时间复杂度的估算
1 | // O(1) |
2.3. 大O表示法
一般用大O表示法来描述复杂度,它表示的是数据规模n对应的复杂度。
- 忽略常数、系数、低阶
- 9(常数) >>
O(1)
- 3log2(n) >>
O(logn)
- 2n + 3 >>
O(n)
- 3n*log2(n) >>
O(nlogn)
- n^2 + 2n + 6 >>
O(n^2)
- 4n^3 + 3n^2 + 22n + 100 >>
O(n^3)
- 9(常数) >>
注意:大O表示法仅仅是一种粗略的分析模型,是一种估算,能帮助我们短时间内了解一个算法的执行效率。
2.4. 常见的复杂度
执行次数: O(1) < O(logn) < O(n) < O(nlogn) < O(n^2 ) < O(n^3 ) < O(2^n ) < O(n!) < O(n^n)
可以借助函数生成工具对比复杂度的大小:https://zh.numberempire.com/graphingcalculator.php
2.4.1. 数据规模较小时
2.4.2. 数据规模较大时
2.5. 斐波那契函数的时间复杂度分析
1 | // 方法一: |
方法二可以直接得出复杂度是O(n)
。
方法一的复杂度分析:
调用次数是1 + 2 + 4 + 8 = 2^0 + 2^1 + 2^2 + 2^3 = 2^4 − 1 = 2^(n−1) − 1 = 0.5 ∗ 2^n − 1
,所以复杂度是O(2^n)
。
方法一和方法二的的差别有多大呢?如果如果有一台1GHz的普通计算机,运算速度是10^9次每秒,假设n为64,O(n)大约耗时6.4 * 10^(−8) 秒,O(2^n) 大约耗时584.94年。所以有时候算法之间的差距,往往比硬件方面的差距还要大。
2.6. 算法的优化方向
- 用尽量少的存储空间;
- 用尽量少的执行步骤(执行时间);
- 根据情况,可以空间换时间或时间换空间。
2.7. 多个数据规模怎么表示复杂度?
1 | public static void test(int n, int k) { |
上面示例代码的时间复杂度是O(n + k)
。
三、LeetCode
LeetCode是一个用于练习算法的好网站。