【问题标题】:Way of understanding recursion properly (javascript)正确理解递归的方式(javascript)
【发布时间】:2016-08-23 04:39:51
【问题描述】:

我正在尝试理解递归,并且我对它的直观工作方式有一定的了解,但是返回数据的聚合是我遇到的问题。

例如,在 javascript 中为了展平一个数组,我想出了以下代码:

var _flatten = function(arr){
    if(!arr instanceof Array) return arr;
    var g = [];

   function flatten(arr){

     for(var i = 0; i < arr.length;i++){
         if(arr[i] instanceof Array){
         flatten(arr[i]);
         }else{
         g.push(arr[i]);
        }
     }
   }

  flatten(arr);
  return g;
}

像这样转

var list = [1,2,3,4,5,6,[1,2,3,4,5,[1,2,3],[1,2,3,4]]];

进入这个:[ 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 2, 3, 1, 2, 3, 4 ]

这很好,但全局变量 g 似乎是某种廉价的黑客行为。我不知道如何考虑到达堆栈顶部时返回的结果以及沿堆栈向下传播的函数的返回。你将如何实现这个功能,我怎样才能更好地掌握这一点?

谢谢!

【问题讨论】:

  • 检查java标签,java不是javascript
  • 对于这样的复杂递归,我会简化问题(使其更小),而不是使用[1,2,3,4,5,6,[1,2,3,4,5,[1,2,3],[1,2,3,4]]],您可以像[1,2,[1,2,3]] 那样简化它,然后从那里尝试了解这里的递归工作。
  • g 不是全局的,在 JavaScript 中使用闭包是一种非常好的方法。
  • 抱歉,我的意思是函数的全局变量,并且在每次迭代时都会重新初始化的 for 循环之外。
  • 要正确理解递归,please refer to this question.

标签: javascript recursion


【解决方案1】:

您可以将 g 作为参数发送给 flatten 函数,而不是全局变量(使其更适合递归),然后使用 return 语句将修改后的 g 传回。

var _flatten = function(arr) {
  if (!arr instanceof Array) return arr;

  function flatten(arr, g) {
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] instanceof Array) {
        flatten(arr[i], g);
      } else {
        g.push(arr[i]);
      }
    }
    return g;
  }

  return flatten(arr, []);
}

【讨论】:

  • 这听起来很明智。在这种情况下我是否必须返回任何内容,或者传入的 g 是否指向用户创建的数组?
  • 你可以返回 g,就像我最近的编辑一样。否则,您可以将数组替换为具有结果项的数组,保存参数。我认为这更符合“设计模式”递归。
  • 另外,这段代码或多或少是“reduce”操作的一个例子。
【解决方案2】:

many ways to write an array flattening procedure,但我知道你的问题是关于一般理解递归

g任何这个词的意义上都不是全局的,但它是实现选择的症状。只要您将其本地化到您的函数中,突变就不一定是坏事——g 永远不会泄露到可能有人可能观察到副作用的函数之外。

就个人而言,我认为最好将您的问题分解为小的通用过程,这样可以更容易地描述您的代码。

您会注意到,我们不必设置像g 这样的临时变量或像i 这样处理递增的数组迭代器——我们甚至不必查看数组的.length 属性。不必考虑这些事情,以声明方式编写我们的程序非常好。

// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = f => xs => xs.map(f).reduce((x,y) => x.concat(y), [])

// id :: a -> a
const id = x => x

// flatten :: [[a]] -> [a]
const flatten = concatMap (id)

// isArray :: a -> Bool
const isArray = Array.isArray

// deepFlatten :: [[a]] -> [a]
const deepFlatten = concatMap (x => isArray(x) ? deepFlatten(x) : x)

// your sample data
let data = [0, [1, [2, [3, [4, 5], 6]]], [7, [8]], 9]

console.log(deepFlatten(data))
// [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

console.log(flatten(data))
// [ 0, 1, [ 2, [ 3, [ 4, 5 ], 6 ] ], 7, [ 8 ], 9 ]

首先你会看到我做了两个不同的展平程序。 flatten 展平一层嵌套,deepFlatten 展平任意深度的数组。

您还会看到我使用了Array.prototype.mapArray.prototype.reduce,因为它们是由ECMAScript 提供的,但这并不意味着您只能使用您拥有的过程。您可以制定自己的程序来填补空白。这里我们制作了concatMap,这是一个由其他语言(例如 Haskell)提供的有用的泛型。

利用这些泛型,您可以看到deepFlatten 是一个非常简单的过程。

// deepFlatten :: [[a]] -> [a]
const deepFlatten = concatMap (x => isArray(x) ? deepFlatten(x) : x)

它由一个表达式组成,包括一个由一个 if 分支组成的 lambda(通过使用三元运算符 ?:


也许有很多东西需要吸收,但希望它表明“编写递归过程”并不总是涉及复杂的状态变量设置和控制递归的复杂逻辑。在这种情况下,这是一个简单的

<b>if</b> (<em>condition</em>) <em>recurse</em> <b>else</b> <em>don't</em>

如果您有任何问题,请告诉我。我很乐意尽我所能帮助您。

【讨论】:

    【解决方案3】:

    其实递归编码非常简单,它的各个方面都应该包含在函数体中。任何需要传递的信息都应通过参数发送到下一个递归。全局事物的使用非常难看,应避免使用。因此,我将简单地按如下方式进行就地数组展平工作;

    var list = [1,2,3,4,5,6,[1,2,3,4,5,[1,2,3],[1,2,[9,8,7],3,4]]];
    
    function flatArray(arr){
      for (var i = 0, len = arr.length; i < len; i++)
        Array.isArray(arr[i]) && (arr.splice(i,0,...flatArray(arr.splice(i,1)[0])), len = arr.length);
      return arr;
    }
    
    console.log(flatArray(list));

    【讨论】:

    • @Eladian 其实很简单。它只是遍历数组。如果它遇到一个数组项(Array.isArray(arr[i]) 为真),它会按顺序执行以下操作...从数组中取出它(arr.splice(i,1)[0])并将其递归发送到平面数组(flatArray(arr.splice(i,1)[0])),然后插入返回的通过将平面数组展开到相同的索引位置(... 展开运算符)(arr.splice(i,0,...flatArray(arr.splice(i,1)[0])
    猜你喜欢
    • 2015-08-31
    • 1970-01-01
    • 1970-01-01
    • 2012-11-08
    • 2010-10-24
    • 2019-04-14
    • 2013-07-28
    • 1970-01-01
    • 2020-12-26
    相关资源
    最近更新 更多