1简介

递归在前端开发中应用还是非常广泛的,首先DOM就是树状结构,而这种结构使用递归去遍历是非常合适的。然后就是对象和数组的深复制很多库也是使用递归实现的例如jquery中的extend方法。甚至在画图时也会经常利用递归实现一些图形,犀牛书中就有相关的例子。
由此可见递归是一个非常有用的工具,本文余下的部分将按照传统的方式讲述递归,首先由分治思想引出递归,因为递归是实现分治的最为直观的算法,然后将通过几个经典的例子如斐波那契数列、阶乘、全排和n皇后来一步步深入了解递归。最终我们将回归前端,使用递归解决一些问题,如实现深复制、遍历dom树等。

2分治简介

这里主要引用《算法笔记》里的定义(其实这个系列算是这本书的读书笔记吧。。)

分治(divide and conquer)全称分而治之,即分治法将原问题划分为若干个规模较小儿结构与原问题相同或者相似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到原问题的解

步骤:

注意:

  • 分解的子问题应该相互独立、没有交叉。不然应该选择其它解决方法。
  • 分治是一种思想,既可以使用递归也可以使用非递归手段实现

3递归

递归就是反复调用自身函数,但每次调用时会吧范围缩小,直到范围缩小到可以直接得到边界数据的结果,然后在返回的路上求出对应的解。
由分治和递归的定义就可以看出,递归是实现分治的最直观的算法。
递归式的重要概念

  • 递归边界:没有递归边界会导致无限递归,然后就会报错 Maximum call stack size exceeded
  • 递归式

下面通过几个例子来深入了解一下递归:

3.1使用递归求解n的阶乘

n!的计算公式:
$$ n!=1*2*3*.....*n $$
n!的递归式:
$$ F(n)=F(n-1)*n $$
有了递归式就可以很方便的写出递归函数:

 function F(n=3){if(n==0) return 1;else return F(n-1)*n;}console.log(F())

为了方便理解,这里选取了3!数量较少递归过程好画,同时给出了图来描述这个递归过程:

同时我们结合实际执行时的gif来动态的了解一下:

这里着重注意一下最右侧的Call Stack,俗称调用堆栈,这个堆栈有个特点就是后进先出(LILO),调用堆栈存放的是函数。在递归过程中,未完成计算的函数将会挂起(压入调用堆栈),不然递归结束的时候没办法进行回溯。图一里的四层就对应Call Stack里放的四个F,然后我们再观察一下那个变量n,每次单步执行的时候n都在变化,步骤1时n=2...步骤3时n=0,当n=0的时候达到了当前的递归边界,结果开始返回,这时我们观察Call Stack下方的Scope:

那个红框标识出来的变量Return value,这时就开始进入回溯阶段,就是步骤4至步骤7,Call Stack开始弹出之前保存的递归函数,每次都返回一个计算好的值,最后合并成最终结果。

3.2求Fibonacci数列的第n项

Fibonacci数列是满足F(0)=1,F(1)=1,F(n)=F(n-1)+F(n-2)(n>=2)的数列。因此可以得出

  • 递归边界:为F(0)=1,F(1)=1
  • 递归式:为F(n)=F(n-1)+F(n-2)(n>=2)

从上面可以看出和3.1中的阶乘很像,我们参照着就可很快写出:

 function Fib(n=4){if(n==0||n==1) return 1;else return Fib(n-1)+Fib(n-2);}console.log(Fib())

现在画一下它的递归树

黑线是递归函数入栈,黄线是出栈,步骤1~17表示顺序。从这种图中我们可以小窥一下画同时调用多个自己的递归树的方法,因为代码都是顺序执行的,所以递归也要按顺序入栈出栈,遇到这种情况就先指着优先级最高的那个一直递归到递归边界,然后在返回的过程中按顺序递归剩下的即可。

3.3全排

引用百度百科的解释:

从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素
中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。
公式:全排列数f(n)=n!(定义0!=1),如1,2,3三个元素的全排列为:
1,2,3
1,3,2
2,1,3
2,3,1
3,1,2
3,2,1
共3*2*1=6种。

本例使用的是字典序(从小到大顺序排序)实现全排,而上段中的1,2,3的全排就是按照字典序排列的。
从递归角度分析,输出n的全排就可以分解为输出以1开头的全排、2开头的全排.....输出以n开头的全排。
定义数组save用于存放当前的排列,设定一个flag数组当flag[x]为true时表示整数x已经存在save中,当递归结束时需要还原。index表示排列位置。递归边界就是index==n+1,表示1~n位置都已经填好。

function Permutation(n) {let flag = new Array(n + 1).fill(false);let save = new Array(n + 1).fill(0);let index = 1;return (function innerPer(index) {if (index == n + 1) {for (let i = 1; i <= n; i++) {console.log(save[i]);}console.log('\n')return;}for (let x = 1; x <= n; x++) {if (!flag[x]) {flag[x] = true;//每向下递归一次,进入if的次数少一save[index] = x;//index在每层循环过程中是不变的innerPer(index + 1);//仅在递归时发生变化flag[x] = false;//递归结束还原状态}}})(index)}Permutation(3);

这个是正序输出,大家也可以实现一下逆序输出,或者画一画递归树加深一下印象。

3.4n皇后问题

n皇后问题很经典,该问题指的是在n*n的棋盘上放置n个皇后,使得这n个皇后不再同一行、同一列、同一对角线上,求合法的方案数量,下图就是n=5的一个合法方案。

因为每一行每一列只能放置一个皇后,所以这个问题就转换为n的排列问题,例如上图按照行号就是24513。这样我们把3.3中的代码稍作修改就能解决现在的问题。(所以说数学好的人就是nb,唉。。)
我们在全排的代码上加上判断每两个皇后是否在对角线上的代码即可。

 function Queen(n) {let flag = new Array(n + 1).fill(false);let save = new Array(n + 1).fill(0);let index = 1;let cnt = 0;return (function innerQ(index) {if (index == n + 1) {let judge = true;for (let i = 1; i <= n; i++) {for (let j = i + 1; j <= n; j++) {if (Math.abs(i - j) == Math.abs(save[i] - save[j])) {judge = false;}}}if (judge) {console.log(save, ++cnt);console.log('\n');}return;}for (let x = 1; x <= n; x++) {if (!flag[x]) {flag[x] = true;save[index] = x;innerQ(index + 1);flag[x] = false;}}})(index)}Queen(10)

这里有个问题就是判断对角线冲突,这里采用两个一维数组解决了二维数组的问题,外层的for循环i,j为列号,一位数组里存的值为行号,通过观察可以知道,如果两个棋盘格子处在对角的位置,那么他们的横坐标之差等于他们的纵坐标之差的绝对值(斜字部分引用自《运用全排列的方法解决八皇后问题》)。
上面这种枚举所有情况然后挨个判断合法性的手段被称之为暴力法,通过观察可以发现只要发现第一次不合法那整个递归就可以返回,无须将递归进行到底再去判断,直接返回上层即可。这就引出了回溯法
回溯法就是在达到递归边界前的某层,由于一些事实导致已经不需要前往任何一个子问题递归,就可以直接返回上一层。
下面举出回溯法的代码,请与上方进行对比。

    function ReQueen(n) {let flag = new Array(n + 1).fill(false);let save = new Array(n + 1).fill(0);let index = 1;let cnt = 0;return (function innerQ(index) {if (index == n + 1) {console.log(save, ++cnt);return;}for (let x = 1; x <= n; x++) {if (!flag[x]) {let judge = true;for (let pre = 1; pre < index; pre++) {//再次强调index代表位置if (Math.abs(pre - index) == Math.abs(save[pre] - x)) {judge = false;break;}}if (judge) {flag[x] = true;save[index] = x;innerQ(index + 1);flag[x] = false;}}}})(index)}ReQueen(8)

3.5递归在前端上的应用

递归在前端中应用还是非常常见的。主要原因是前端数据量一般不大,而且从es6开始支持尾递归的优化。同时DOM也是树状结构,在遍历树这种数据结构的时候也常用递归。所以相对与前面讲的哈希,递归是要重点掌握的。

3.5.1遍历DOM获取文本

在使用textContent和innerText时有时并不能满足我们的要求,这时我们就要手工的收集文本来得到想要的结果。

function GetText(elem){var text="";var length = elem.childNodes.length;for(let i=0;i<length;i++){var cur = elem.childNodes[i];if(cur.nodeType===3)text+=cur.nodeValue;//遇到文本节点就保存else if(cur.nodeType===1)text+=GetText(cur);//遇到元素节点就进行递归}return text;}

3.5.2使用递归实现属性查询

原生的js并不提供通过标签属性去获取标签,在这里我们通过递归遍历dom树去实现这个功能。
首先我们要实现递归遍历dom树

function WalkDom(node, func) {func(node);//首先把传入的节点,传给func进行操作node = node.firstChild;//提取节点的第一个子节点while (node) {//递归的终止条件就是节点不存在WalkDom(node, func);//递归node = node.nextSibling;//获取兄弟节点}}

这里的递归终止条件就是节点不存在。
现在实现通过属性获取标签,这里主要用到的是原生方法getAttribute

function getElementByAttr(attr, value ,node=document.body) {//两个可选参数,属性对应的值value,指定范围节点nodelet res = [];WalkDom(node, function (node) {let actual = node.nodeType == 1 && node.getAttribute(attr);//这里主要用到&&运算的特点,第一个值为真则返回第二个值,第一个值为假则返回第一个值,第二个表达式将不进行计算if (typeof actual == 'string' && (actual === value || typeof value != 'string')) {res.push(node);}})return res;}

3.5.3使用递归实现深复制

因为js存在引用型(对象和数组都是引用型),引用型的有一个特点就是复制上分为浅复制和深复制。浅复制和深复制的区别如下:

var a=[1,2,3],b=a,c=[];//把a直接赋值给b的这种情况就是浅复制b.pop();//修改b的同时a也被改变了console.log(a);//输出[1,2]a.forEach(function(elem,index){c[index]=elem})c.push(4)//修改c,不影响aconsole.log(a,c)

js的引用型在赋值的时候,赋给变量的可能是地址,而非数据的副本。因为在使用数组和对象的时候经常嵌套数组或对象所以我们要通过递归的方式来实现数据的备份。

function DeepClone(obj){if(!obj||typeof obj!=='object')return obj;var tmp =new obj.constructor();for(let key in obj){tmp[key]=DeepClone(obj[key]);}return tmp;}var cc=[[1,2,4],{'dd':123}],bb=DeepClone(cc)console.log(bb.pop(),cc)

这是一个简陋的实现,看那个类型判断就可以看出来很不严谨,不过大部分库都提供有完备的深复制的方法。例如jq的extend就可以实现深复制。

4小结

和js相关的例子还是太少太理论化,未来会增加一些更接地气的。
树和图结构也会用到递归,所以递归这一节请深入研究一下。

参考

《算法笔记》
《javascript函数式编程》
《javascript语言精粹》
《javascript忍者秘籍》

js算法入门(3)--递归相关推荐

  1. js算法入门(2)--哈希表

    1.简介 哈希表(hash table)又被称为散列表,可能是翻译的问题好多书上一会儿称散列一会儿称哈希,更有甚者煞有介事的对此进行区分.经过简单的搜索(wiki链接)发现这两个词是一回事.由此可见学 ...

  2. 算法入门篇九 暴力递归

    牛客网 左程云老师的算法入门课 暴力递归 原则  汉诺塔问题 问题 打印n层汉诺塔从左边移动到最右边的过程 思想 一共六个过程,左到右.左到中,中到左,中到右,右到左,右到中,互相嵌套使用 左到右 将 ...

  3. LeetCode《算法入门》刷题笔记(31 题全)

    LeetCode<算法入门>刷题笔记(31 题全) 二分查找 1. 二分查找 _解法1:二分搜索(迭代) 解法2:二分搜索(递归) 2. 第一个错误的版本 _解法1:二分 3. 搜索插入位 ...

  4. js算法初窥03(搜索及去重算法)

    前面我们了解了一些常用的排序算法,那么这篇文章我们来看看搜索算法的一些简单实现,我们先来介绍一个我们在实际工作中一定用到过的搜索算法--顺序搜索. 1.顺序搜索 其实顺序搜索十分简单,我们还是以第一篇 ...

  5. 算法入门篇七 前缀树

    牛客网 左程云老师的算法入门课 找二叉树的节点的后继节点 原则 如果节点有右子树,那么后继节点就是右子树的最左边的第一个节点 如果节点没有右子树,如果节点是父节点的右孩子,就继续往上找,直到找到一个父 ...

  6. 算法入门篇六 二叉树

    牛客网 算法入门篇 左程云老师 个人复习,如果侵全,设为私密 二叉树遍历(递归) 先序遍历(中,左,右) 中序遍历(左,中,右) 后序遍历(左,右,中) 如上图所示结构,二叉树的遍历本质上都是递归序, ...

  7. js console 输出到文件_Node.js核心入门

    正文 核心模块是Node.js的心脏,主要是有一些精简高效的库组成(这方面和Python有很大的相似之处),为Node.js提供了基础的API.主要内容包括: Node.js核心入门(一) 全局对象 ...

  8. LeetCode算法入门- Generate Parentheses -day16

    LeetCode算法入门- Generate Parentheses -day16 题目描述 Given n pairs of parentheses, write a function to gen ...

  9. LeetCode算法入门- Merge Two Sorted Lists -day15

    LeetCode算法入门- Merge Two Sorted Lists -day15 题目描述: Merge two sorted linked lists and return it as a n ...

最新文章

  1. 稳压源GWINSTEKGPD3303系列控制软件
  2. Struts2学习第二天——动态方法调用
  3. mysql jdbc路径,mysql转存数据库后,如何修改jdbc:mysql的路径
  4. jvm性能调优 - 03垃圾回收机制
  5. TABLES ABOUT CRM MARTETING
  6. Spark入门(十五)之分组求最小值
  7. 【C++grammar】格式化输出与I/O流函数
  8. android 信号强度变化,Android监听WIFI网络的变化并且获得当前信号强度
  9. matlab算法应用论文(带代码)_左手论文 右手代码 深入理解网红算法XGBoost
  10. 使用Filter实现用户自动登录
  11. c语言怎么加分数,用C语言编程平均分数
  12. WIN32汇编语言之通用对话框的使用
  13. sprutcam 多机器人_Sprutcam工业机器人离线编程系统
  14. Font Awesome 字体符号的使用
  15. TscanCode代码扫描工具
  16. 阿里程序员,过完年第一天就要被劝退!让人感觉现实是如此残酷!
  17. Parameter 0 of method linkDiscoverers in org.springframework.hateoas.config.HateoasConfiguration
  18. Fedora 34 Workstation安装后的配置
  19. 前端框架MVC和MVVM的理解
  20. unity 输入框弹出输入法_国产输入法那么多,我为什么选择了「不接地气」的 Gboard?...

热门文章

  1. 1.mongodb在centos上面安装
  2. Axis2 客户端调用 设置超时时间
  3. 端口扫描程序nmap使用详解
  4. 09最短小说:意见统一
  5. java ftp下载文件源码_java实现ftp文件下载的源代码
  6. 怎么用python判断数据是否已经存在于表里_数据基本操作(二)
  7. python知识点:文件读写以及其他基础知识点
  8. Python爬虫利器之Beautiful Soup的全世界最强用法 五百行文章!
  9. ctf 文件头crc错误_[CTF隐写]png中CRC检验错误的分析
  10. centos 删除crontab_centos crontab(定时任务) 使用