JavaScript递归与尾递归详解及结构树绘制实战

作者:青柠时间:2026-10-23 11:25:25浏览:364

本文深入解析JavaScript中的递归与尾递归概念,通过阶乘、斐波那契数列等经典案例对比普通递归与尾递归在栈空间复杂度上的差异(O(n)与O(1))。文章列举了数组求和、深拷贝、扁平化数组等常见应用场景,并重点演示如何利用Node.js文件系统API结合递归思想,实现自定义样式的目录结构树可视化绘制,帮助开发者掌握递归在复杂数据结构处理中的实际应用。

递归与尾递归基础

递归与尾递归基础

递归与尾递归

简而言之,递归是指函数在执行过程中调用自身的一种算法机制,广泛应用于各类程序设计语言中。其核心逻辑是将一个庞大复杂的问题,层层分解转化为与原问题相似但规模更小的子问题来求解。通常,递归的实现需要包含边界条件、递归前进阶段以及递归返回阶段三个要素。当边界条件未被满足时,程序进入递归前进阶段;一旦边界条件满足,则开始递归返回阶段。

然而,对于追求性能的开发者而言,必须认识到递归算法的运行效率通常低于普通循环等常规算法。因此,除非缺乏更优算法或特定场景下递归更具优势,否则应尽量避免使用递归。在递归调用过程中,系统会为每一层的返回点、局部变量等分配栈空间进行存储。若递归层级过深,极易导致栈溢出错误。

为解决上述问题,尾递归应运而生。尾递归要求函数中所有的递归调用语句均位于函数的末尾。由于尾递归仅保留一个调用记录,因此能够彻底避免“栈溢出”错误的发生。

以阶乘计算为例,若采用普通递归方式,实现代码如下:


function factorial(n) {
 if (n === 1) return 1;
 return n * factorial(n - 1);
}

factorial(5) // 120

该方式最多需保存n个调用栈,时间复杂度为O(n)。若改用尾递归实现:


function factorial(n, total) {
 if (n === 1) return total;
 return factorial(n - 1, n * total);
}

factorial(5) // 120

此时仅需保存一个调用栈,时间复杂度降至O(1)。通过此案例,相信您对递归的精髓已有所体会。接下来,本文将介绍几个常用的递归应用案例,并在此基础上实现标题所述的树形结构绘制功能。

递归的常用应用案例

递归的常用应用案例

1. 数组求和

针对已知数组arr,计算其所有元素之和。


function sumArray(arr, total) {
 if(arr.length === 1) {
  return total
 }
 return sum(arr, total + arr.pop())
}

let arr = [1,2,3,4];
sumArray(arr, arr[1]) // 10

该方法向函数传递数组参数及初始值(即数组的第一项),通过迭代方式实现数组求和。

2. 斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,数列为:1、1、2、3、5、8、13、21、34……在数学上,该数列通过递推方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)。在现代物理、准晶体结构、化学等领域,斐波那契数列均有直接应用。接下来使用JavaScript实现求第n个斐波那契数的方法:


// 斐波那契数列
function factorial1 (n) {
 if(n <= 2){
  return 1
 }
 return factorial1(n-1) + factorial1(n-2)
}

// 尾递归优化后
function factorial2 (n, start = 1, total = 1) {
 if(n <= 2){
  return total
 }
 return factorial2 (n -1, total, total + start)
}

由尾递归优化后的函数可知,每次调用自身时,都会将更新后的初始值和最终结果传递进去,通过回溯求得最终结果。

3. 阶乘

阶乘在上文已提及,如需回顾请向上翻阅。

4. 省市级联多级联动

省市级联多级联动的本质是生成结构化的数据。在element或antd等UI库中均有对应实现,此处不再赘述。

5. 深拷贝

深拷贝示例较为常见,此处仅给出一个简单的实现思路:


function clone(target) {
 if (typeof target === 'object') {
  let cloneTarget = Array.isArray(target) ? [] : {};
  for (const key in target) {
   cloneTarget[key] = clone(target[key]);
  }
  return cloneTarget;
 } else {
  return target;
 }
};

6. 爬梯问题

假设有n个台阶,每次只能走一个或两个台阶,问走完这些台阶共有多少种走法。


n =1; result = 1 --> 1
n =2; result = 2 --> 11 2
n =3; result = 3 --> 111 12 21
...
如果第一步走1个台阶,由以上规律可以发现剩下的台阶有n-1种走法;
如果第一步走2个台阶,由以上规律可以发现剩下的台阶有n-2种走法;
则一共有fn(n-1) + fn(n-2) 种走法
function steps(n) {
 if(n <= 1) {
  return 1
 }
 return steps(n-1) + steps(n-2)
}

7. 对象数据格式化

此题为笔者面试阿里时的一道笔试题,问题场景为:若服务器返回嵌套对象,且对象键名大小写不确定,如何统一将键名转换为小写。


let obj = {
 a: '1',
 b: {
  c: '2',
  D: {
   E: '3'
  }
 }
}
转化为如下:
let obj = {
 a: '1',
 b: {
  c: '2',
  d: {
   e: '3'
  }
 }
}

// 代码实现
function keysLower(obj) {
 let reg = new RegExp("([A-Z]+)", "g");
 for (let key in obj) {
  if (obj.hasOwnProperty(key)) {
   let temp = obj[key];
   if (reg.test(key.toString())) {
    // 将修改后的属性名重新赋值给temp,并在对象obj内添加一个转换后的属性
    temp = obj[key.replace(reg, function (result) {
     return result.toLowerCase()
    })] = obj[key];
    // 将之前大写的键属性删除
    delete obj[key];
   }
   // 如果属性是对象或者数组,重新执行函数
   if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
    keysLower(temp);
   }
  }
 }
 return obj;
};

具体过程与思路已在代码注释中说明,感兴趣者可自行研究。

8. 遍历目录/删除目录

此处使用Node.js实现目录删除功能。虽然Node现有的API提供删除目录的功能,但若目录下包含文件或子目录,fs.rmdir和fs.rmdirSync无法直接删除。因此,需先删除目录下的文件,最后再删除文件夹。


function deleteFolder(path) {
 var files = [];
 if(fs.existsSync(path)) { // 如果目录存在
  files = fs.readdirSync(path);
  files.forEach(function(file,index){
   var curPath = path + "/" + file;
   if(fs.statSync(curPath).isDirectory()) { // 如果是目录,则递归
    deleteFolder(curPath);
   } else { // 删除文件
    fs.unlinkSync(curPath);
   }
  });
  fs.rmdirSync(path);
 }
}

9. 绘制分形图形

借助递归,我们在图形学领域可获得更大的自由度,但请记住,这并非最佳选择。

递归与尾递归基础

递归与尾递归基础

我们可以借助相关工具与递归思想,实现上述分形图案。

10. 扁平化数组Flat

数组拍平即把嵌套数组展开为单层数组,案例如下:


let a = [1,2,3, [1,2,3, [1,2,3]]]
// 变成
let a = [1,2,3,1,2,3,1,2,3]
// 具体实现
function flat(arr = [], result = []) {
  arr.forEach(v => {
    if(Array.isArray(v)) {
      result = result.concat(flat(v, []))
    }else {
      result.push(v)
    }
  })
  return result
}

flat(a)

这仅是笔者实现的一种方式,更多实现方式等待您去探索。

用递归绘制结构树实战

用递归画一棵自定义风格的结构树

通过上述介绍,相信大家对递归及其应用已具备基本概念。接下来,本文将逐步带领大家使用递归绘制一棵结构树。

效果图:

递归与尾递归基础

递归与尾递归基础

该图形根据目录结构生成目录树图,在诸多应用场景中被广泛使用。接下来我们来看其实现过程:


const fs = require('fs')
const path = require('path')
// 遍历目录/生成目录树
function treeFolder(path, flag = '|_') {
  var files = [];
  
  if(fs.existsSync(path)) {
    files = fs.readdirSync(path);
    files.forEach(function(file,index){
      var curPath = path + "/" + file;
      if(fs.statSync(curPath).isDirectory()) { // recurse
        // obj[file] = treeFolder(curPath, {});
        console.log(flag, file)
        treeFolder(curPath, '  ' + flag)
      } else {
        // obj['--'] = file
        console.log(flag, file)
      }
    })
    // return obj
  }
}

treeFolder(path.resolve(__dirname, './test'))

test为我们创建的测试目录,结构如下:

递归与尾递归基础

通过短短十几行代码,我们便实现了一个生成结构树的小应用,是否让您感受到递归的趣味?在此函数中,第一个参数为目录的绝对路径,第二个参数为标示符,标示符决定了生成树枝的样式,我们可以自定义不同的样式。

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或工作具有一定的参考借鉴价值,感谢大家对本站的支持。

作者最新文章