数据结构与算法学习笔记
复杂度分析
算法的效率评估
一般而言,算法的效率分为时间效率和空间效率两种,所以一般算法的追求都是两者都达到最佳实践。
一般而言算法效率评估的方式有两种:实际测试和理论估算
实际测试:
有两个局限性:
第一在不同的机器上同一个算法的表现是不一样的,而且就算在同一个机器上的两个算法,随着机器改变算法仍旧效率会不一样。
第二有些算法在数据集大的时候好用有些在数据集小的时候好用,所以展开一个完整的测试往往很麻烦
理论估计:
一般通过一些计算来评估算法的效率,称之为复杂度分析,用于判断时间随着输入数据大小的增加,算法执行所需时间和空间的增长趋势。
迭代和递归
迭代是什么?
是重复执行一个任务的控制结构,迭代中程序会在满足一定条件的时候执行某个任务直到条件不再满足
一般而言,while循环对于for循环的自由度更高,我们可以更自由地设计条件变量的初始化和更新步骤,但是for循环更加的简洁紧凑,要根据实践来研究
Eg:用while更好实现的例子
function whileLoopII(n) {
let res = 0;
let i = 1; // 初始化条件变量
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += i;
// 更新条件变量
i++;
i *= 2;
}
return res;
}
嵌套循环:
就是一个循环里面套一个循环,一般而言每套一层循环都会进行一次升维,如二重循环函数操作数量就和n方成正比。
递归是什么?
是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段。
-
递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
-
归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。
Eg:
/* 递归 */ function recur(n) { // 终止条件 if (n === 1) return 1; // 递:递归调用 const res = recur(n - 1); // 归:返回结果 return n + res; }这里如果以n=3为例子,就会一层层的先把n递到1然后再从1开始一次次把结果加回去也就是归
递归和迭代的区别
- 迭代:“自下而上”地解决问题。从最基础的步骤开始,然后不断重复或累加这些步骤,直到任务完成。
- 递归:“自上而下”地解决问题。将原问题分解为更小的子问题,这些子问题和原问题具有相同的形式。接下来将子问题继续分解为更小的子问题,直到基本情况时停止(基本情况的解是已知的)。
每次递归的时候系统都会给新开始的函数分配内存,在触发终止条件之前,这些内存不会被释放,所以递归一般比比迭代的效率更低,如果有n个未完成的递归函数,递归深度就为n
首尾递归的区别:
以求和为例,一般而言求和操作在普通递归中是在归的环节中进行的,所以要保留上下文,但是如果是尾递归,在递的环节就已经做了求和了,归只需要返回就行了,所以每次递以后就可以删除上下文节省空间。
递归树:
以斐波那契数列为例,每次处理问题的时候都会把一个问题分解为两个更小的子问题来解决,达成递归树的效果。所以本质上递归的核心在于把问题分解的这种思维范式。
在算法的角度看,搜索,排序,回溯,分治,动态规划都会用到递归。
从数据结构的角度看,递归天然适合处理链表,树,图类问题,因为它们很容易用分治的思想来解决问题
时间复杂度:
一般而言,时间复杂度统计的是算法运行时间随着数据输入的增长趋势,而不是具体的统计出我到底一个算法要多少时间
时间复杂度的局限性:
哪怕都是常数阶的函数,运行时间可能都天差地别,所以这里时间复杂度只是用来说明增长的一个趋势而已。
时间复杂度的简单计算:
1.忽略所有常数项,+1,-1在最后不影响
2.忽略所有系数
3.在循环嵌套的时候使用乘法,直接计算内外层操作之积
常见的时间复杂度
效率从高到低分别为
-
O(1) — 常数复杂度
-
O(log n) — 对数复杂度
-
O(n) — 线性复杂度
-
O(n log n) — 对数线性复杂度
-
O(nᵏ) — 多项式复杂度
-
O(kⁿ) — 指数复杂度
-
O(n!) — 阶乘复杂度
常数复杂度:
不以输入的数改变的复杂度
eg:
function constant(n) {
let count = 0;
const size = 100000;
for (let i = 0; i < size; i++) count++;
return count;
}
线性复杂度:
相对于输入数据的大小n以线性级别增长
eg:
/* 线性阶 */
function linear(n) {
let count = 0;
for (let i = 0; i < n; i++) count++;
return count;
}