【问题标题】:How to understand scope/closure with recursion and helper function?如何通过递归和辅助函数来理解作用域/闭包?
【发布时间】:2021-01-05 22:17:31
【问题描述】:

以下是简单问题的示例:

示例 1:查找二叉树的最大深度。

我得到了正确的答案,但不知道为什么我原来的错误答案是错误的。

正确答案:

var maxDepth = function(root) {
    if (root === null) return 0;
    var maxDepth = 1;
    maxDepth = maxDepthHelper(root, 1, maxDepth);
    return maxDepth;
};

function maxDepthHelper(tree, depth, maxDepth) {
    if (tree.left === null && tree.right === null) {
        maxDepth = depth > maxDepth ? depth : maxDepth;
        return maxDepth;
    }
    if (tree.left) {
    maxDepth = maxDepthHelper(tree.left, depth + 1, maxDepth);
    }
    if (tree.right) {
    maxDepth = maxDepthHelper(tree.right, depth + 1, maxDepth);
    }
    return maxDepth;
}

错误答案:

var maxDepth = function(root) {
    if (root === null) return 0;
    var maxDepth = 1;
    maxDepthHelper(root, 1, maxDepth);
    return maxDepth;
};

function maxDepthHelper(tree, depth, maxDepth) {
    if (tree.left === null && tree.right === null) {
        maxDepth = depth > maxDepth ? depth : maxDepth;
        return;
    }
    if (tree.left) {
        maxDepthHelper(tree.left, depth + 1, maxDepth);
    }
    if (tree.right) {
        maxDepthHelper(tree.right, depth + 1, maxDepth);
    }
}

这与我认为 maxDepth 应该由辅助函数更改并且最终当我返回时它应该返回更改但它没有更改有关。它只返回 1,这是我分配给它的原始内容。但是在下面的示例中,我可以在辅助函数中从父级更改一个变量,那么我在这里缺少什么?

示例 2:给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中的第 k 个最小元素。

解决方案:

var kthSmallest = function(root, k) {
    let smallestArr = [];
    kthSmallestHelper(root, k, smallestArr);
    return smallestArr.pop()
};

function kthSmallestHelper(bst, k, array) {
    if (bst === null) return;
    kthSmallestHelper(bst.left, k, array);
    if (array.length === k) return;
    array.push(bst.val);
    kthSmallestHelper(bst.right, k, array);
}

【问题讨论】:

    标签: javascript algorithm recursion data-structures binary-tree


    【解决方案1】:

    在第二个程序中,maxDepth 是一个数字,通过值(复制)而不是通过引用传递。递归调用实际上是无操作的,它们的返回值立即被丢弃。对于正在学习如何将不同的变量类型从一个函数传递到另一个函数的初学者来说,这是一个常见的错误。


    也就是说,递归是一种函数式遗产,因此将其与函数式风格一起使用会产生最佳结果。这意味着避免诸如突变和变量重新分配之类的副作用。您可以大大简化您的 depth 程序 -

    function depth(tree)
    { if (tree == null)
        return 0
      else
        return 1 + max(depth(tree.left), depth(tree.right))
    }
    
    function max (a, b)
    { if (a > b)
        return a
      else
        return b
    }
    

    通常首选基于表达式的语法,因为表达式会计算为值,而语句(如 ifreturn)则不会 -

    const depth = tree =>
      tree == null
        ? 0
        : 1 + max(depth(tree.left), depth(tree.right))
    
    const max = (a, b) =>
      a > b
        ? a
        : b
    

    您的kthSmallest 程序比较困难,但JavaScript 的命令式生成器可以快速解决问题。使用了突变k--,但无法从函数外部观察到-

    function *inorder (tree)
    { if (tree == null) return
      yield* inorder(tree.left)
      yield tree.val
      yield* inorder(tree.right)
    }
    
    function kthSmallest (tree, k)
    { for (const v of inorder(tree))
        if (k-- == 0)
          return v
    }
    

    这个程序的纯表达形式略有不同-

    const inorder = tree =>
      tree == null
        ? []
        : [ ...inorder(tree.left), tree.val, ...inorder(tree.right) ]
    
    const kthSmallest = (tree, k) =>
      inorder(tree)[k]
    

    这是一个功能演示 -

    import { depth, fromArray, inorder, kthSmallest } from "./Tree"
    
    const rand = _ =>
      Math.random() * 100 >> 0
    
    const t =
      fromArray(Array.from(Array(10), rand))
    
    console.log("inorder:", Array.from(inorder(t)))
    console.log("depth:", depth(t))
    console.log("0th:", kthSmallest(t, 0))
    console.log("1st:", kthSmallest(t, 1))
    console.log("2nd:", kthSmallest(t, 2))
    console.log("99th:", kthSmallest(t, 99))
    

    输出 -

    inorder: [ 12, 14, 25, 44, 47, 53, 67, 70, 85, 91 ]
    depth: 5
    0th: 12
    1st: 14
    2nd: 25
    99th: undefined
    

    像下面的 Tree 这样编写模块是分离关注点和组织代码的好习惯 -

    // Tree.js
    
    const empty =
      null
    
    const node = (val, left = empty, right = empty) =>
      ({ val, left, right })
    
    const fromArray = (a = []) =>
      a.length < 1
        ? empty
        : insert(fromArray(a.slice(1)), a[0])
    
    const insert = (t = empty, v = null) =>
      t === empty
        ? node(v)
    : v < t.val
        ? node(t.val, insert(t.left, v), t.right)
    : v > t.val
        ? node(t.val, t.left, insert(t.right, v))
    : t
    
    const depth = (tree = empty) => ...
    
    const inorder = (tree = empty) => ...
    
    const kthSmallest = (tree = empty, k = 0) => ...
    
    export { depth, empty, fromArray, inorder, kthSmallest, node }
    

    展开下面的sn-p,在自己的浏览器中验证结果-

    const empty =
      null
    
    const node = (val, left = empty, right = empty) =>
      ({ val, left, right })
    
    const fromArray = (a = []) =>
      a.length < 1
        ? empty
        : insert(fromArray(a.slice(1)), a[0])
    
    const insert = (t = empty, v) =>
      t === empty
        ? node(v)
    : v < t.val
        ? node(t.val, insert(t.left, v), t.right)
    : v > t.val
        ? node(t.val, t.left, insert(t.right, v))
    : t
    
    const inorder = (tree = empty) =>
      tree === empty
        ? []
        : [ ...inorder(tree.left), tree.val, ...inorder(tree.right) ]
    
    const kthSmallest = (tree = empty, k = 0) =>
      inorder(tree)[k]
    
    const depth = (tree = empty) =>
      tree == null
        ? 0
        : 1 + Math.max(depth(tree.left), depth(tree.right))
    
    const rand = _ =>
      Math.random() * 100 >> 0
    
    const t =
      fromArray(Array.from(Array(10), rand))
    
    console.log("inorder:", JSON.stringify(Array.from(inorder(t))))
    console.log("depth:", depth(t))
    console.log("0th:", kthSmallest(t, 0))
    console.log("1st:", kthSmallest(t, 1))
    console.log("2nd:", kthSmallest(t, 2))
    console.log("99th:", kthSmallest(t, 99))

    【讨论】:

    • 不,你认为错误的答案并没有错。它不会覆盖它,因为在右侧的第二次调用中,之前的 maxDepth 作为参数传入,并且它只会在大于该值时更改 maxDepth。该解决方案通过了 leetcode 上的所有测试用例(100 个测试用例)。
    • 我的问题的答案是,当我尝试这样做时,您无法使用辅助函数不断正确地重新分配变量。您只能以这种方式改变对象/数组。一旦我将其更改为 maxDepth = {value: 1},而是更改了 helper 中的 value 属性,一切正常
    • @Kush,我错了,你是对的。看到maxDepth 的重新分配和突变导致一个人的大脑打结。 “错误”程序失败的原因是因为Number 值是按值(复制)传递的,而Object 值(如第一个程序中的数组)是通过引用传递的。希望我的回答告诉你这些问题是可以避免的。
    • 啊,这很有意义。谢谢你告诉我这个数字是通过副本而不是通过引用传递的。
    【解决方案2】:

    函数maxDepth(不幸命名)中的变量maxDepth(我们称之为外部maxDepth)存储一个值(数字1)。当你调用maxDepthHelper(root, 1, maxDepth)时,值1被传入并存储在maxDepthHelper内部的局部变量maxDepth中。我们可以给本地的maxDepth赋值,但是不会影响外部maxDepth中存储的值,因为它们是两个不同的变量。

    函数kthSmallest中的变量smallestArr存储一个值;该值是指向空数组的指针(内存位置)。当kthSmallestHelper(root, k, smallestArr) 像以前一样被调用时,该值(指针)被传入并存储在kthSmallestHelper 中的局部变量array 中。实际上,现在arraysmallestArr 都存储了指向同一个空数组的指针(内存位置)。如果我们现在对array 进行任何赋值,例如array=['some new arr'],变量smallestArr 将不会受到影响。但是当你调用一个突变方法时,比如array.push(bst),发生的事情是Javascript引擎查看存储在array中的指针,并修改存储在那个内存位置的数组。因为smallestArr存储了指向这个修改后数组的指针,所以如果你调用smallestArr.pop(),Javascript引擎会弹出修改后数组的最后一项。

    要记住的重要一点是,每当您编写像let x = /* some array or object */ 这样的表达式时,都会创建一个数组/对象,然后将指向该数组/对象的指针存储在变量中。如果写let x = /* some primitive value (like 3)*/,则值3直接存入变量中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-03-26
      • 2017-05-03
      • 2019-06-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-09
      • 1970-01-01
      相关资源
      最近更新 更多