【问题标题】:Very big recursion javascript非常大的递归javascript
【发布时间】:2019-04-09 22:01:02
【问题描述】:

我有这段代码,它用递归计算数组元素的总和,但是当数组太大时,它会抛出 Maximum call stack size exceeded 错误。

var a = new Array(10 ** 7);
a.fill(0);

function sum(arr, i = 0) {
  if(i === arr.length) {
      return 0;
  }

  return arr[i] + sum(arr, i + 1);
}

sum(a);

所以我需要以某种方式对其进行更改以使其在所有情况下都能正常工作,并且我认为我可以使其与 Promises 异步工作,但它总是返回 Promise pending。

var a = new Array(10 ** 7);
a.fill(0);

var sum = (arr, i = 0) => new Promise((resolve) => {
  setTimeout(() => {
    if(i === arr.length) {
      return resolve(0);
    }

    return  sum(arr, i + 1).then(el => el + arr[i]);
  }, 0);
});

sum(a);

我该如何解决?

任何帮助将不胜感激!

【问题讨论】:

  • 你为什么需要递归,你不能只 reduce() 吗?
  • 学根没坏处
  • 当然可以,但这是一项任务,我必须修改代码并使其适用于大数组
  • 不要为此任务使用递归(在 javascript 中)。您对此过于复杂:const sum = arr => arr.reduce((a, b) => a + b, 0)

标签: javascript recursion promise


【解决方案1】:

你只是解决了 i 是 arr.length 的情况,所以所有的链式 Promise 都将永远挂起。 Return 不会自动为我们解决,所以需要明确:

var a = new Array(10);
a.fill(0);
a[0] = 3;
var sum = (arr, i = 0) => new Promise((resolve) => {
  setTimeout(() => {
    if(i === arr.length) {
      resolve(0);
    } else {
      resolve(sum(arr, i + 1).then(el => el + arr[i]));
    }
  }, 0);
});

sum(a).then(console.log)

【讨论】:

    【解决方案2】:

    对于使用本机任务的问题,有一些解决方案。这个是使用 Promise 来安排一个微任务:

    (function() {
      function sum(arr, i = 0) {
        if(arr.length === i) {
            return Promise.resolve(0);
        }
    
        return Promise.resolve(null)
          .then(() => sum(arr, i + 1))
          .then((x) => x + arr[i]);
      }
    
      sum(a).then(s => console.log(s));
    }());
    

    但这会强制引擎等待执行完成。所以对于大型数组,我不建议你在主线程上这样做。

    您还可以执行以下操作:

    (function() {
      function sum(arr, i = 0) {
        if(arr.length === i) {
            return Promise.resolve(0);
        }
    
        return new Promise(resolve => {
          setTimeout(() => {
            sum(arr, i + 1).then(x => resolve(x + arr[i]));
          });
        });
      }
    
      sum(a).then(s => console.log(s));
    }());
    

    然后对这段代码进行一些更改并使其更优雅,我们可以执行以下操作:

    (function() {
      const defer = () => new Promise((resolve) => setTimeout(resolve));
    
      async function sum(arr, i = 0) {
        if(arr.length === i) {
            return 0
        }
    
        await defer();
        return await sum(arr, i + 1) + arr[i];
      }
    
      sum(a).then(s => console.log(s));
    }());
    

    如果您的环境支持尾递归,您可以更改它以使用它:http://2ality.com/2015/06/tail-call-optimization.html

    更新

    实际上,还有另一种方法可以做到这一点。 rxjs 库提供了一个队列调度器,它可以帮助您将递归逻辑进行迭代,而无需对代码进行很多更改。我已经创建了您的 sum 方法 here 的示例。

    【讨论】:

      【解决方案3】:

      我不知道您为什么要使用 Promise 并使函数异步。但如果你需要解决这两种情况:

      const sum = (arr, i = 0) => new Promise((resolve) => {
        setTimeout(() => {
          if(i === arr.length) {
            return resolve(0);
          }
      
          return  sum(arr, i + 1).then(el => resolve(el + arr[i]));
        }, 0);
      });
      

      现在这也返回了一个承诺。一旦你异步了,你就再也不能回去了。你需要使用返回的promise来获取返回值:

      sum([1, 2, 3, 4]).then(return => console.log(return));
      

      最好不要让它异步。 ES6 支持尾递归,所以你可以这样:

      function sum(arr) {
          function helper(acc, i) {
              if (i<0) {
                  return acc;
              }
              return helper(arr[i]+acc, i-1);
          }
      
          return helper(0, arr.length - 1);
      }
      

      由于顺序无关紧要,我冒昧地对这些值求和。现在信不信由你,但 helper 已经作为一种抽象存在于语言中,它为您提供每个元素的值以及每次迭代中的 acc。因此,您可以这样做:

      function sum(arr) {
          return arr.reduce((acc, val) => acc + val, 0)
      }
      

      【讨论】:

      • “ES6 支持尾递归” 不在我所知道的任何实现上。一年多前,Babel 也取消了该功能。
      • 另外,为什么要在 Promise 构造函数中使用setTimeout?
      • @user633183 这是 OP 代码。我刚刚添加了缺少的resolve。由于这是一项要求,因此它将适用于所有符合 ES6 的实现。 Safari/iOS 支持尾递归,可能是最符合 ES6 的。现在你知道了。
      • 哇,对 Safari 的新尊重 ✊
      【解决方案4】:

      setTimeout 不是解决堆栈溢出问题的方法。管理堆栈是对函数调用进行排序的问题。您可以通过多种方式做到这一点,最直观的是loop/recur蹦床。

      const loop = f =>
      { let acc = f ()
        while (acc && acc.tag === recur)
          acc = f (...acc.values)
        return acc
      }
      
      const recur = (...values) =>
        ({ tag: recur, values })
      
      const sum = (xs = []) =>
        loop ((result = 0, i = 0) =>         // initial state
          i >= xs.length                     // exit condition
            ? result                         // return value
            : recur (result + xs[i], i + 1)) // next state
            
      const tenMillionNumbers =
        Array.from (Array (1e7), (_,n) => n)
        
      console.time ('recursion is not slow')
      console.log (sum (tenMillionNumbers))
      console.timeEnd ('recursion is not slow')
      
      // => 49999995000000
      // recursion is not slow: 2171ms

      我介绍了许多其他技术来解决这个问题here。

      堆栈安全递归是我经常写的东西,主题是 almost 30 answers

      【讨论】:

        【解决方案5】:
        var a = new Array(1000);
        a.fill(1);
        function sum(arr, i = 0, res = 0, resolve) {
          if(!i) return new Promise((resolve) => sum(arr, i + 1, res + arr[i], resolve), 0); 
          if(i === arr.length) return resolve(res);
          setTimeout(function(){
            sum(arr, i + 1, res + arr[i], resolve);
          }, 0); 
        }
        sum(a).then(console.log);
        

        【讨论】:

        • 需要对您编写的代码以及它将如何解决问题进行一些解释。
        【解决方案6】:

        您需要使用迭代过程而不是递归过程。因此,您无需累积调用,而是计算每次迭代调用的总和。这可以通过使用一个辅助函数来实现,该函数将获得(sum, array, currentValue)

        【讨论】:

        • 即使在你的函数内部?
        • @AngelaHayrapetian 当然不要使用预建,但你必须能够制作自己的助手
        • @AliaksandrSushkevich 是的。我必须让它异步工作,这就是为什么我试图用承诺来做到这一点,但我做错了什么
        【解决方案7】:

        如果出于某种原因您需要使用递归并且不介意这需要长时间,您可以使用超时;但是我绝对不推荐使用这个。我主要发布它,因为它是可能的。

        var arr = new Array(10 ** 7);
            arr.fill(0);
        
        var len = arr.length,
            idx = 0,
            sum = 0,
            sumFn = () => {
              setTimeout(() => {
                sum += arr[idx];
                idx++;
        
                if (idx < len) {
                    sumFn();
                } else {
                    //complete
                }
              }, 1);
            }
        
        sumFn();
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-04-06
          • 1970-01-01
          • 1970-01-01
          • 2016-04-15
          • 2015-08-21
          • 2023-02-16
          • 2016-06-05
          • 1970-01-01
          相关资源
          最近更新 更多