在reduce方法中过滤accumulator/current有什么区别

what is the difference between filtering on accumulator/current in reduce method

Array.prototype.reduceArray.prototype.filter 链接时,在过滤当前值而不是累加器值时有什么区别(概念上和底层)?

// function union creates a union of all values that appear among arrays
// example A

const union = (...arrays) => {
    return arrays.reduce((acc, curr) => {
      const newElements = acc.filter(el => !curr.includes(el));

      return curr.concat(newElements);
    });
  };
 console.log(union([1, 10, 15, 20], [5, 88, 1, 7], [1, 10, 15, 5]));

// output (7) [1, 10, 15, 5, 88, 7, 20]

// example B

const union = (...arrays) => {
    return arrays.reduce((acc, curr) => {
      const newElements = curr.filter(el => !acc.includes(el));

      return acc.concat(newElements);
    });
  }; 
  console.log(union([1, 10, 15, 20], [5, 88, 1, 7], [1, 10, 15, 5]));

//output (7) [1, 10, 15, 20, 5, 88, 7]

输出的差异表明数组的计算顺序是 'opposite'。据我所知,在使用 arr.filter 时,值是从头到尾计算的,而 curr.filter 则相反。除此之外,它们是否有任何其他后果取决于您是通过累加器还是当前值进行过滤?这会在不同的上下文中引发错误吗?

问题不在于 reducefilter 的使用,而在于您使用 acc 和 [=20] 的顺序=].

当我 运行 遇到像这样看似奇怪的不一致时,我通常采取的第一步是创建一个测试用例,然后 运行 手动通过它。在这里,您已经为我们创建了一个测试用例...

const testData = [
  [1, 10, 15, 20],
  [5, 88, 1, 7],
  [1, 10, 15, 5],
]

现在我们需要运行遍历函数的每个版本,看看每个阶段的输出是什么。

需要注意的一件事(我直到今天晚上才知道!)是如果 reduce 没有收到 initialValue 作为第二个参数,它将使用第一个项目在数组中作为 initialValue。这意味着我们只需要考虑每个函数的 2 次执行而不是 3 次。

示例 A

const union = (...arrays) => {
  return arrays.reduce((acc, curr) => {
    const newElements = acc.filter(el => !curr.includes(el))

    return curr.concat(newElements)
  })
}

在函数的第一个版本中,对正在发生的事情的简短描述是我们循环遍历累加器 (acc) 并删除我们当前正在处理的数组中已经存在的所有项目比较(curr)。然后我们将该列表添加到 curr.

end

我们将 newElements 推到 curr 末尾这一事实很重要。这就是为什么两个不同版本的顺序不同的原因。

第一次执行

const acc = [1, 10, 15, 20]
const curr = [5, 88, 1, 7]
const newElements = [10, 15, 20] // these elements exist in acc but not in curr
curr.concat(newElements) === [5, 88, 1, 7, 10, 15, 20]

第二次执行

const acc = [5, 88, 1, 7, 10, 15, 20] // carried over from first execution
const curr = [1, 10, 15, 5]
const newElements = [88, 7, 20] // these elements exist in acc but not in curr
curr.concat(newElements) === [1, 10, 15, 5, 88, 7, 20]

示例 B

const union = (...arrays) => {
  return arrays.reduce((acc, curr) => {
    const newElements = curr.filter(el => !acc.includes(el))

    return acc.concat(newElements)
  })
}

在函数的第一个版本中,对正在发生的事情的简短描述是我们循环遍历我们当前正在比较的数组 (curr) 并删除数组中已经存在的所有项目累加器(acc)。然后我们将该列表添加到 acc.

的末尾

您已经可以在下面的第一次执行结束时看到结果的顺序大不相同。

第一次执行

const acc = [1, 10, 15, 20]
const curr = [5, 88, 1, 7]
const newElements = [5, 88, 7] // these elements exist in curr but not in acc
acc.concat(newElements) === [1, 10, 15, 20, 5, 88, 7]

第二次执行

const acc = [1, 10, 15, 20, 5, 88, 7] // carried over from first execution
const curr = [1, 10, 15, 5]
const newElements = [] // these elements exist in acc but not in curr
acc.concat(newElements) === [1, 10, 15, 20, 5, 88, 7]

结论

对您问题的简短回答是,对累加器和当前数组进行过滤的区别在于,只要输入不同,结果就会不同。 ‍♂️

Besides from that are they any other consequences dependent on if you filter through the accumulator or current value? Could this throw an error in a different context?

幸运的是,没有任何关于错误的担忧。但是,值得注意的是,函数的第二个版本比第一个版本 ~10% faster。我猜这纯粹是间接的。不同的测试数据集可能会产生不同的性能结果。

在示例 1 中,当您连接两个列表时,您要确保 accumulator 不会包含来自 current 的任何元素。

另一方面,在示例 2 中,您要确保 current 不包含 accumulator 中已经存在的任何元素。

不同之处在于元素出现的最终顺序


我认为这两个示例都不是有效的,因为它们都涉及 O(n2) 时间复杂度,因为您是嵌套迭代。正如其他人所说,第二个可能性能更高一些,因为嵌套迭代将在一个可能比累加器短的块上进行。


我宁愿或多或少这样写:

const union = (...tuples) => Array.from(
  new Set(
    tuples.flatMap(n => n),
  )
);



console.log(
  union([1, 10, 15, 20], [5, 88, 1, 7], [1, 10, 15, 5]),
);