【问题标题】:Is there a better way to do partial sums of array items in JavaScript?有没有更好的方法在 JavaScript 中对数组项进行部分求和?
【发布时间】:2019-10-24 10:23:28
【问题描述】:

我想知道是否有更好的方法可以为数组的部分和生成性能更好的解决方案。

给定一个数组x = [ 0, 1, 2, 3, 4, 5 ],我生成了项目的子数组,然后计算每个数组的总和,得到:

[ 0, 1, 3, 6, 10, 15 ]

所以完整的代码是:

x.map((y,i)=>x.filter((t,j)=>j<=i))
 .map(ii=>ii.reduce((x,y)=>x+y,0))

我想知道平面地图或其他一些数组方法是否会有不需要扩展每个子数组的解决方案。

【问题讨论】:

  • 顺便说一下,这叫做prefix-sumscan。许多集合框架在标准库中都有它,但不幸的是 ECMAScript 没有。

标签: javascript arrays functional-programming prefix-sum


【解决方案1】:

很多,通过保持一个运行总计:

function* partialSums(iterable) {
    let s = 0;

    for (const x of iterable) {
        s += x;
        yield s;
    }
}

const x = [0, 1, 2, 3, 4, 5];
console.log(Array.from(partialSums(x)).join(', '));

线性时间,在线。 (也可以直接生成数组;下面展开。)

const partialSums = arr => {
    let s = 0;
    return arr.map(x => s += x);
};

const x = [0, 1, 2, 3, 4, 5];
console.log(partialSums(x).join(', '));

【讨论】:

  • 呃...yield 是一个很弱的词。将每个项目添加到数组中,然后像男人一样return
  • @LogicalBranch:那不会在线,那你还是用第二个代码sn-p吧。
  • @LogicalBranch 为什么? 2019 年的迭代器 FTW,不是吗?
  • @LogicalBranch 迭代器是当你有一个大数组或者你只需​​要一个特定值时进行部分求和的方法。为什么不喜欢收益?我知道它不像 return 那样在你的脸上,但它描述了它在做什么。
  • 这是一个很好的命令式答案,但是,它不适合用函数式编程标记的问题。这只是将特定辅助函数以 1:1 的比例分配给非常特定的问题。零泛化,什么都没学到。
【解决方案2】:

平面地图在您的情况下不会有用,因为您不是试图将作为列表的部分结果展平,但我们可能会尝试在单个 reduce 中解决您的问题:

[0, 1, 2, 3, 4, 5]
.reduce(
   ([arr, sum], el) => { // We pass along array and running sum
       const next = sum + el
       return [[...arr, next], next]
   },
   [[], 0] // We need to seed our reduce with empty array and accumulator for calculating running sum
)[0] // Array containing array and the last sum is returned, so we need to take only the first element

它也只对数组进行一次迭代,因此它可能比创建切片然后对它们求和的解决方案性能更高。

或带有array.push 的版本,它重用相同的数组:

[0, 1, 2, 3, 4, 5]
.reduce(
   ([arr, sum], el) => { // We pass along array and running sum
       const next = sum + el
       arr.push(next)
       return [arr, next]
   },
   [[], 0] // We need to seed our reduce with empty array and accumulator for calculating running sum
)[0] 

【讨论】:

  • 这同样会很慢 (O(n²)),因为它每次想要添加一些东西时都会复制数组。
  • @Ry- 你可以很容易地用push 替换它,但它会是一个变异数组,这将使它不是纯粹的功能。你看到标签functional-programming的问题了吗?
  • 没有什么是纯粹的功能性的。您拥有该阵列。完全没问题。
  • @Ry- 不是真的,你可以有纯粹的函数方法。但是正如您所提到的,使用push 的版本是引用透明的,所以也可以。编辑了我的答案以包含第二个版本。
【解决方案3】:

下面,scan 采用映射函数 f 和初始累加器 r -

const scan = (f, r, [ x, ...xs ]) =>
  x === undefined
    ? [ r ]
    : [ r, ...scan (f, f (r, x), xs) ]
  
const add = (x, y) =>
  x + y

const print = (...vs) =>
  vs .forEach (v => console .log (v))

const data =
  [ 0, 1, 2, 3, 4, 5 ]
  
print
  ( scan (add, 0, data)
  , scan (Math.max, 3, data)
  , scan (add, 0, [])
  )

// [ 0, 0, 1, 3, 6, 10, 15 ]
// [ 3, 3, 3, 3, 3, 4, 5 ]
// [ 0 ]

如果您需要一个不使用初始累加器的程序,则可以使用输入数组的第一个元素。这种变体称为scan1 -

const scan = (f, r, [ x, ...xs ]) =>
  x === undefined
    ? [ r ]
    : [ r, ...scan (f, f (r, x), xs) ]
    
const scan1 = (f, [ x, ...xs ]) =>
  x === undefined
    ? []
    : scan (f, x, xs)

const add = (x, y) =>
  x + y
  
const print = (...vs) =>
  vs .forEach (v => console .log (v))

const data =
  [ 0, 1, 2, 3, 4, 5 ]

print
  ( scan1 (add, data)
  , scan1 (Math.max, data)
  , scan1 (Math.min, data)
  , scan1 (add, [])
  )
  
// [ 0, 1, 3, 6, 10, 15 ]
// [ 0, 1, 2, 3, 4, 5 ]
// [ 0, 0, 0, 0, 0, 0 ]
// []

如有必要,可以进行性能优化并解决堆栈溢出问题,所有这些都不会牺牲功能样式 -

const scan = (f, init, xs) =>
  loop
    ( ( r = []
      , a = init
      , i = 0
      ) =>
        i >= xs.length
          ? push (a, r)
          : recur
              ( push (a, r)
              , f (a, xs[i])
              , i + 1
              )
    )

现在让我们用大量输入来运行它 -

// BIG data!
const data =
  Array .from (Array (10000), (_, x) => x)

// fast and stack-safe
console .time ("scan")
const result = scan (add, 0, data)
console .timeEnd ("scan")
// scan: 8.07 ms

console .log (result)
// [ 0, 0, 1, 3, 6, 10, 15, ..., 49985001 ]

这取决于以下通用功能过程 -

const recur = (...values) =>
  ({ recur, values })

const loop = f =>
{ let r = f ()
  while (r && r.recur === recur)
    r = f (...r.values)
  return r
}

const push = (x, xs) =>
  ( xs .push (x)
  , xs
  )

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

const recur = (...values) =>
  ({ recur, values })

const loop = f =>
{ let r = f ()
  while (r && r.recur === recur)
    r = f (...r.values)
  return r
}

const push = (x, xs) =>
  ( xs .push (x)
  , xs
  )

const scan = (f, init, xs) =>
  loop
    ( ( r = []
      , a = init
      , i = 0
      ) =>
        i >= xs.length
          ? push (a, r)
          : recur
              ( push (a, r)
              , f (a, xs[i])
              , i + 1
              )
    )

const add = (x, y) =>
  x + y

const data =
  Array .from (Array (10000), (_, x) => x)
  
console .time ("scan")
const result = scan (add, 0, data)
console .timeEnd ("scan")

console .log (result)
// [ 0, 0, 1, 3, 6, 10, 15, ..., 49995000 ]

【讨论】:

  • scan 很好。在 JavaScript 中实现 scan 就好像它是 Haskell 一样不好,因为它会给你带来二次时间复杂度和甚至不是特别大的数组上的堆栈溢出。
  • @Ry-,谢谢,但我没有看到二次时间复杂度。中间数组肯定是浪费的,但简单的程序准确地显示了需要做什么。这种程序的优化很容易,所以我通常把它们留给最了解他们需求的最终用户。对答案的更新显示了堆栈安全的scan。感谢您的评论。
  • [ r, ...scan (f, f (r, x), xs) ] spread – 0 + 1 + ... + n−1 个项目的副本,二次方。 [ x, ...xs ] 解构 – n−1 + n−2 + ... + 0 个项目的副本,二次方。感谢您添加其他版本。
  • 是的,但是 O(n*(n+1)/2) = O(n²)。 “一半”是一个常数因子。
  • @ScottSauyet: [r, ...s][r].concat(s) 在这方面是相同的。
【解决方案4】:

您可以简单地使用带有变量的for 循环来跟踪最后的总和

let x = [ 0, 1, 2, 3, 4, 5 ]

let sum = (arr) => {
  let sum = 0
  let final = []
  for(let i=0; i<arr.length; i++){
    sum+= arr[i]
    final.push(sum)
  }
  return final
}

console.log(sum(x))

你也可以使用地图:

let x = [0, 1, 2, 3, 4, 5]

let sum = (arr) => {
  let sum = 0
  return arr.map(current => sum += current )
}

console.log(sum(x))

【讨论】:

  • 第二个代码 sn-p 看起来非常熟悉,因为它被隐藏了。
  • @Ry- 上面一行声明you can also use map,我只想展示传统和实用的方法,如果人们认为这是一个不赞成投票的正当理由,我很高兴 :)
【解决方案5】:

您只需在每一步中将当前值添加到上一个结果中,这样您就可以使用简单的 reduce。

const array = [0, 1, 2, 3, 4, 5, 6];

const sums = array.reduce((acc,current,index) => {
  const prev = acc.length ? acc[index-1] : 0;
  acc.push(prev + current);
  return acc;
},[]);

console.log(sums.toString());

【讨论】:

    【解决方案6】:

    如果你问有没有更快更有效的方法,那么其他答案就足够了。

    但是,如果我们将其表述为映射函数,我认为类似于您当前解决方案的内容更易于阅读且更具声明性。

    特别是像“将每个值映射到自身加上数组中所有先前的值”。

    您可以使用过滤器,就像您在问题中所做的那样,但我认为切片更清晰。

    const x = [ 0, 1, 2, 3, 4, 5 ];
    
    // A common generic helper function
    const sum = (acc, val) => acc + val
    
    const sums = x.map((val, i, self) => val + self.slice(0, i).reduce(sum, 0))
    

    【讨论】:

    • 代码使用相同的方法,但我不认为该答案解释了该方法的价值。答案应该不仅仅是代码。
    【解决方案7】:

    如果您保留一个外部累加器变量,则可以直接使用 map:

    const x = [ 0, 1, 2, 3, 4, 5 ];
    
    let acc = 0;
    const prefixSum = x.map(x => acc += x);
    
    console.log(prefixSum);

    【讨论】:

    • 我投了赞成票。我猜那些投反对票的人不熟悉assignment expression,不熟悉x =&gt; acc += x
    【解决方案8】:

    一种选择是使用单个.map,它在内部使用.reduce 来汇总切片的部分数组:

    const x = [0, 1, 2, 3, 4, 5];
    
    const sum = (x, y) => x + y;
    const partialSums = x.map((_, i, arr) => arr.slice(0, i + 1).reduce(sum));
    console.log(partialSums);

    【讨论】:

      【解决方案9】:

      这是一个使用递归函数的简单答案。

      var array = [ 0, 1, 2, 3, 4, 5 ];
      
      function sumArray(arrayToSum, index){
          if(index < arrayToSum.length-1){
              arrayToSum[index+1] = arrayToSum[index] + arrayToSum[index+1];
              return sumArray(arrayToSum, index+1);
        }else
          return arrayToSum;
      
      }
      sumArray(array, 0);
      
      console.log(array);
      

      【讨论】:

      • 这个优势为零,需要 JIT 编译器优化掉递归,否则很容易溢出大型数组的调用堆栈。它还将运行总数存储到数组中并读回,而不是将其保存在变量中。
      【解决方案10】:

      一种方法可以使用 for each 然后对数组进行切片以逐个获取元素,然后将它们全部通过array.reduce 求和您可以这样做

      let x = [0, 1, 2, 3, 4, 5]
      let sum = []
      x.forEach((_, index) => {
        index++;
        sum.push(x.slice(0, index).reduce((a, b) => a + b))
      })
      console.log(sum)

      我们得到[0],然后是[0,1],然后是[0,1,2],然后是[0,1,2,3],通过[0,1,2].reduce((a, b) =&gt; a + b)),我们得到3。只需将其推送到新数组即可。哪个是你的答案。

      这样做我们可以走得更短。对我来说,这似乎是一个非常优化的解决方案。

      let ar = [0, 1, 2, 3, 4, 5]
      let s = 0
      let arr = []
      ar.forEach((n, i) => arr.push(s += n))
      console.log(arr)

      或者.map,你可以

      let ar = [0, 1, 2, 3, 4, 5], s = 0
      console.log(ar.map((n, i) => s += n))

      【讨论】:

      • “对我来说,这似乎是一个非常优化的解决方案。” 不是。 “你也可以用.map代替forEach like”但是为什么呢?
      • 另外,我的最后一个解决方案比你的更好jsben.ch/BAg36你甚至可以在那里检查它@Ry-
      • 不同之处在于它毫无意义地使用map 来实现推送副作用并丢弃结果。 (嗯,我想这不是毫无意义的,因为这意味着你在技术上没有复制我的答案。)
      • 不,我只是解释了为什么它不像你想象的那样快。尝试基准测试。当然,无论如何你都可以使用它,但无论如何这都是不好的做法。
      • @Ry- 好的,我会接受你的方法,因为我是一名初级人员,并且 .map 返回一个数组,这也可能导致一些数组浪费。编辑了我的答案
      【解决方案11】:

      最简单的答案是单行:如果 x 是 [0,1,2,3,4,5]

      x.map(i=>s+=i, s=0)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-07-24
        • 1970-01-01
        • 2023-04-05
        • 2010-10-03
        • 2016-02-13
        • 2013-03-05
        • 1970-01-01
        相关资源
        最近更新 更多