1. 迭代
1. for 循环
for 循环是最常用的迭代方式,它允许你通过设定初始值、循环条件和每次迭代后要执行的操作来重复执行一段代码。
// 例子:打印 0 到 4
for (let i = 0; i < 5; i++) {
console.log(i);
}
初始值:let i = 0,定义一个初始值。
循环条件:i < 5,只要条件为 true 就继续循环。
每次迭代后操作:i++,每次循环后 i 加 1。
2. while 循环
while 循环在每次迭代前检查条件是否为 true。如果条件为 true,则执行循环体中的代码。
// 例子:打印 0 到 4
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
初始值:let i = 0,定义一个初始值。
循环条件:i < 5,检查条件。
每次迭代后操作:i++,每次循环后 i 加 1。
while循环比for循环更加灵活,能处理更多复杂的迭代。
3. 嵌套循环
嵌套循环是指在一个循环内部再放置一个循环。常用于处理二维数据结构,比如矩阵。
// 例子:打印 2D 矩阵
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
console.log(`i: ${i}, j: ${j}`);
}
}
- 外层循环:
for (let i = 0; i < 3; i++) - 内层循环:
for (let j = 0; j < 3; j++)
当你不停的嵌套循环的时候,几维的循环其实能模拟几维的空间,一层for循环可以模拟出一条线上的轨迹,两层for循环能模拟出一个平面上的动态,三层for循环能模拟出三维空间中的人的运动,四层for循环就是加上时间这个轴以此类推... 但是低维的生物是无法想象更高维的空间是如何运行的。
2. 递归(分为递和归两个部分)
1. 调用栈
递归函数会在调用栈中不断地推入新的帧,每个帧保存了函数的状态。当递归条件结束时,调用栈会被逐一弹出,函数逐层返回。
function factorial(n) {
if (n === 0) return 1;//终止条件
return n * factorial(n - 1);//递归调用和返回结果写一起了
}
递归分为三个部分
终止条件
递归调用
返回结果
- 终止条件:用于决定什么时候由“递”转“归”。
- 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。
- 返回结果:对应“归”,将当前递归层级的结果返回至上一层。
-
调用栈示意:
factorial(3)调用factorial(2)factorial(2)调用factorial(1)factorial(1)调用factorial(0)factorial(0)返回 1,逐层返回。
2. 尾递归
尾递归是递归的一种特殊形式,在尾递归中,递归调用是函数执行的最后一步。尾递归的特点使得它可以被编译器或解释器优化,减少递归调用的开销,提高性能。
尾递归的定义
尾递归是指递归函数在返回结果之前,最后一步操作是直接调用自身,而不是在返回之前执行其他操作。简单来说,尾递归的形式是:
function tailRecursiveFunction(params, ...args) {
// 一些处理
return tailRecursiveFunction(...newArgs); // 递归调用
}
在尾递归中,递归调用的结果会直接返回,而不会在返回之前进行任何额外计算。
为什么尾递归更高效
正常的递归调用每次都会创建新的调用栈帧,导致空间复杂度较高,可能会导致栈溢出。而尾递归由于递归调用是函数的最后一步,可以通过尾递归优化(Tail Call Optimization,TCO)来避免创建新的栈帧,从而减少空间复杂度。
尾递归优化
尾递归优化(TCO)是编译器或解释器对尾递归的优化技术。它将尾递归调用转换为循环,从而避免了递归调用带来的额外栈帧开销。
尾递归示例
下面是一个计算阶乘的尾递归示例:
function factorial(n, acc = 1) {
if (n === 0) return acc;
return factorial(n - 1, n * acc); // 尾递归
}
- 参数
n:当前计算的值。 - 参数
acc:累积结果,用于存储当前的阶乘值。
在这个例子中,递归调用 factorial(n - 1, n * acc) 是函数的最后一步操作,所以它是尾递归的。
尾递归与普通递归的对比
-
普通递归
function factorial(n) { if (n === 0) return 1; return n * factorial(n - 1); // 非尾递归 }在这个例子中,
factorial(n - 1)的结果需要乘以n,因此它不是尾递归。函数需要保存状态以便在递归调用返回后继续计算。 -
尾递归
function factorial(n, acc = 1) { if (n === 0) return acc; return factorial(n - 1, n * acc); // 尾递归 }在这个例子中,递归调用
factorial(n - 1, n * acc)是函数的最后一步,返回值不会被进一步操作,因此它是尾递归。 假设你正在倒计时从10到1,每秒钟减去1,直到到达0。这个过程可以用递归来描述:
- 普通递归:每秒钟的倒计时需要等前一秒的倒计时结束后才能继续进行。
- 尾递归:每秒钟的倒计时在进行下一秒的倒计时前不需要做额外的计算,直接进入下一秒的倒计时。
例子
普通递归的倒计时
在普通递归中,每个倒计时的结果需要等到前一个倒计时完成后才能继续进行:
function countdown(n) {
if (n <= 0) {
console.log("Time's up!");
} else {
console.log(n);
countdown(n - 1); // 递归调用
}
}
解释:
- 每秒钟的倒计时
countdown(n - 1)需要等待前一个倒计时完成后才能开始。 - 每个递归调用都需要保存当前的倒计时状态,直到递归到达0。
尾递归的倒计时
在尾递归中,每个倒计时调用的结果直接传递到下一个倒计时调用,不需要在返回之前进行额外的处理:
function countdown(n) {
function helper(n) {
if (n <= 0) {
console.log("Time's up!");
return;
}
console.log(n);
helper(n - 1); // 尾递归调用
}
helper(n);
}
解释:
helper(n - 1)是函数的最后一步操作,因此这是一个尾递归调用。- 在尾递归中,
helper(n - 1)不需要保留当前调用的状态,直接进行下一次调用。这个过程可以被优化为循环,从而避免额外的栈开销。
实际的尾递归优化
在尾递归中,编译器或解释器可以优化这个过程,将递归调用转换为循环。这意味着每秒钟的倒计时不需要创建新的调用栈帧,只需更新当前的状态并进行下一次倒计时。这减少了空间开销,使得倒计时过程更高效。
注意事项
- 语言支持:虽然 JavaScript 的规范(ECMAScript 6)支持尾递归优化,但实际的 JavaScript 引擎(如 V8)可能不一定实现尾递归优化。因此,在某些环境中,尾递归优化可能不会生效。
- 性能考虑:即使语言支持尾递归优化,在实际应用中还是要注意递归的深度和性能。对于深度很大的递归,尾递归优化可能会有帮助,但还是要谨慎使用。
3. 递归树
递归树是一个树形结构,用来表示递归函数的调用关系。每个节点表示一个函数调用,每个子节点表示递归调用。
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
-
递归树示意:
fibonacci(4)调用fibonacci(3)和fibonacci(2)fibonacci(3)调用fibonacci(2)和fibonacci(1)- 依此类推,形成树状结构。
3. 两者对比
-
性能:迭代通常比递归更高效,因为递归需要处理调用栈,而迭代则没有这些额外的开销。递归在深度较大时可能导致栈溢出。
-
代码简洁性:递归可以使代码更简洁,尤其是处理树状结构和分治算法时,递归代码通常比迭代代码更易读。
-
使用场景:
- 迭代:适用于大多数情况,尤其是需要处理大数据集时。
- 递归:适用于处理分治问题、树形结构和动态规划等问题。
代码
//for循环迭代
function forLoop(n){
let res = 0
for(let i=0;i<n;i++){
res += i
}
return res
}
//while循环迭代
function whileLoop(n){
let res = 0;
let i = 0;
while(i < n){
res += i;
i++
}
return res
}
//作为while循环迭代 它比for循环的更灵活
function whileLoopII(n){
let res = 0;
let i = 0;
while(i <= n){
res += i;
i++;
i *= 2;//每两次更新一次
}
return res;
}
//递归调用
function recur(n){
if(n === 1)return 1;//终止条件
const res = recur(n - 1);//递归调用
return n + res//返回结果
}
//尾递归
function tailRecur(n,res){
if(n === 0) return res;
return tailRecur(n - 1,res)//把计算的过程丢进递中,归只需要层层返回
}
/**
* 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。
* 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。
*/
// 斐波那契求和 经典递归题目
function fib(n){
if(n === 1 || n === 2) return n - 1;
const res = fib(n - 1) + fib(n - 2);
return res
}
//用栈去模拟递归这个过程
function forLoopRecur(n){
const stack= [];
let res = 0;
for(let i = n;i > 0;i--){
stack.push(i)//递的过程
}
while(stack.length){
res += stack.pop();//归的过程
}
return res;
}