面试经典150题——盛最多水的容器(图解从本质看问题)

​"Hardships often prepare ordinary people for an extraordinary destiny." 

- C.S. Lewis

a man standing in the middle of a desert

1. 题目描述

2.  题目分析与解析

2.1 思路一——暴力求解

遇到问题最怕的就是没有思路,就好像人迷茫的时候最怕的就是一直迷茫,不知道怎么干那就先试试最笨的办法,先动起来,大不了就是多花点时间,等后面可以慢慢优化嘛。

暴力求解思路:遍历数组的每一个位置,以它为起始位置,与自身以外的所有元素进行匹配,保留能存的水中最大的值。计算出所有位置的的最大值后,取出所有位置的值中最大的值,可以看下图:

第一次从第一个位置从前向后寻找它(红色)与其它(绿色)的积水量记为黄色

第二次从第二个位置从前向后寻找:

以此轮询,就可以算出每一次的最大积水量:

这种算法显而易见复杂度为O(n^2),效率并不高,但是起码我们能够解决问题,在能够解决问题的基础上,我们再看怎么优化。代码如下:

    // 方法1:暴力求解public int maxArea(int[] height) {int ret = 0;for (int i = 0; i < height.length; i++) {for (int j = 0; j < height.length; j++) {// 计算面积// 长:j - i// 宽:Math.min(height[i], height[j])// 面积:(j - i) * Math.min(height[i], height[j])if (i != j) {ret = Math.max(ret, (j - i) * Math.min(height[i], height[j]));}}}return ret;}

下图是我们采用上述方式的搜索空间:

可以很明显的看到,这个是对称的,那就可以在我们在计算时内层循环可以从当前的外层循环开始遍历到结尾。如下:

    // 方法1-1:暴力求解public int maxArea1_1(int[] height) {int ret = 0;for (int i = 0; i < height.length; i++) {for (int j = i + 1; j < height.length; j++) {// 计算面积// 长:j - i// 宽:Math.min(height[i], height[j])// 面积:(j - i) * Math.min(height[i], height[j])ret = Math.max(ret, (j - i) * Math.min(height[i], height[j]));}}return ret;}

就将搜索空间变成如下格式:

但是还是会超时的。所有我们现在看怎么用更巧妙的办法。

2.2 思路二——双指针

首先我们假设有两个指针:leftright,left指向数组的第一个元素,right指向数组的最后一个元素,这样是不是就代表了我们的两个指针包含了全部的可能储水的搜索范围(因为left与right可以移动),即left=0right=height.length() - 1。如下:

那我们的目的就是在这个范围内怎么最快的找到题目要求的最大储水量,也就是找到对应的left与right

因为根据上式可以看出储水量仅仅受两个变量left与right决定。

  • Tips:对于我们需要求出的解,不妨列出公式查看决定它的因素有哪些,假如我们要求A=B与C的某种运算,就尝试找到B与C的决定因素,比如B=D*F,C=G+H,就这样递归找到问题本质的决定因素,那么我们就可以知道A本质上是由D,F,G,H决定的。然后就可以对D,F,G,H进一步分析。

因此我们就需要找到合适的right与left,而left的初始位置为0,决定了它移动的范围为[0, height.length() - 1],而right的初始位置为height.length() - 1,决定了它移动的范围为[height.length() - 1, 0]。而在移动的过程中如果left == right那么肯定是表示结束遍历了,那么走过的步骤里肯定有最大储水量的解(因为我们是在所有的解空间里查找的,当left等于right时解空间为空集,说明我们已经走过了整个解空间)。


现在我们需要思考的就是left与right 在每一步怎么动,它的运动逻辑是什么?

对于这个问题,我们还是按照刚才的步骤,先找出left和right的解空间:

  • 对于left——两种状态

    • 保持不动

    • +1 变大

  • 对于right——两种状态

    • 保持不动

    • -1 变小

保持不动自然不用说,它没有再进一步探讨的必要因为对于结果的影响不变。而对于+1-1操作,就需要探讨它对于结果的影响。

对于left的+1操作,还存在两种可能的状态:

  • +1后left指向的值height[left]变大

  • +1后left指向的值height[left]变小

对于right的-1操作,还存在两种可能的状态:

  • -1后left指向的值height[left]变大

  • -1后left指向的值height[left]变小

现在再看一眼决定结果的公式:

我们可以发现有一个Min函数的影响,虽然本质上还是left和right对结果的影响,但是因为left与right值决定了height[left]和height[right]的值,并且height[left]和height[right]的值决定了Min函数后的值,而最终的结果是来自于Min(height[left],  height[right])和 (right - left) 。文字描述可能稍显空洞,请看下图:

根据上图我们可以知道我们现在要倒推查看对于left的+1操作和对于right的-1操作对height和min以及right-left的影响。对right-left的影响很显而易见:

  • left+1整个值right-left减小1

  • right-1整个值right-left减小1

所有我们主要看对于height和min逻辑的影响

  1. 对于left而言,+1操作可以分为两种大情况

  • height[left+1] > height[left]

    • A:如果原先height[left] > height[right]那么移动后对于储水量而言肯定变小,因为储水量变化等于移动后的储水量减去移动前的储水量,公式如下:

    • B:如果原先height[left] <= height[right]那么移动后对于储水量而言可能变大可能变小,是不能确定大小的

  • height[left+1] <= height[left]

    • C:如果原先height[left] > height[right]那么移动后对于储水量而言是肯定变小

    • D:如果原先height[left] <= height[right]那么移动后对于储水量而言是肯定变小

  1. 对于right而言,+1操作也可以分为两种大情况

  • height[right-1] > height[right]

    • E:如果原先height[right] > height[left]那么移动后对于储水量而言肯定变小

    • F:如果原先height[right] <= height[left]那么移动后对于储水量而言可能变大可能变小

  • height[right-1] <= height[right]

    • G:如果原先height[right] > height[left]那么移动后对于储水量而言肯定变小

    • H:如果原先height[right] <= height[left]那么移动后结对于储水量而言肯定变小

由于我们的目的是找到最大的解,也就是说我们肯定不想通过移动让值变小。而经过上面的论证,我们可以发现是有一定的规律的,对于height[left]与height[right],

  • 如果我们移动大的一边,也就是对应上述{A, C, E, G},结果肯定是变小的

  • 而如果移动小(或相等)的一边,也就是对应上述 {B, D, F, H}:

    • 如果要移动的位置大于当前位置,对应 {B, F}, 那得到的结果可能变大可能变小。

    • 如果要移动的位置小于当前位置,对应 {F, H}, 那得到的结果肯定变小。

所以分析到这里我们就可以知道两个指针left与right的运动规律了。如果我们想要的到最大的结果,那么就得让指针按照上述{B,F}的方向走,也就是必须得移动小的一边,虽然说包含了{B, D, F, H},但起码有变大的可能。。这样当两个指针从两头走到碰面,就可以保证经历了所有的可能变大的结果,其中最大的值就是找到的最大储水量。


所以解题思路为:

  1. 左右指针指向数组头尾

  2. 移动指向的值较小的指针,计算并保存最大值

  3. 得到结果

看到这里肯定有些人还有疑惑,为什么这样是正确的?凭什么你说这样就能考虑了所有的情况?

解释:开始的left与right指向两端,如之前给过的图一样:

它是能包含所有解的,因为right和left的移动范围是包含了整个解空间的,而我们的上述分析只需要走 {B, D, F, G}方向的路径,是相当于用数学的方法进行了走向的优化,减少了不必要的行程。而最终当right于left碰面就退出,那时候已经走到了边界,继续再向下搜索是会得到重复的结果的,可以执行,但没必要。以题目中的测试用例为例子,其搜索过程是如下的:

如果继续往下搜索,那么会得到如下:

最终的搜索路线为:

可以看到无非就是重复了一次right > left的路线。


现在着重讲一下为什么我这样走就能找到最优解

首先,假设我们现在在初始位置:

根据前面介绍的公式,我们可以知道,此时决定储水量的看left和right指向的较小的值,因为此时height[left]=1,height[right]=7,较小的值为height[left]=1,理论上是需要将left+1。而如果我们在这个位置左移right,也就是不断将right - 1,那么储水量肯定是不会大于当前值的。因为 Min(height[left], height[right])中较小的值肯定是小于等于目前的值,换句话说,就是Min(height[left],height[right])中现在最小的是height[left],如果不断变小right,则变化的值为height[right]。而这个变化的height[right]

  • 要么大于height[left],则Min(height[left],height[right])仍然为height[left]

  • 要么等于或者小于height[left],则Min(height[left],height[right])等于height[right],仍然小于等于height[left]

所以储水量肯定不会大于当前值,所以就可以排除如下图黄色部分:

当走到第二个位置同理,因为height[left] > height[right],所以需要动right,将right - 1。如果我们假设让left移动,就是让left不断加1,那么储水量肯定也是不会大于当前值的。同上,因为 Min(height[left], height[right])中较小的值肯定是小于等于目前的值,换句话说,就是Min(height[left],height[right])中现在最小的是height[right],如果不断变大left,则变化的值为height[left]。而这个变化的height[left]

  • 要么大于height[right],则Min(height[left],height[right])仍然为height[right]

  • 要么等于或者小于height[right],则Min(height[left],height[right])等于height[left],仍然小于等于height[right]的

得到的储水量肯定不会大于当前值,所以就可以排除如下图紫色部分:

同理按照上述步骤,后续过程如下,紫色部分为每一次的排除部分:

总结

其实我们每一步的搜寻都是尝试将部分可能解基于我们前面推理的数学公式排除掉了,从开始的暴力枚举到排除一半可行解,再到双指针每次搜寻排除某一行或者某一列的解,这个过程其实就是不断优化的过程。

3. 代码实现

3.1 思路一——暴力求解

3.2 思路二——双指针

4. 运行结果

方法一

会报超时

方法二

5. 相关复杂度分析

方法一

暴力枚举所有可行解,需要走所有的解空间,所以时间复杂度为O(n^2),空间复杂度为O(1)

方法二

  • 时间复杂度:O(N),双指针总计最多遍历整个数组一次。

  • 空间复杂度:O(1),只需要额外的常数级别的空间。

个人公众号:推出最新高质量文章:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/676106.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Lua 教程

Lua 教程 (今天又又又开新坑啦) Lua 教程 手册简介 Lua 是一种轻量小巧的脚本语言&#xff0c;用标准C语言编写并以源代码形式开放。 手册说明 Lua是什么? Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学&#xff08;Pontifical Catholic University of Rio de …

Java stream 流的基本使用

Java stream 的基本使用 package com.zhong.streamdemo.usestreamdemo;import jdk.jfr.DataAmount; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;import java.util.ArrayList; import java.util.Comparator; import java.util.Li…

【ETOJ P1050】【模板】差分 题解(数学+差分+前缀和)

题目描述 给定一个长度为 n n n 的数组 a a a。 有 m m m 次修改&#xff0c;每次修改让数组 a a a 在区间 [ l , r ] [l,r] [l,r] 增加 x x x。 在修改完成后有 q q q 次询问&#xff0c;每次询问数组在区间 [ l , r ] [l,r] [l,r] 的和。 输入格式 第一行一个整…

源码梳理(3)MybatisPlus启动流程

文章目录 1&#xff0c;MybatisPlus的使用示例2&#xff0c;BaseMapper方法的执行2,1 MybatisMapperProxy代理对象2.2 InvocationHandler接口&#xff08;JDK动态代理&#xff09;2.3 MapperMethodInvoker接口2.4 MybatisMapperMethod 3&#xff0c;SqlSession的执行流程3.1 Sq…

js基础(1)

操作数组 数组.push() 将一个或多个元素添加到数组末尾&#xff0c;返回数组新长度 数组.unshift() 将一个或多个元素添加到数组末尾&#xff0c;返回数组新长度 数组.pop() 删除最后一个元素&#xff0c;返回该元素的值 更灵活的删除方法&#xff0c;删除指定元素 数组.spli…

【MySQL题】——基础概念论述(一)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

Github 2024-02-09 开源项目日报 Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-09统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目4Go项目2Scala项目1PLpgSQL项目1Ruby项目1HTML项目1Solidity项目1Lua项目1 开源个人理财应用 Mayb…

如何连接ChatGPT?无需科学上网,使用官方GPT教程

随着AI的发展&#xff0c;ChatGPT也越来越强大了。 它可以帮你做你能想到的几乎任何事情&#xff0c;妥妥的生产力工具。 然而&#xff0c;对于许多国内的用户来说&#xff0c;并不能直接使用ChatGPT&#xff0c;不过没关系&#xff0c;我最近发现了一个可以直接免科学上网连…

【图形图像的C++ 实现 01/20】 2D 和 3D 贝塞尔曲线

目录 一、说明二、贝塞尔曲线特征三、模拟四、全部代码如下​五、资源和下载 一、说明 以下文章介绍了用 C 计算和绘制的贝塞尔曲线&#xff08;2D 和 3D&#xff09;。    贝塞尔曲线具有出色的数学能力来计算路径&#xff08;从起点到目的地点的曲线&#xff09;。曲线的形…

新版MQL语言程序设计:模板方法模式的原理、应用及代码实现

文章目录 一、什么是模板方法模式二、模板方法模式的实现原理三、模板方法模式的应用场景四、模板方法模式的代码实现 一、什么是模板方法模式 模板方法模式是一种行为设计模式&#xff0c;它定义了一个算法的骨架&#xff0c;将一些步骤的实现延迟到子类中。在模板方法模式中&…

[145] 二叉树的后序遍历 js

题目描述&#xff1a;给你一棵二叉树的根节点 root &#xff0c;返回其节点值的 后序遍历 解题思路&#xff1a; 迭代法&#xff1a; 后序&#xff08;左右根&#xff09; 先序是根左右 后序是左右根 后序翻转一下就是 根右左 所以后序的结果实际就是 先序的方法&#xff0…

Java汽车销售管理

技术架构&#xff1a; springboot mybatis Mysql5.7 vue2 npm node 有需要该项目的小伙伴可以私信我你的Q。 功能描述&#xff1a; 针对汽车销售提供客户信息、车辆信息、订单信息、销售人员管理、财务报表等功能&#xff0c;提供经理和销售两种角色进行管理 效果图&…

医院挂号预约|医院挂号预约小程序|基于微信小程序的医院挂号预约系统设计与实现(源码+数据库+文档)

医院挂号预约小程序目录 目录 基于微信小程序的医院挂号预约系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、小程序用户端 2、系统服务端 &#xff08;1&#xff09; 用户管理 &#xff08;2&#xff09;医院管理 &#xff08;3&#xff09;医生管理 &…

Cisco firepower2100系列使用FDM管理FTD

Cisco firepower2100系列使用FDM管理FTD 啥是FDM Firepower Device Manager 当思科Firepower系列运行的FTD镜像时&#xff0c;可以通过2种方式进行管理 第1种方式&#xff1a; FMC (Firepower management Center) 可以进行统一管理&#xff0c;一台FMC可以管理多个FTD&…

[office] 怎么在Excel2003菜单栏自定义一个选项卡 #其他#微信#知识分享

怎么在Excel2003菜单栏自定义一个选项卡 怎么在Excel2003菜单栏自定义一个选项卡 ①启动Excel2003&#xff0c;单击菜单栏--工具--自定义。 ②在自定义界面&#xff0c;我们单击命令标签&#xff0c;在类别中选择新菜单&#xff0c;鼠标左键按住新菜单&#xff0c;拖放到菜单栏…

设计模式巡礼:多板适配案例解析与深度重构

theme: cyanosis 月黑风高&#xff0c;好兄弟发给我一个重构需求&#xff0c;咨询我的意见。 一、 场景分析 开发的产品是需要运行到不同的定制Android板子&#xff0c;不同板子有对应的不同SDK提供的API&#xff0c;目前的业务端&#xff0c;业务流程基本是确定的&#xff0…

Python中HTTP隧道的基本原理与实现

HTTP隧道是一种允许客户端和服务器之间通过中间代理进行通信的技术。这种隧道技术允许代理服务器转发客户端和服务器之间的所有HTTP请求和响应&#xff0c;而不需要对请求或响应内容进行任何处理或解析。Python提供了强大的网络编程能力&#xff0c;可以使用标准库中的socket和…

单片机学习笔记---DS1302实时时钟工作原理

目录 DS1302介绍 学会读芯片手册&#xff08;DS1302芯片手册&#xff09; 封装 引脚定义 电源部分 时钟部分 通信部分 总结列表 内部结构图 电源控制部分 时钟控制部分 寄存器部分 访问部分 寄存器部分的详细定义 命令字 时序的定义 单字节读 单字节写 提前预…

flinksqlbug : AggregateFunction udf Could not extract a data type from

org.apache.flink.table.api.ValidationException: SQL validation failed. An error occurred in the type inference logic of function ‘default_catalog.default_database.CollectSetSort’. org.apache.flink.table.api.ValidationException: An error occurred in the t…

LC 1696. 跳跃游戏 VI

1696. 跳跃游戏 VI 难度 : 中等 题目大意: 给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。 一开始你在下标 0 处。每一步&#xff0c;你最多可以往前跳 k 步&#xff0c;但你不能跳出数组的边界。也就是说&#xff0c;你可以从下标 i 跳到 [i 1&#xff0c; min(n…