如何优雅的使用javascript递归画一棵结构树

递归和尾递归

简单的说,递归就是函数自己调用自己,它做为一种算法在程序设计语言中广泛应用。其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

但是作为一个合格的程序员,我们也因该知道,递归算法相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出等。

这个时候,我们就需要用到尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

举个例子,我们来实现一下阶乘,如果用普通的递归,实现将是这样的:

1function factorial(n) { 2 if (n === 1) return 1; 3 return n * factorial(n - 1); 4} 5 6factorial(5) // 120

最多需要保存n个调用栈,复杂度 O(n),如果我们使用尾递归:

1function factorial(n, total = 1) { 2 if (n === 1) return total; 3 return factorial(n - 1, n * total); 4} 5 6factorial(5) // 120

此时只需要保存一个调用栈,复杂度 O(1) 。通过这个案例,你是否已经慢慢理解其精髓了呢?接下来我将介绍几个常用的递归应用的案例,并在其后实现本文标题剖出的树的实现。

递归的常用应用案例

1. 数组求和

对于已知数组arr,求arr各项之和。

1function sumArray(arr, total) { 2 if(arr.length === 1) { 3 return total 4 } 5 return sum(arr, total + arr.pop()) 6} 7 8let arr = [1,2,3,4]; 9sumArray(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*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用。接下来我们用js实现一个求第n个斐波那契数的方法:

1// 斐波那契数列 2function factorial1 (n) { 3 if(n <= 2){ 4 return 1 5 } 6 return factorial1(n-1) + factorial1(n-2) 7} 8 9// 尾递归优化后 10function factorial2 (n, start = 1, total = 1) { 11 if(n <= 2){ 12 return total 13 } 14 return factorial2 (n -1, total, total + start) 15}

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

3. 阶乘

阶乘在上文以提到过,如想回顾,请向上翻阅。

4. 省市级联多级联动

省市级联多级联动的方法本质是生成结构化的数据结构,在element或antd中都有对应的实现,这里就不做过多介绍了。

5. 深拷贝

深拷贝的例子大家也已经司空见惯了,这里只给出一个简单的实现思路:

1function clone(target) { 2 if (typeof target === 'object') { 3 let cloneTarget = Array.isArray(target) ? [] : {}; 4 for (const key in target) { 5 cloneTarget[key] = clone(target[key]); 6 } 7 return cloneTarget; 8 } else { 9 return target; 10 } 11};

6. 爬梯问题

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

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

7. 对象数据格式化

这道题是本人曾经面试阿里的一道笔试题,问题是如果服务器返回了嵌套的对象,对象键名大小写不确定,如果统一让键名小写。

1let obj = { 2 a: '1', 3 b: { 4 c: '2', 5 D: { 6 E: '3' 7 } 8 } 9} 10转化为如下: 11let obj = { 12 a: '1', 13 b: { 14 c: '2', 15 d: { 16 e: '3' 17 } 18 } 19} 20 21// 代码实现 22function keysLower(obj) { 23 let reg = new RegExp("([A-Z]+)", "g"); 24 for (let key in obj) { 25 if (obj.hasOwnProperty(key)) { 26 let temp = obj[key]; 27 if (reg.test(key.toString())) { 28 // 将修改后的属性名重新赋值给temp,并在对象obj内添加一个转换后的属性 29 temp = obj[key.replace(reg, function (result) { 30 return result.toLowerCase() 31 })] = obj[key]; 32 // 将之前大写的键属性删除 33 delete obj[key]; 34 } 35 // 如果属性是对象或者数组,重新执行函数 36 if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') { 37 keysLower(temp); 38 } 39 } 40 } 41 return obj; 42};

具体过程和思路在代码中已经写出了注释,感兴趣可以自己研究一下。

8. 遍历目录/删除目录

我们这里使用node来实现删除一个目录,用现有的node API确实有删除目录的功能,但是目录下如果有文件或者子目录,fs.rmdir && fs.rmdirSync 是不能将其删除的,所以要先删除目录下的文件,最后再删除文件夹。

1function deleteFolder(path) { 2 var files = []; 3 if(fs.existsSync(path)) { // 如果目录存在 4 files = fs.readdirSync(path); 5 files.forEach(function(file,index){ 6 var curPath = path + "/" + file; 7 if(fs.statSync(curPath).isDirectory()) { // 如果是目录,则递归 8 deleteFolder(curPath); 9 } else { // 删除文件 10 fs.unlinkSync(curPath); 11 } 12 }); 13 fs.rmdirSync(path); 14 } 15}

9. 绘制分形图形

通过递归,我们可以在图形学上有更大的自由度,但是请记住,并不是最好的选择。

我们可以借助一些工具和递归的思想,实现如上的分形图案。

10. 扁平化数组Flat

数组拍平实际上就是把一个嵌套的数组,展开成一个数组,如下案例:

1let a = [1,2,3, [1,2,3, [1,2,3]]] 2// 变成 3let a = [1,2,3,1,2,3,1,2,3] 4// 具体实现 5function flat(arr = [], result = []) { 6 arr.forEach(v => { 7 if(Array.isArray(v)) { 8 result = result.concat(flat(v, [])) 9 }else { 10 result.push(v) 11 } 12 }) 13 return result 14} 15 16flat(a)

当然这只是笔者实现的一种方式,更多实现方式等着你去探索。

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

通过上面的介绍,我想大家对递归及其应用已经有一个基本的概念,接下来我将一步步的带大家用递归画一棵结构树。 效果图:

该图形是根据目录结构生成的目录树图,在很多应用场景中被广泛使用,接下来我们就来看看他的实现过程吧:

1const fs = require('fs') 2const path = require('path') 3// 遍历目录/生成目录树 4function treeFolder(path, flag = '|_') { 5 var files = []; 6 7 if(fs.existsSync(path)) { 8 files = fs.readdirSync(path); 9 files.forEach(function(file,index){ 10 var curPath = path + "/" + file; 11 if(fs.statSync(curPath).isDirectory()) { // recurse 12 // obj[file] = treeFolder(curPath, {}); 13 console.log(flag, file) 14 treeFolder(curPath, ' ' + flag) 15 } else { 16 // obj['--'] = file 17 console.log(flag, file) 18 } 19 }) 20 // return obj 21 } 22} 23 24treeFolder(path.resolve(__dirname, './test'))

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

我们通过短短10几行代码就实现了一个生成结构树的小应用,是不是感觉递归有点意思呢?在这个函数中,第一个参数是目录的绝对路径,第二个是标示符,标示符决定我们生成的树枝的样式,我们可以自定义不同的样式。

欢迎大家相互学习交流,一起探索前端的边界。

更多推荐

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

浅谈JS中的递归

一、递归递归(英语:Recursion)在数学与计算机科学中,是指在函数的定义中使用函数自身的方法在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满