算法:盛最多水的容器

911 阅读1分钟

给你n个非负整数a1, a2, a3, ..., an,每个数代表坐标中的一个点(i, ai)。在坐标内画n条垂直线,垂直线i的两个端点分别为(i, ai)和(i, 0)。找出其中的两条线,使得它们与x轴共同构成的容器可以容纳最多的水。

示例1
question_11.jpeg

输入:[1,8,6,2,5,4,8,3,7]输出:49解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。
在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49

示例2

输入:[1,1]输出:2

示例3

输入:[4,3,2,1,4]
输出:16

提示

  • n = 数组长度
  • 2 <= n <= 10510^5
  • 0 <= 数组[i] <= 10410^4

题目主要考察数组的遍历组合,很容易想到的方法是数组项i, j两两组合求其面积,并逐步遍历找出最大的面积值。其中要注意的是算面积的y值是取两项的较小值。代码v1:

/**
 * 将数组项i,j两两组合,求其面积,并逐步遍历,最大面积值即为容器最大盛水量
 * @param {number[]} height
 * @return {number}
 */
 var maxArea = function(height) {
     let maxArea = 0
    for (let i = 0; i < height.length - 1; i++) {
        for (let j = i + 1; j < height.length; j++) {
            // height[i], height[j]取交小的值,盛水量按最小挡板算
            const x = j - i, y = Math.min(height[i], height[j])
            const tempArea = x * y
            maxArea = Math.max(maxArea, tempArea)
        }
    }

    return maxArea
}

console.log(maxArea([1,8,6,2,5,4,8,3,7]))

v1版本时间复杂度为O(N²),考虑如何减少遍历次数,可使用将第二层遍历长度由n变为n/2,代码v2:

/**
 * 使用双向指针,减少第二循环的遍历次数,时间复杂度变为O(N²/2)
 * @param {number[]} height
 * @return {number}
 */
 var maxArea = function(height) {
     const n = height.length
     let maxArea = 0
    for (let i = 0; i < n - 1; i++) {
        for (let j = i + 1, k = n - 1; j <= k; j++, k--) {
            const area1 = (j - i) * Math.min(height[i], height[j])
            const area2 = (k - i) * Math.min(height[i], height[k])
            maxArea = Math.max(maxArea, area1, area2)
        }
    }

    return maxArea
}

console.log(maxArea([2,3,4,5,18,17,6]))

v2版本时间复杂度降为O(N²/2),基于此版本联想是否可以两层循环都采用方双指针式,这样时间复杂度可再降低一半O(N²/4), 代码v3:

/**
 * 两次循环都采用双向指针,时间复杂度降低为O(N²/4)
 * @param {number[]} height
 * @return {number}
 */
 var maxArea = function(height) {
     const n = height.length
     let maxArea = 0
    for (let i = 0, j = n - 2; i <= j; i++, j--) {
        for (let k = i + 1, l = n - 1; k <= l; k++, l--) {
            const area1 = (k - i) * Math.min(height[i], height[k])
            const area2 = (l - i) * Math.min(height[l], height[i])

            maxArea = Math.max(maxArea, area1, area2)
            if (j < l) {
                const area3 = (l - j) * Math.min(height[l], height[j])
                maxArea = Math.max(maxArea, area3)
            }
        }
    }

    return maxArea
}

console.log(maxArea([2,3,4,5,18,17,6]))

v1,v2,v3版本其实都是采用暴力解法,只是相对减少了遍历次数,有没有只遍历一次来实现同样的效果?考虑在遍历的过程中丢弃掉不必要的比较。

可以使用一层循环的双向指针,从i = 0, j = n - 1两边开始向内遍历,假设水槽两边围成的面积为S(i, j), h[i]、h[j]分别为水槽两边的高度。

屏幕快照 2021-09-05 下午7.24.51.png

例如当前i =0、j = 8, S(0, 8)状态下h[0] > h[8],此时指针有两种移法。如果

  • 移动高的: 丢弃掉S(0, 1)、S(0, 2)、...、S(0, 7),但其中S(0, 6)的面积可能大于S(0, 8),所以此种移法不可行。
  • 移动矮的:丢弃掉S(1, 8)、S(2, 8),...,S(7, 8),其中肯定不会出现比S(0, 8)面积更大的,因为宽度j - i变小,但高度不会超过h[8]。所以此类移动法可有效减少不必要的遍历。

具体实现代码如下,时间复杂度降低为O(N), 空间复杂度为O(1)。

/**
 * 单层双指针循环法,由外向内遍历,找出面积的最大值
 * @param {number[]} height
 * @return {number}
 */
 var maxArea = function(height) {
     const n = height.length
     let maxArea = 0, i = 0, j = n - 1

     while(i < j) {
         maxArea = Math.max(maxArea, (j - i) * Math.min(height[i], height[j]))
         if (height[i] < height[j]) {
             i++
         } else {
             j--
         }
     }

    return maxArea
}

console.log(maxArea([2,3,4,5,18,17,6]))