笛卡尔积指两个集合中所有可能的有序对组合,例如,集合 A = { 1 , 2 } 和 B = { a , b } ,笛卡尔积为: { ( 1 , a ) , ( 1 , b ) , ( 2 , a ) , ( 2 , b ) } 。
前端开发中,笛卡尔积常用于处理多维度组合场景,如商品规格组合,测试用例生成,动态路由生成,数据可视化网络等。
两个集合求笛卡尔积
如果只有两个集合,求笛卡尔积是很容易的,只需要两层遍历,就可以得到所有的组合,代码如下
function cartesianProduct(arr1, arr2) {
const res = []
arr1.forEach(v1 => {
arr2.forEach(v2 => {
res.push([v1, v2])
})
})
return res
}
console.log(cartesianProduct([1,2], ['a','b']));
// [ [ 1, 'a' ], [ 1, 'b' ], [ 2, 'a' ], [ 2, 'b' ] ]
多个集合
对于多个集合的情况,我们的思路是采用递归的思路,将问题简化为:假设后面的集合我们已经求解了笛卡尔积,只要将第一个集合,与后面的集合求解。代码如下
function cartesian(arrs) {
// 如果数组长度为 0 返回空数组
if (arrs.length === 0) {
return []
}
// 如果数组长度为 1,对于集合中每一个元素,生成一个数组
if (arrs.length === 1) {
return arrs[0].map(it => [it])
}
const result = []
// 对第一个以后的元素求笛卡尔积
const rest = cartesian(arrs.slice(1))
// 第一个元素,和上面的结果,求两个集合的笛卡尔积
arrs[0].forEach(v1 => {
const res = []
rest.forEach(v2 => {
res.push([v1, ...v2])
})
result.push(...res)
})
return result
}
console.log(cartesian([[1,2], ['a','b'], [true,false]]));
/*
[
[ 1, 'a', true ],
[ 1, 'a', false ],
[ 1, 'b', true ],
[ 1, 'b', false ],
[ 2, 'a', true ],
[ 2, 'a', false ],
[ 2, 'b', true ],
[ 2, 'b', false ]
]
*/
利用数组的方法简化代码
当我们已经理解了求解笛卡尔积的基本思路以后,可以利用 JS 数组自带的迭代和遍历方法,进行代码的简化。
flatMap 方法:其实就是结合了 flat 和 map,进行遍历和 flat 运算,在遍历的同时进行扁平化,可以简化上面的 forEach 部分的代码
function cartesian(arrs) {
if (arrs.length === 0) {
return []
}
if (arrs.length === 1) {
return arrs[0].map(it => [it])
}
const rest = cartesian(arrs.slice(1))
return arrs[0].flatMap(v1 => rest.map(v2 => [v1, ...v2]))
}
还可以利用数组的 reduce 方法进行迭代,就省去了递归的部分,最终精简版代码如下:
function cartesian(arrs) {
return arrs.reduce(
(acc, current) => current.flatMap(v1 => acc.map(v2 => [v1, ...v2])),
[[]]
)
}
reduce 方法传入两个参数,callback 和 initialValue
callback:迭代计算函数,第一个参数为上次计算的结果,第二个参数为当前计算的元素
initialValue:计算的初始值
reduce 方法会对每一个元素进行一次计算callback 计算,返回最终的计算结果。
商品 SKU 计算
了解了笛卡尔积计算的方法后,对于简单的 sku 计算应该已经没有问题了,但是对于比较复杂的情况,还是可以用到上面的思路的,例如有如下的商品,计算 sku
const goods = {
color: ['red', 'green', 'blue'],
size: ['small', 'middle', 'big'],
version: ['v1', 'v2', 'v3', 'v4'],
}
/*
[
{ color: 'red', size: 'small', version: 'v1' },
{ color: 'red', size: 'small', version: 'v2' },
{ color: 'red', size: 'small', version: 'v3' },
{ color: 'red', size: 'small', version: 'v4' },
{ color: 'red', size: 'middle', version: 'v1' },
{ color: 'red', size: 'middle', version: 'v2' },
{ color: 'red', size: 'middle', version: 'v3' },
{ color: 'red', size: 'middle', version: 'v4' },
{ color: 'red', size: 'big', version: 'v1' },
{ color: 'red', size: 'big', version: 'v2' },
{ color: 'red', size: 'big', version: 'v3' },
{ color: 'red', size: 'big', version: 'v4' },
{ color: 'green', size: 'small', version: 'v1' },
{ color: 'green', size: 'small', version: 'v2' },
{ color: 'green', size: 'small', version: 'v3' },
{ color: 'green', size: 'small', version: 'v4' },
{ color: 'green', size: 'middle', version: 'v1' },
{ color: 'green', size: 'middle', version: 'v2' },
{ color: 'green', size: 'middle', version: 'v3' },
{ color: 'green', size: 'middle', version: 'v4' },
{ color: 'green', size: 'big', version: 'v1' },
{ color: 'green', size: 'big', version: 'v2' },
{ color: 'green', size: 'big', version: 'v3' },
{ color: 'green', size: 'big', version: 'v4' },
{ color: 'blue', size: 'small', version: 'v1' },
{ color: 'blue', size: 'small', version: 'v2' },
{ color: 'blue', size: 'small', version: 'v3' },
{ color: 'blue', size: 'small', version: 'v4' },
{ color: 'blue', size: 'middle', version: 'v1' },
{ color: 'blue', size: 'middle', version: 'v2' },
{ color: 'blue', size: 'middle', version: 'v3' },
{ color: 'blue', size: 'middle', version: 'v4' },
{ color: 'blue', size: 'big', version: 'v1' },
{ color: 'blue', size: 'big', version: 'v2' },
{ color: 'blue', size: 'big', version: 'v3' },
{ color: 'blue', size: 'big', version: 'v4' }
]
*/
基本思路就是,对于每次遍历到的元素,将其key,value 累加到一条记录中。
还是利用上面的思想,先假设我们已经获得了 size 和 version 所有可能的情况,只要把 color 和其余情况组合,就变得简单了
// 假设已经都放入到 tmpResult 中了
const result = []
goods.color.forEach(val => {
tmpResult.forEach(tmp => {
result.push({...tmp, color: val})
})
})
所以我们还是基于这个思路去进行递归,唯一需要注意的就是,递归的函数中需要将 tmp 传下去,这样才可以继续追加属性,总体代码如下
function sku(goods) {
const keys = Object.keys(goods)
if (!keys.length) {
return
}
function combin(index, tmp) {
if (index >= keys.length) {
return [tmp]
}
const result = []
const key = keys[index]
const attrs = goods[key]
attrs.forEach(value => {
const obj = {...tmp, [key]: value}
const skus = combin(index + 1, obj)
result.push(...skus)
})
return result
}
return combin(0, {})
}
其中,index 标记遍历到哪个属性了,用于获取 key,combin 用于从 goods 读取当前 key 对应的所有属性值,并进行属性的追加,tmp 用于传递元素。