算法:四数相加

321 阅读1分钟

给定四个包含整数的数组列表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, 所有的整数范围在228-2^{28}2282^{28}- 1之间,最终结果不会超过2312^{31} - 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个数组组合,一共有N4N^4种组合,事件复杂度为O(N4N^4), 可以想象性能会非常低。简单粗暴的代码:

/**
 * 试试直接用组合的方式,测试性能
 * @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(N3N^3)、O(N2N^2), 表达式左右两边求和的结果可通过hash表存储,这样在二次匹配时能快速查询,由于需要建立hash表,空间复杂度将由原来的O(1)分别变为O(N3N^3)、O(N2N^2)。第二个表达式的实现代码如下:

/**
 * 降低时间复杂度,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,下一级可以考虑继续降纬度。