前端笛卡尔积

640 阅读4分钟

笛卡尔积指两个集合中所有可能的有序对组合,例如,集合 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 用于传递元素。