映射和缩减的主要区别

我使用了这两种方法,但对于这两种方法的用法,我感到非常困惑。

有没有什么是 map可以做,而 reduce不能做的,反之亦然?

注意: 我知道如何使用这两种方法,我质疑这两种方法之间的主要区别,以及我们何时需要使用。

74451 次浏览

一般来说,“ map”意味着将一系列输入转换为 等长度系列输出,而“ reduce”意味着将一系列输入转换为 更小系列输出。

人们所说的“ map-reduce”通常被理解为“变换,可能是并行的,串行的结合”。

当您“映射”时,您正在编写一个函数,该函数将 xf(x)转换为某个新值 x1。当您“ reduce”时,您正在编写某个函数 g(y),它接受数组 y并发出数组 y1。它们处理不同类型的数据并产生不同的结果。

map()函数通过在输入数组中的每个元素上传递一个函数来返回一个新数组。

这与 reduce()不同,reduce()以相同的方式接受数组和函数,但函数接受 2输入——一个累加器和一个当前值。

因此,如果函数的下一个输出始终是 .concat到累加器上,那么 reduce()可以像 map()一样使用。然而,它更常用于减少数组的维数,因此要么取一个维数并返回一个值,要么平坦化二维数组等。

让我们一个一个来看看这两个。

地图

Map 接受一个回调函数,并针对数组中的每个元素运行它,但是 makes it unique is it 根据现有数组 生成一个新数组.

var arr = [1, 2, 3];


var mapped = arr.map(function(elem) {
return elem * 10;
})


console.log(mapped); // it genrate new array

Reduce

数组对象的归约方法用于 将数组减少到一个值

var arr = [1, 2, 3];


var sum = arr.reduce(function(sum, elem){
return sum + elem;
})


console.log(sum) // reduce the array to one single value

Source

mapreduce都将数组和您定义的函数作为输入。它们在某种程度上是互补的: map不能为多个元素的数组返回单个元素,而 reduce总是返回您最终更改的累加器。

map

使用 map迭代元素,并为每个元素返回所需的元素。

例如,如果你有一个数字数组并且想得到它们的平方,你可以这样做:

// A function which calculates the square
const square = x => x * x


// Use `map` to get the square of each number
console.log([1, 2, 3, 4, 5].map(square))

reduce

使用数组作为输入,您可以根据获取 accumulatorcurrent_element参数的回调函数(第一个参数)获取单个元素(比如 Object、 Number 或另一个 Array) :

const numbers = [1, 2, 3, 4, 5]


// Calculate the sum
console.log(numbers.reduce(function (acc, current) {
return acc + current
}, 0)) // < Start with 0


// Calculate the product
console.log(numbers.reduce(function (acc, current) {
return acc * current
}, 1)) // < Start with 1


当你可以用两者做同样的事情时,你应该选择哪一个?试着想象一下代码是什么样子的。对于提供的示例,您可以使用 reduce计算正方形数组,如上所述:

// Using reduce
[1, 2, 3, 4, 5].reduce(function (acc, current) {
acc.push(current*current);
return acc;
}, [])


// Using map
[1, 2, 3, 4, 5].map(x => x * x)

现在,看看这些,显然第二个实现看起来更好,而且更短。通常你会选择更清洁的解决方案,在这种情况下是 map。当然,你可以用 reduce来做,但简而言之,想想哪个会更短,最终会更好。

要理解 map、 filter 和 reduce 之间的区别,请记住:

  1. 所有这三个方法都应用于 array ,因此无论何时您想对数组执行任何操作,都将使用这些方法。
  2. 所有这三种方法都遵循函数方法,因此 原始数组保持不变。原始数组不会改变,而是返回一个新的数组/值。
  3. Map和原始数组中的 返回一个元素数目相等的新数组一样。因此,如果原始数组有5个元素,则返回的数组也将有5个元素。每当我们想对数组中的每个单独元素进行一些更改时,都会使用此方法。您可以记住,ann 数组的每个元素都被映射到输出数组中的某个新值,因此名称为 map 例如,

var originalArr = [1,2,3,4]
//[1,2,3,4]
var squaredArr = originalArr.map(function(elem){
return Math.pow(elem,2);
});
//[1,4,9,16]

  1. Filter 返回一个元素数等于或少于元素数的新数组 than the original array. It returns those elements in the array which have passed some condition. This method is used when we want to apply a filter on the original array therefore the name filter. For eg,

var originalArr = [1,2,3,4]
//[1,2,3,4]
var evenArr = originalArr.filter(function(elem){
return elem%2==0;
})
//[2,4]

  1. 与 map/filter 不同,Reduce返回单个值。因此,每当我们想要对数组的所有元素运行操作,但是想要使用所有元素的单个输出时,我们使用 reduce。您可以记住一个数组的输出减少到一个值,因此名称为 reduce。例如,

var originalArr = [1,2,3,4]
//[1,2,3,4]
var sum = originalArr.reduce(function(total,elem){
return total+elem;
},0)
//10

我认为这张图片将回答你关于这些 高阶函数之间的差异

enter image description here

我认为这个问题是一个非常好的问题,我不能不同意这些答案,但是我有一种感觉,我们完全没有抓住重点。

更抽象地思考 mapreduce可以为我们提供许多非常好的见解。

答案分为三部分:

  • 定义和决定 map 和 reduce (7分钟)
  • 故意使用 reduce (8分钟)
  • 桥接地图和减少与传感器(5分钟)

映射或缩小

共同特征

map and reduce are implemented in a meaningful and consistent way on a wide range of objects which are not necessarily collections.

它们返回一个对周围的算法有用的值,并且它们只关心这个值。

它们的主要作用是传达关于结构转换或保存的意图。

结构

我所说的“结构”是指一组概念属性,它们描述抽象对象的特征,如无序列表或2D 矩阵,以及它们在数据结构中的具体化。

请注意,这两者之间可能存在断开:

  • 一个无序列表可以存储为一个数组,该数组具有由索引键进行排序的概念;
  • 一个2D 矩阵可以存储为 TypedArray,它缺乏维度(或嵌套)的概念。

map

map是一个严格的保持结构的转换。

在其他类型的对象上实现它是有用的,以掌握它的语义价值:

class A {
constructor (value) {
this.value = value
}


map (f) {
return new A(f(this.value))
}
}


new A(5).map(x => x * 2); // A { value: 10 }

实现 map的对象可以有各种各样的行为,但是它们总是返回与开始时相同的对象类型,同时使用提供的回调函数转换值。

Array.map返回与原始数组相同长度和相同顺序的数组。

On the callback arity

因为它保留了结构,所以 map被视为一种安全的操作,但是并不是每个回调都是相等的。

使用一元回调: map(x => f(x)),数组的每个值与其他值的存在完全无关。

另一方面,使用其他两个参数会引入耦合,这可能与原始结构不同。


想象一下在下面的数组中移除或重新排序第二项: 在地图之前或之后做都不会产生相同的结果。

与数组大小耦合:

[6, 3, 12].map((x, _, a) => x/a.length);
// [2, 1, 4]

配合订购:

['foo', 'bar', 'baz'].map((x, i) => [i, x]);
// [[0, 'foo'], [1, 'bar'], [2, 'baz']]

结合一个特定的价值:

[1, 5, 3].map((x, _, a) => x/Math.max(...a));
//[ 0.2, 1, 0.6]

与邻居联手:

const smooth = (x, i, a) => {
const prev = a[i - 1] ?? x;
const next = a[i + 1] ?? x;
const average = (prev + x + next) / 3;
return Math.round((x + average) / 2);
};


[1, 10, 50, 35, 40, 1].map(smoothh);
// [ 3, 15, 41, 38, 33, 8 ] 

我建议在调用站点上明确说明是否使用了这些参数。

const transfrom = (x, i) => x * i;


❌ array.map(transfrom);
⭕ array.map((x, i) => transfrom(x, i));

map中使用可变函数时,这样做还有其他好处。

❌ ["1", "2", "3"].map(parseInt);
// [1, NaN, NaN]
⭕ ["1", "2", "3"].map(x => parseInt(x));
// [1, 2, 3]

减少

reduce设置一个与其周围结构无关的值。

同样,让我们在一个更简单的对象上实现它:

class A {
constructor (value) {
this.value = value
}


reduce (f, init) {
return init !== undefined
? f(init, this.value)
: this.value
}
}


new A(5).reduce(); // 5


const concat = (a, b) => a.concat(b);
new A(5).reduce(concat, []); // [ 5 ]

无论是将该值保留下来,还是将其放回其他位置,reduce的输出都可以是任意形状。它与 map完全相反。

数组的含义

数组可以包含多个或零个值,这会产生两个需求,有时这两个需求是相互冲突的。

需要结合

How can we return multiple values with no structure around them?

这是不可能的,为了只返回一个值,我们有两个选择:

  • 将价值概括为一个价值;
  • 将值移动到不同的结构中。

Doesn't it make more sense now?

需要初始化

如果没有返回值怎么办?

如果 reduce返回一个假值,那么就没有办法知道源数组是否为空或者它是否包含这个假值,所以除非我们提供一个初始值,否则 reduce必须抛出。

减速器的真正用途

在下面的代码片段中,您应该能够猜到减速器 f是做什么的:

[a].reduce(f);
[].reduce(f, a);

没有。

这是一个简单的例子: a是我们想要返回的单个值,因此不需要 f

顺便说一下,这就是我们之前没有在 A类中强制使用 reduce 的原因: 因为它只包含一个值。在数组上是强制性的,因为数组可以包含多个值。

因为只有当你有2个或者更多的值时才会调用 reduce,所以说它的唯一目的就是把它们合并起来只是一步之遥。

On transforming values

对于可变长度的数组,期望约简器转换值是危险的,因为正如我们所发现的,它可能不会被调用。

当你需要转换值和改变形状时,我鼓励你在 reduce之前先转换 map

无论如何,为了可读性,将这两个关注点分开是一个好主意。

什么时候不用还原剂

因为 reduce是实现结构转换的通用工具,所以我建议您在需要返回数组时避免使用它,如果存在另一个更集中的方法,该方法可以满足您的需要。

具体来说,如果您在使用 map中的嵌套数组时遇到困难,请在使用 reduce之前考虑 flatMapflat

在减少的核心

递归二元运算

在数组上实现 reduce引入了这个反馈回路,其中约简器的第一个参数是前一次迭代的返回值。

不用说,它看起来一点也不像 map的回调。

我们可以像这样递归地实现 Array.reduce:

const reduce = (f, acc, [current, ...rest]) =>
rest.length == 0
? f(acc, current)
: reduce(f, f(acc, current), rest)

这突出显示了简化程序 f的二进制特性,以及它的返回值在下一次迭代中如何成为新的 acc

我让你说服自己以下是真的:

reduce(f, a, [b, c, d])


// is equivalent to
f(f(f(a, b), c), d)


// or if you squint a little
((a ❋ b) ❋ c) ❋ d

This should seem familiar: you know arithmetic operations obey rules such as "associativity" or "commutativity". What I want to convey here is that the same kind of rules apply.

reduce可能会去掉周围的结构,这些数值在转换的时候仍然被绑定在一个代数结构中。

还原代数

代数结构远远超出了这个答案的范围,所以我将只触及它们是如何相关的。

((a ❋ b) ❋ c) ❋ d

Looking at the expression above, it is self-evident that there is a constraint that ties all the values together : must know how to combine them the same way + must know how to combine 1 + 2 and just as importantly (1 + 2) + 3.

最脆弱的安全结构

One way to ensure this is to enforce that these values belong to a same set on which the reducer is an "internal" or "closed" binary operation, that is to say: combining any two values from this set with the reducer produces a value which belongs to the same set.

在抽象代数中这被称为 岩浆。你也可以查找 半群,这是更多的谈论,是相同的事情与结合(没有大括号需要) ,虽然 reduce不关心。

更不安全

生活在岩浆中并不是绝对必要的: 我们可以想象这样一种情况: 可以结合 ab,但不能结合 cb

An example of this is function composition. One of the following functions returns a string, which constrains the order in which you can combine them:

const a = x => x * 2;
const b = x => x ** 2;
const c = x => x + ' !';


// (a ∘ b) ∘ c
const abc = x => c(b(a(x)));
abc(5); // "100 !"


// (a ∘ c) ∘ b
const acb = x => b(c(a(x)));
acb(5); // NaN

像许多二进制运算一样,复合函数可以用作减速器。

知道我们是否处于重新排序或从数组中删除元素可能导致 reduce中断的情况是有价值的。

所以,岩浆: 不是绝对必要的,但是非常重要。

初始值呢

假设我们想通过引入一个初始值来防止在数组为空时抛出异常:

array.reduce(f, init)


// which is really the same as doing
[init, ...array].reduce(f)


// or
((init ❋ a) ❋ b) ❋ c...

We now have an additional value. No problem.

"No problem"!? We said the purpose of the reducer was to combine the array values, but init is not a 没错 value: it was forcefully introduced by ourselves, it should not affect the result of reduce.

问题是:

我们应该选择哪个 init使得 f(init, a)init ❋ a返回 a

我们需要一个初始值,它的作用就像它不存在一样。我们需要一个中立的元素(或“身份”)。

你可以查找 单位岩浆或者 幺半群(和结合性一样) ,这些词都是指含有中性元素的岩浆。

一些中性元素

你已经知道一些中性元素了

numbers.reduce((a, b) => a + b, 0)


numbers.reduce((a, b) => a * b, 1)


booleans.reduce((a, b) => a && b, true)


strings.reduce((a, b) => a.concat(b), "")


arrays.reduce((a, b) => a.concat(b), [])


vec2s.reduce(([u,v], [x,y]) => [u+x,v+y], [0,0])


mat2s.reduce(dot, [[1,0],[0,1]])

您可以对多种抽象重复此模式。注意,中性元素和计算不需要如此简单(极端的例子)。

中立元素困境

我们必须接受这样一个事实,即某些减少只对非空数组是可能的,并且添加差劲的初始化程序并不能解决问题。

Some examples of reductions gone wrong:

只是部分中立
numbers.reduce((a, b) => b - a, 0)


// does not work
numbers.reduce((a, b) => a - b, 0)

b中减去 0返回 b,但从 0中减去 b返回 -b。 我们说只有“权利认同”是真实的。

Not every non-commutative operation lack a symmetrical neutral element but it's a good sign.

超出范围了
const min = (a, b) => a < b ? a : b;


// Do you really want to return Infinity?
numbers.reduce(min, Infinity)

对于非空数组,Infinity是唯一不改变 reduce输出的初始值,但是我们不太可能希望它真的出现在我们的程序中。

中性元素不是我们为了方便而添加的 Joker 值。它必须是一个允许的值,否则它不能完成任何事情。

胡说八道

下面的减少依赖于位置,但是添加一个初始化器会自然地将第一个元素移动到第二个位置,这需要扰乱减少器中的索引来维持行为。

const first = (a, b, i) => !i ? b : a;
things.reduce(first, null);
const camelCase = (a, b, i) =>  a + (
!i ? b : b[0].toUpperCase() + b.slice(1)
);
words.reduce(camelCase, '');

接受数组不能为空这一事实并简化 reducers 的定义会干净得多。

此外,首字母值是退化的:

  • null不是空数组的第一个元素。

  • 空字符串绝不是有效的标识符。

没有办法用初始值来保持“第一性”的概念。

conclusion

代数结构可以帮助我们更系统地思考我们的程序。知道哪一个我们正在处理可以准确地预测我们可以期望从 reduce,所以我只能建议你查找他们。

更进一步

我们已经看到了 mapreduce在结构上是如何如此不同,但它们并不像是两个孤立的东西。

我们可以用 reduce来表示 map,因为总是可以重新构建我们开始时使用的相同结构。

const map = f => (acc, x) =>
acc.concat(f(x))
;


const double = x => x * 2;
[1, 2, 3].reduce(map(double), []) // [2, 4, 6]

再进一步推进,就产生了诸如传感器之类的巧妙技巧。

我不会详细介绍它们,但是我希望你们注意到一些事情,它们将重复我们之前所说的。

传感器

首先让我们看看我们要解决什么问题

[1, 2, 3, 4].filter(x => x % 2 == 0)
.map(x => x ** 2)
.reduce((a, b) => a + b)
// 20

我们将迭代3次并创建2个中间数据结构。此代码是声明性的,但效率不高。传感器试图调和这两者。

首先介绍一些使用 reduce组合函数的实用工具,因为我们不打算使用方法链接:

const composition = (f, g) => x => f(g(x));
const identity = x => x;


const compose = (...functions) =>
functions.reduce(composition, identity)
;


// compose(a, b, c) is the same as x => a(b(c(x)))

现在注意 mapfilter波纹管的实现。我们传入这个 reducer函数,而不是直接连接。

const map = f => reducer => (acc, x) =>
reducer(acc, f(x))
;


const filter = f => reducer => (acc, x) =>
f(x) ? reducer(acc, x) : acc
;

仔细看看这个:
reducer => (acc, x) => [...]
在应用了回调函数 f之后,我们只剩下一个函数,它接受一个 reduce 作为输入并返回一个 reduce。

这些对称函数是我们传递给 compose的:

const pipeline = compose(
filter(x => x % 2 == 0),
map(x => x ** 2)
);

请记住,compose是用 reduce实现的: 我们的 composition函数是由对称函数组合而成的。

这个操作的输出是一个相同形状的函数: 期望一个减速器并返回一个减速器的东西,这意味着

  • 我们有岩浆,只要它们有这个形状,我们就可以继续构成变形。
  • 我们可以使用这个链,方法是将结果函数应用于一个 reduce,它将返回一个可以与 reduce一起使用的 reduce

如果你需要说服力的话,我让你把整件事扩大了。如果这样做,您将注意到转换将方便地从左到右应用,这与 compose的方向相反。

好吧,我们用这个怪胎:

const add = (a, b) => a + b;


const reducer = pipeline(add);


const identity = 0;


[1, 2, 3, 4].reduce(reducer, identity); // 20

我们已经将各种操作(如 mapfilterreduce)组合成一个单独的 reduce,只迭代一次,没有中间数据结构。

这可不是小成就!而且,仅仅根据语法的简洁性在 mapreduce之间做出选择是无法得出这种方案的。

Also notice that we have full control over the initial value and the final reducer. We used 0 and add, but we could have used [] and concat (more realistically push performance-wise) or any other data-structure for which we can implement a concat-like operation.

Map 函数对每个元素执行一个给定的函数,但 reduce 函数将数组减少到一个值。我举两个例子:

// map function
var arr = [1, 2, 3, 4];
var mappedArr = arr.map((element) => { // [10, 20, 30, 40]
return element * 10;


})
// reduce function
var arr2 = [1, 2, 3, 4]
var sumOfArr2 = arr2.reduce((total, element) => { // 10
return total + element;


})

的确,reduce将一个数组减少为一个值,但是因为我们可以将一个对象作为 initialValue传递,所以我们可以在它的基础上构建一个比我们开始时更复杂的对象,比如这个例子,我们根据一些条件对项进行分组。因此,术语“减少”对于 reduce的能力可能有轻微的误导,认为它必然减少信息可能是错误的,因为它也可以添加信息。

let a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let b = a.reduce((prev, curr) => {
if (!prev["divisibleBy2"]) {
prev["divisibleBy2"] = []
}
if (curr % 2 === 0) {
prev["divisibleBy2"].push(curr)
}
if (!prev["divisibleBy3"]) {
prev["divisibleBy3"] = []
}
if (curr % 3 === 0) {
prev["divisibleBy3"].push(curr)
}
if (!prev["divisibleBy5"]) {
prev["divisibleBy5"] = []
}
if (curr % 5 === 0) {
prev["divisibleBy5"].push(curr)
}


return prev
}, {})
console.log(b)