提高 JavaScript 函数的速度

Posted

技术标签:

【中文标题】提高 JavaScript 函数的速度【英文标题】:Improve the speed of a JavaScript function 【发布时间】:2020-11-24 05:52:55 【问题描述】:

我在 CodeWars 上找到了一个任务,我设法解决了它,但是,提交后说:

执行超时:(12000 毫秒)

当我尝试测试函数通过时,但我猜它太慢了。 在你谴责我没有自己找到答案之前。我真的不关心提交它作为回复,但我不知道如何让它更快,这就是我在这里的原因。 这是函数:

const ls = [0, 1, 3, 6, 10]

const partsSums = (ls) => 
    const sum = []
    for(let i = 0, len = ls.length; i < len + 1; i++) 
        let result = ls.slice(i).reduce( (accumulator, currentValue) => accumulator + currentValue, 0)
        sum.push(result)
    
    return sum

以下是说明:

让我们考虑这个例子(以一般格式编写的数组):

ls = [0, 1, 3, 6, 10]

它的以下部分:

ls = [0, 1, 3, 6, 10]
ls = [1, 3, 6, 10]
ls = [3, 6, 10]
ls = [6, 10]
ls = [10]
ls = []

对应的总和是(放在一个列表中):[20, 20, 19, 16, 10, 0]

函数parts_sums(或其他语言的变体)将采用 作为参数列表 ls 并返回其部分总和的列表 上面定义的。

【问题讨论】:

其实你需要去这里codereview.stackexchange.com @Ifaruki 我不同意。 CR 适用于工作但可以做得更好的代码。这个不起作用。它在执行时间内失败,这意味着它不满足要求。它有问题需要解决,这就是 SO 的意义所在。 @VLAZ:这太荒谬了。如果要相信 OP,则该代码有效。它只是需要改进,而不是修复。这就是代码审查的意义所在。 你能把 Codewars kata 的链接放上去吗? @VLAZ Code Review 甚至还有time-limit-exceeded tag。 【参考方案1】:

对于这种数组操作,最好不要使用内置方法,例如 slicereduce,因为与 for 循环或任何其他循环方法相比,它们的速度很慢。

这种方法采用单循环并使用索引来获取给定数组的值并获取新数组的最后一个总和。

对Codewars: Sums of Parts的一些速度测试:

5621 毫秒,稀疏数组 sum = []; sum[i] = 0;(此答案的第一个版本), 3452 毫秒 使用 Array(i + 1).fill(0) 和不使用 sum[i] = 0;1261 毫秒Array(i + 1)sum[i] = 0;(见下文), 3733 毫秒,使用 Icepickle 的 first attempt。

const
    partsSums = (ls) => 
        let i = ls.length;
        const sum = Array(i + 1);

        sum[i] = 0;
        while (i--) sum[i] = sum[i + 1] + ls[i];

        return sum;
    ,
    ls = [0, 1, 3, 6, 10];

console.log(...partsSums(ls));

【讨论】:

slicereduce 与显式循环相比并不慢,OP 代码的问题在于 O(n²) 算法。 知道为什么mine 似乎更快吗?是不是数组索引访问,好像真的没有多大意义……再看,如果我预先填充sum数组,执行时间也快了很多 @Icepickle 是的,Nina 的回答从最后填充了数组,创建了一个可能没有得到很好优化的稀疏数组。从前面生长它通常更快。 现在具有所需长度的数组。现在应该可以更快地工作了。 当我从原始问题运行 Cortoloman 的函数时,它需要 1129 毫秒 - 这比这个答案要快!【参考方案2】:

您仍然可以采用更实用的方法,但要优化您进行计算的方式。

这是一个想法 - 因为您要对所有项目求和,然后对除第一个之外的所有项目求和,然后对除第二个之外的所有项目求和,等等,在数学上相当于得到总和,然后按顺序从中减去每个数字,保留总数。

[sum([41, 42, 43]), sum([42, 43]), sum([43]), sum([])]

等同于:

total = sum([41, 42, 43])
[total - 0, total - 0 - 41, total - 0 - 41 - 42, total - 0 - 41 - 42- 43]

等同于:

total = sum([41, 42, 43])
[total -= 0, total -= 41, total -= 42, total -= 43]

概括地说,这看起来像:

total = sum([a1, a2, ..., aN])
[total -= 0, total -= a1, total -= a2, ..., total -= aN]

使用可信赖的Array#reduce,我们可以得出一次总和。然后我们可以使用Array.map 使用ls.map(num =&gt; total -= num) 导出新数组。

这里唯一的问题是我们少了一个项目——我们没有计算所有项目都必须存在的初始total -= 0。一种方法是将其附加到开头[0].concat(ls) 将创建正确的数组以进行映射。但是,由于我们已经知道该值是多少,我们可以跳过这一步,直接用total 替换(毕竟total -= 0 的结果是total 并且保持total 不变)。因此,我们可以直接使用[total].concat(ls.map(num =&gt; total -= num))total 开头并添加其余项目。到最后。

const ls = [0, 1, 3, 6, 10]

const partsSums = (ls) => 
    let total = ls.reduce((a, b) => a + b, 0);
    
    return [total]
      .concat(
        ls.map(num => total -= num)
      );


console.log(partsSums(ls));

【讨论】:

我误读为num =&gt; total - num如果您必须在map 中使用副作用(不是非常实用的方法),最好明确说明:num =&gt; total -= num; return total; 【参考方案3】:

就个人而言,我只会使用前一个总和来计算下一个总和,我认为没有必要重新迭代所有以前的总和,所以,我可能会进行基本循环,然后反转结果,比如所以

function partsSums(ls) 
  const result = [0];
  if (ls.length === 0) 
    return result;
  
  for (let i = ls.length, q = 0; i--; q++) 
    result.push(result[q] + ls[i]);
  
  return result.reverse();

或者,不反转,看起来更像Nina的解决方案(除了预定义数组的长度)

function partsSums(ls) 
  const len = ls.length;
  const result = new Array(len+1);
  result[len] = 0;
  for (let i = len; i--;) 
    result[i] = result[i+1] + ls[i];
    
  return result;

在codewars nodejs 引擎上,两者似乎也比 Nina 运行得更快,在第一部分可能是因为 push,在第二部分,可能是因为数组的长度是从一开始就定义的,有关更多信息,请参阅 this question

【讨论】:

【参考方案4】:

在执行时使用普通 for 循环的解决方案。

var arr = [0, 1, 3, 6, 10];


function giveList(array)
    
    var sum=0;
    for(let i=0;i<array.length;i++)
       sum=sum+array[i];
    

    var result = [];
    result.push(sum);
    var temp;
    for(let i=0;i<array.length;i++)
       temp=sum-array[i];
       result.push(temp); 
       sum=sum-array[i];
        
     
 return result;


console.time();
console.log(giveList(arr));
console.timeEnd();

【讨论】:

【参考方案5】:
const partsSums = (ls, sum = 0) =>
   [...ls, 0].reverse().map(x => sum = x + sum).reverse();

当我在 CodeWars 上运行它时大约需要 1100 毫秒,这比其他答案略快。

【讨论】:

【参考方案6】:

重复操作太多了。例如:当你计算 [3, 6, 10] 的总和时,上一步 [1, 3, 6, 10] 已经计算了。所以你可以换个方向思考,背对尾计算数组的总和

const ls = [0, 1, 3, 6, 10];

function partSums(ls) 
   const len = ls.length;
   const dp = [];

   if(len === 0)  return [0] 
   dp[len] = 0;
   dp[len - 1] = ls[len - 1];
   for (let i = len - 2; i >= 0; i--) 
     dp[i] = dp[i + 1] + ls[i];
   

   return dp;

【讨论】:

以上是关于提高 JavaScript 函数的速度的主要内容,如果未能解决你的问题,请参考以下文章

javasc面向对象编程

如何提升JavaScript循环的运行速度

提高网站打开速度秘诀:压缩html,Javascript和CSS文件

帝国cms提高网站网页打开速度的手段

Javascript / D3.js - 绘制大型数据集 - 提高 d3.js 绘制的 svg 图表中的缩放和平移速度

JavaScript系列:函数式编程(开篇)