给定四个包含整数的数组列表A、B、C、D,计算多有多少个元素(i, j, k, l), 使得A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的A、B、C、D具有相同的长度N,且0<=N<=500, 所有的整数范围在到 - 1之间,最终结果不会超过 - 1。
输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]
输出:
[0, 0, 0, 1]
[1, 1, 0, 0]
解释:
两个元组如下:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0
该题涉及到四个数组的组合,如果直接按4个数组组合,一共有种组合,事件复杂度为O(), 可以想象性能会非常低。简单粗暴的代码:
/**
* 试试直接用组合的方式,测试性能
* @param {number[]} nums1
* @param {number[]} nums2
* @param {number[]} nums3
* @param {number[]} nums4
* @return {number}
*/
var fourSumCount = function(nums1, nums2, nums3, nums4) {
const result = [], len = nums1.length
const startTIme = Date.now()
for (let i = 0; i < len; i++) {
for (let j = 0; j < len; j++) {
for (let k = 0; k < len; k++) {
for (let l = 0; l < len; l++) {
if (nums1[i] + nums2[j] + nums3[k] + nums4[l] === 0) {
result.push([i, j, k, l])
}
}
}
}
}
const endTime = Date.now()
console.log(`execut time: ${endTime - startTIme}ms`)
return result
};
function generateArr(start, diff, len) {
const arr = []
for (let index = 0; index < len; index++) {
arr.push(start + diff * (index + 1))
arr.push(start - diff * (index + 1))
}
return arr
}
const A = generateArr(1, 2, 150)
const B = generateArr(2, 2, 150)
const C = generateArr(0, 1, 150)
const D = generateArr(-1, 3, 150)
console.log(fourSumCount(A, B, C, D))
将A、B、C、D数组长度设置为300, 通过Node执行以上代码,耗时27s, 共计8205149个结果。 如果数组长度都设置为500, 代码将会遍历500(4),可想象时间复杂度太大了。
其他的办法要考虑如何降低组合维度,降低时间复杂度,要满足A[i] + B[j] + C[k] + D[l] = 0,可以转换为:
A[i] + B[j] + C[k] = -D[l] 或者
A[i] + B[j] = - (C[k] + D[l])
两个表达式的时间复杂度分别降为O()、O(), 表达式左右两边求和的结果可通过hash表存储,这样在二次匹配时能快速查询,由于需要建立hash表,空间复杂度将由原来的O(1)分别变为O()、O()。第二个表达式的实现代码如下:
/**
* 降低时间复杂度,A + B = -(C + D), 这种方式时间复杂度降为O(n²), 空间复杂度由O(1)变为O(n²), 临时结果用hash表存储。
* @param {number[]} nums1
* @param {number[]} nums2
* @param {number[]} nums3
* @param {number[]} nums4
* @return {number}
*/
var fourSumCount = function(nums1, nums2, nums3, nums4) {
const result = [], len = nums1.length
const startTime = Date.now()
function combination(arr1, arr2) {
const map = new Map()
for (let i = 0; i < len; i++) {
for (let j = 0; j < len; j++) {
// map存储格式: key: string, value: [[i1, j1], [i2, j2]], 不同组合之和可能相同
const sum = arr1[i] + arr2[j]
const value = map.get(sum) || []
value.push([i, j])
map.set(sum, value)
}
}
return map
}
const leftHash = combination(nums1, nums2)
const rightHash = combination(nums3, nums4)
const leftKeys = leftHash.keys()
const hashStartTime = new Date()
for (const key of leftKeys) {
if (rightHash.has(-key)) {
const leftValue = leftHash.get(key), rightValue = rightHash.get(-key)
for (i = 0; i < leftValue.length; i++) {
for (j =0; j < rightValue.length; j++) {
result.push(leftValue[i].concat(rightValue[j]))
}
}
}
}
const hashEndTime = new Date()
console.log(`execut hash iterate: ${hashEndTime - hashStartTime}ms`)
const endTime = Date.now()
console.log(`execut time: ${endTime - startTime}ms`)
return result
};
function generateArr(start, diff, len) {
const arr = []
for (let index = 0; index < len; index++) {
arr.push(start + diff * (index + 1))
arr.push(start - diff * (index + 1))
}
return arr
}
const A = generateArr(1, 2, 200)
const B = generateArr(2, 2, 200)
const C = generateArr(0, 1, 200)
const D = generateArr(-1, 3, 200)
console.log(fourSumCount(A, B, C, D))
数组长度为300情况下,耗时4086ms,共计8205149个结果。相对于第一种简单粗暴的方案,执行耗时减少了近85%。
以上代码抽离了两两组合函数combination,分别生成leftHash、rightHash两个hash表,从空间、时间复杂度考虑,后续的hash表遍历完全放到第二个两两组合遍历部分,这样不仅减少了代码再次遍历hash表的时间,也省去第二个hash表。优化后代码:
/**
* 降低时间复杂度,A + B = -(C + D), 这种方式时间复杂度降为O(n²), 空间复杂度由O(1)变为O(n²), 临时结果用hash表存储。
* @param {number[]} nums1
* @param {number[]} nums2
* @param {number[]} nums3
* @param {number[]} nums4
* @return {number}
*/
var fourSumCount = function(nums1, nums2, nums3, nums4) {
const result = [], len = nums1.length
const startTime = Date.now()
const map = new Map()
for (let i = 0; i < len; i++) {
for (let j = 0; j < len; j++) {
// map存储格式: key: string, value: [[i1, j1], [i2, j2]], 不同组合之和可能相同
const sum = nums1[i] + nums2[j]
const value = map.get(sum) || []
value.push([i, j])
map.set(sum, value)
}
}
for (let i = 0; i < len; i++) {
for (let j = 0; j < len; j++) {
const sum = -(nums3[i] + nums4[j])
if (map.has(sum)) {
const value = map.get(sum)
for (const arr of value) {
result.push(arr.concat([i, j]))
}
}
}
}
const endTime = Date.now()
console.log(`execut time: ${endTime - startTime}ms`)
return result
};
function generateArr(start, diff, len) {
const arr = []
for (let index = 0; index < len; index++) {
arr.push(start + diff * (index + 1))
arr.push(start - diff * (index + 1))
}
return arr
}
const A = generateArr(1, 2, 150)
const B = generateArr(2, 2, 150)
const C = generateArr(0, 1, 150)
const D = generateArr(-1, 3, 150)
console.log(fourSumCount(A, B, C, D))
执行时间比上一版平均减少100ms左右,空间上减少了一个hash表的存储。
解此类题的思路就是考虑如何降时间复杂度,该题是四个数组,如果数组变成N个该如何解决?类似二分法,直接把纬度降低到N/2,下一级可以考虑继续降纬度。