资讯专栏INFORMATION COLUMN

JavaScript 数据结构与算法之美 - 递归

Rocko / 1047人阅读

摘要:递归常见问题及解决方案警惕堆栈溢出可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。文章输出计划数据结构与算法之美的系列文章,坚持天左右更新一篇,暂定计划如下表。

前言

算法为王。

排序算法博大精深,前辈们用了数年甚至一辈子的心血研究出来的算法,更值得我们学习与推敲。

因为之后要讲有内容和算法,其代码的实现都要用到递归,所以,搞懂递归非常重要。

1. 定义

方法或函数调用自身的方式称为递归调用,调用称为递,返回称为归。

简单来说就是:自己调用自己

现实例子:周末你带着女朋友去电影院看电影,女朋友问你,咱们现在坐在第几排啊 ?电影院里面太黑了,看不清,没法数,现在你怎么办 ?

于是你就问前面一排的人他是第几排,你想只要在他的数字上加一,就知道自己在哪一排了。
但是,前面的人也看不清啊,所以他也问他前面的人。
就这样一排一排往前问,直到问到第一排的人,说我在第一排,然后再这样一排一排再把数字传回来。
直到你前面的人告诉你他在哪一排,于是你就知道答案了。

基本上,所有的递归问题都可以用递推公式来表示,比如:

f(n) = f(n-1) + 1; 
// 其中,f(1) = 1 

f(n) 表示你想知道自己在哪一排,f(n-1) 表示前面一排所在的排数,f(1) = 1 表示第一排的人知道自己在第一排。

有了这个递推公式,我们就可以很轻松地将它改为递归代码,如下:

function f(n) {
  if (n == 1) return 1;
  return f(n-1) + 1;
}
2. 为什么使用递归 ?递归的优缺点 ?

优点:代码的表达力很强,写起来简洁。

缺点:空间复杂度高、有堆栈溢出风险、存在重复计算、过多的函数调用会耗时较多等问题。

3. 什么样的问题可以用递归解决呢 ?

一个问题只要同时满足以下 3 个条件,就可以用递归来解决。

问题的解可以分解为几个子问题的解。何为子问题 ?就是数据规模更小的问题。

比如,前面讲的电影院的例子,你要知道,自己在哪一排的问题,可以分解为前一排的人在哪一排这样一个子问题。

问题与子问题,除了数据规模不同,求解思路完全一样

比如电影院那个例子,你求解自己在哪一排的思路,和前面一排人求解自己在哪一排的思路,是一模一样的。

存在递归终止条件

比如电影院的例子,第一排的人不需要再继续询问任何人,就知道自己在哪一排,也就是 f(1) = 1,这就是递归的终止条件。

4. 递归常见问题及解决方案

警惕堆栈溢出:可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。

警惕重复计算:通过某种数据结构来保存已经求解过的值,从而避免重复计算。

5. 如何实现递归 ?

1. 递归代码编写

写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。

2. 递归代码理解

对于递归代码,若试图想清楚整个递和归的过程,实际上是进入了一个思维误区。

那该如何理解递归代码呢 ?

如果一个问题 A 可以分解为若干个子问题 B、C、D,你可以假设子问题 B、C、D 已经解决。

而且,你只需要思考问题 A 与子问题 B、C、D 两层之间的关系即可,不需要一层层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。

屏蔽掉递归细节,这样子理解起来就简单多了。

因此,理解递归代码,就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。

6. 例子 1. 一个阶乘的例子:
function fact(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * fact(num - 1);
    }
}
fact(3) // 结果为 6

以下代码可导致出错:

var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); //出错 

由于 fact 已经不是函数了,所以出错。

使用 arguments.callee

arguments.callee 是一个指向正在执行的函数的指针,arguments.callee 返回正在被执行的对现象。
新的函数为:

function fact(num){ 
    if (num <= 1){ 
        return 1; 
    }else{ 
        return num * arguments.callee(num - 1); //此处更改了。 
    } 
} 
var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); // 结果为 24
2. 再看一个多叉树的例子

先看图

叶子结点:就是深度为 0 的结点,也就是没有孩子结点的结点,简单的说就是一个二叉树任意一个分支上的终端节点。

数据结构格式,参考如下代码:

const json = {
  name: "A",
  children: [
    {
      name: "B",
      children: [
        {
          name: "E",
        },
        {
          name: "F",
        },
        {
          name: "G",
        }
      ]
    },
    {
      name: "C",
      children: [
        {
          name: "H"
        }
      ]
    },
    {
      name: "D",
      children: [
        {
          name: "I",
        },
        {
          name: "J",
        }
      ]
    }
  ]
}

我们如何获取根节点的所有叶子节点个数呢 ?

递归代码如下:

/**
 * 获取根节点的所有 叶子节点 个数
 * @param {Object} json Object 对象
 */
function getLeafCountTree(json) {
  if(!json.children){
      return 1;
  } else {
      let leafCount = 0;
      for(let i = 0 ; i < json.children.length ; i++){
          // leafCount = leafCount + getLeafCountTree(json.children[i]);
          leafCount = leafCount + arguments.callee(json.children[i]);
      }
      return leafCount;
  }
}

递归遍历是比较常用的方法,比如:省市区遍历成树、多叉树、阶乘等。

7. 文章输出计划

JavaScript 数据结构与算法之美 的系列文章,坚持 3 - 7 天左右更新一篇,暂定计划如下表。

| 标题 | 链接 |
| :------ | :------ |
| 时间和空间复杂度 | https://github.com/biaochenxu... |
| 线性表(数组、链表、栈、队列) | https://github.com/biaochenxu... |
| 实现一个前端路由,如何实现浏览器的前进与后退 ?| https://github.com/biaochenxu... |
| 栈内存与堆内存 、浅拷贝与深拷贝 | https://github.com/biaochenxu... |
| 递归 | https://github.com/biaochenxu... |
| 非线性表(树、堆) | 精彩待续 |
| 冒泡排序 | 精彩待续 |
| 插入排序 | 精彩待续 |
| 选择排序 | 精彩待续 |
| 归并排序 | 精彩待续 |
| 快速排序 | 精彩待续 |
| 计数排序 | 精彩待续 |
| 基数排序 | 精彩待续 |
| 桶排序 | 精彩待续 |
| 希尔排序 | 精彩待续 |
| 堆排序 | 精彩待续 |
| 十大经典排序汇总 | 精彩待续 |

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。
8. 最后
文章可以转载,但须注明作者及出处,需要转载到公众号的,喊我加下白名单就行了。

参考文章:

递归:如何用三行代码找到“最终推荐人”?

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105312.html

相关文章

  • 优秀程序员都应该学习的 GitHub 上开源的数据结构算法项目

    摘要:强烈推荐上值得前端学习的数据结构与算法项目,包含图的演示过程与视频讲解。该仓库包含了多种基于的算法与数据结构,提供进一步阅读的解释和链接。数据结构和算法必知必会的个代码实现。 showImg(https://segmentfault.com/img/bVbvpYZ); 前言 算法为王。想学好前端,先练好内功,内功不行,就算招式练的再花哨,终究成不了高手;只有内功深厚者,前端之路才会走得...

    cheukyin 评论0 收藏0
  • JavaScript 数据结构算法之美 - 归并排序、快速排序、希尔排序、堆排序

    摘要:之所以把归并排序快速排序希尔排序堆排序放在一起比较,是因为它们的平均时间复杂度都为。归并排序是一种稳定的排序方法。因此,快速排序并不稳定。希尔排序思想先将整个待排序的记录序列分割成为若干子序列。 showImg(https://segmentfault.com/img/bVbvpYZ?w=900&h=250); 1. 前言 算法为王。 想学好前端,先练好内功,只有内功深厚者,前端之路才...

    haitiancoder 评论0 收藏0
  • JavaScript 数据结构算法之美 - 非线性表中的树、堆是干嘛用的 ?其数据结构是怎样的 ?

    摘要:笔者写的数据结构与算法之美系列用的语言是,旨在入门数据结构与算法和方便以后复习。非线性表中的树堆是干嘛用的其数据结构是怎样的希望大家带着这两个问题阅读下文。其中,前中后序,表示的是节点与它的左右子树节点遍历访问的先后顺序。 showImg(https://segmentfault.com/img/bVbuvnj?w=900&h=250); 1. 前言 想学好前端,先练好内功,内功不行,...

    singerye 评论0 收藏0
  • JavaScript 数据结构算法之美 - 栈内存堆内存 、浅拷贝深拷贝

    摘要:栈内存与堆内存浅拷贝与深拷贝,可以说是前端程序员的内功,要知其然,知其所以然。栈内存与堆内存中的变量分为基本类型和引用类型。 showImg(https://segmentfault.com/img/bVbuvnj?w=900&h=250); 前言 想写好前端,先练好内功。 栈内存与堆内存 、浅拷贝与深拷贝,可以说是前端程序员的内功,要知其然,知其所以然。 笔者写的 JavaScrip...

    dailybird 评论0 收藏0
  • JavaScript 数据结构算法之美 - 十大经典排序算法汇总

    摘要:笔者写的数据结构与算法之美系列用的语言是,旨在入门数据结构与算法和方便以后复习。这应该是目前较为简单的十大经典排序算法的文章讲解了吧。比如原本在的前面,而,排序之后,在的后面十大经典排序算法冒泡排序思想冒泡排序只会操作相邻的两个数据。 showImg(https://segmentfault.com/img/bVbvHet); 1. 前言 算法为王。想学好前端,先练好内功,内功不行,就...

    zsy888 评论0 收藏0

发表评论

0条评论

Rocko

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<