前言
最近在搞一个航线管理系统,如图,需要根据起始点,中转点和终点绘制航线图。这就涉及到点与点之间的组合算法。用专业点的说法,就是多维数组之间的元素组合。就拿上面的航线图来说,我们可以抽象为下面的数组:
[
['香港','常州'],
['成都'],
['乌鲁木齐','敦煌']
]
通过一个简单的数学计算,可以得到共有2x1x2=4
条线。但是如何得到这具体四条线分别包含哪些节点呢?像这种问题,一般都会涉及到递归的算法。下面我们就来实现一下。
方案一:多维数组转换为二维数组进行计算
一、分析
直接计算多维(超过二维)数组会比较绕。我们可以将这个问题分解为多个二维数组进行叠加运算。拿上面的例子来说,可以先用['香港','常州']
和['成都']
进行组合,得到:
[
['香港','成都'],
['常州','成都']
]
然后再用这个临时数组与[‘乌鲁木齐’,‘敦煌’]组合,得到:
[
['香港','成都','乌鲁木齐'],
['常州','成都','敦煌'],
['香港','成都','乌鲁木齐'],
['常州','成都','敦煌']
]
整个的实现流程可以概括为下图:
因此,我们可以先这样实现:
二、简单的实现
const dataList=[
['香港','常州'],
['成都'],
['乌鲁木齐','敦煌']
]
/**
* 数组组合
* @param arr1 基础数组
* @param arr2 待组合数组
* @returns {*[]}
*/
const comb = (arr1, arr2) => {
let index = 0
const res = []
for (let a1 of arr1) {
for (let a2 of arr2) {
if (Array.isArray(a1)) {
res[index++] = [...a1, a2]
} else {
res[index++] = [a1, a2]
}
}
}
return res
}
const res1=comb(dataList[0],dataList[1])
console.log(res1) // [['香港','成都'],['常州','成都']]
至此,我们组合了起始点和中转点。那还剩下终点哇,我们就这样组合:
const res2=comb(comb(dataList[0],dataList[1]),dataList[2])
如果还有点,以此类推:
const res2=comb(comb(comb(dataList[0],dataList[1]),dataList[2]),dataList[3])...
但是我们不能直接这样用,如果中间点类型多了,那么集合从二维变成N维还这样写的话,会写死人。
仔细看一下求解模式,还是有点规律的,因为后面都是在不断地调用comb方法,递归嵌套。
三、优化实现
/**
* 多维数组组合算法
* @param arr1 基础数组
* @param arr2 待组合数组
* @param list 总数组
* @param depth 当前组合深度,可以看作组合时,指向待组合数组在总数组中的下标
* @returns {*[]|[]|*}
*/
const combWrapper = (arr1, arr2, list, depth = 0) => {
if (depth < 2) { // 前两个直接组合
return comb(arr1, arr2)
} else {
const res = comb(arr1, arr2)
if (depth < list.length) { // 收敛条件为深度不能超过总数组长度
return combWrapper(res, list[++depth - 1], list, depth)
} else {
return res
}
}
}
console.log(combWrapper(dataList[0], dataList[1], dataList, 2)) // 这里的深度从2也就是总数组中第三个元素开始组合
但是这样写的话,参数太多,我们还可以精简:
const _combWrapper = (list, depth = 2, arr1, arr2) => {
if (!arr1) {
arr1 = list[0]
}
if (!arr2) {
arr2 = list[1]
}
if (depth < 2) { // 前两个直接组合
return comb(arr1, arr2)
} else {
const res = comb(arr1, arr2)
if (depth < list.length) { // 收敛条件为深度不能超过总数组长度
return _combWrapper(list, ++depth, res, list[depth - 1])
} else {
return res
}
}
}
console.log(_combWrapper(dataList))
利用js方法参数不用写全的特性,可以只传入一个原始数组,就可以自动组合了。
!需要注意的是上面代码++的位置!
方案二:深度遍历算法
我们直接上代码看看:
const temp=[]
const results=[]
const comb=(arr,depth=0)=>{
const list=arr[depth]
for(let data of list){
temp[depth]=data
if(depth !== arr.length-1){
comb(arr,++depth)
}else{
results.push(temp)
}
}
}
comb(dataList)
console.log('res',results)
一、分析
首先,光从代码量上,这种方法就比前面的少很多。最外层有两个数组集合:temp和results。temp用于临时存储每个起始->中转->终点的线路。results存储的是最终的结果。temp被赋值时是根据depth的下标来的。depth会在每次递归的时候加1,直到递归完所有的数据深度。
与转为二维数组的方式对比的话,前者一开始的线路只有两个元素,然后后面递归的时候不断增加。而深度遍历的话,每次遍历完成都有最终深度长度的元素。但是上面的代码真的玩美吗?
二、异常剖析
首先,思路是没问题的。问题的关键在于js的变量的引用。
我们声明的temp对象相对comb方法是个全局变量,并且是个数组不是普通基础数据类型。根据depth来确定被赋值的元素的位置。但是我们在后面将这个depth自增了,会导致temp[depth]的指向的地址变了。再者,results永远都是在push这个temp对象。可以变相看作是一种强引用。这样得到结果,就是最终所有的result都是同一个最后的temp。针对第一个depth的问题,我们可以将++depth变为depth+1或者设置一个临时变量,对这个临时变量自增。对于后者的push,我们可以对temp进行深度拷贝。而js里面最简单明了的深度拷贝方式,就是利用JSON的序列化和反序列化进行操作:
const comb0 = (arr, depth = 0) => {
const currentArr = arr[depth]
currentArr.forEach(c => {
temp[depth] = c
if (depth !== arr.length - 1) {
comb0(arr, depth + 1)
// let a=depth
// comb0(arr, ++a)
} else {
res.push(JSON.parse(JSON.stringify(temp))) // 深度拷贝temp
}
})
}
方案三:利用js的数组的reduce方法实现
什么是reduce函数?
定义和用法
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
reduce() 可以作为一个高阶函数,用于函数的 compose。
注意: reduce() 对于空数组是不会执行回调函数的。
举个栗子
加入我们想统计一个数组里面的所有元素之和:
const list1 = [1, 2, 3, 4, 5, 6, 7]
const res__ = list1.reduce((pre, current) => {
return pre + current
})
console.log(res__) // 28
那这个又和我们的多维数组组合有啥联系呢?
reduce本身类似filte,map这些遍历方法。请注意,reduce的前两个参数。第一个prev是表示截至遍历到当前的累计结果。也就是你的return里面结果的一个集合。第二个则是当前遍历需要执行的对象。这样的话,我们可以改写一下方案一:
const comb = (arr1, arr2) => {
let index = 0
const res = []
for (let a1 of arr1) {
for (let a2 of arr2) {
if (Array.isArray(a1)) {
res[index++] = [...a1, a2]
} else {
res[index++] = [a1, a2]
}
}
}
return res
}
const doComb=(arr)=>{
return arr.reduce((prev,current)=>{
return comb(prev,current)
)
}
console.log(doComb(dataList))
是不是简洁了很多。