双指针降时间复杂度至o(n),关键在按较矮侧已知最大值计算当前水量;常见错误是移动指针后立即更新max,正确顺序为:先算水、再更新max、最后移指针。

为什么双指针比暴力解法快,但容易错在边界更新逻辑
双指针解法核心是避免对每个位置都扫全数组找左右最大值,时间从 O(n²) 降到 O(n)。关键不在“两个指针”,而在“谁决定当前能接多少水”——取决于较矮那边的已知最大值。
常见错误是写成:left++ 或 right-- 后立刻用新位置的 height[left] 更新 left_max,导致用还没验证过的值参与计算。正确顺序必须是:先用旧 left_max 算当前格子的水量,再更新 left_max,最后移动指针。
- 移动指针前,先用当前
left_max和right_max计算min(left_max, right_max) - height[i] - 只有当
height[left] 时才动 <code>left,因为此时left处的瓶颈由left_max决定,right_max是冗余信息 -
left_max必须用max(left_max, height[left])更新,不是直接赋值;否则会丢失历史峰值
单调栈怎么存“可能被填满的坑”,而不是存高度值本身
单调栈在这里不是为了排序,而是维护一个“待结算的左侧下降沿”。栈里存的是下标,不是高度值——因为要算宽度(i - stack.top() - 1)和高度差(min(height[stack.top()-1], height[i]) - height[stack.top()])。
典型误操作是把栈设成 stack<int></int> 却往里 push 高度值,结果算宽度时完全没法定位。另一个坑是 while 循环条件写成 !st.empty() && height[i] > height[st.top()],漏了栈空检查,导致 st.top() 崩溃。
立即学习“C++免费学习笔记(深入)”;
- 栈类型必须是
stack<int></int>,存下标,别存int值 - while 条件要写全:
!st.empty() && height[i] > height[st.top()],不能省略!st.empty() - 每次 pop 后,必须确保栈非空才能取新的
st.top()作为左边界,否则跳过这次结算
两种方法在空洞嵌套场景下的行为差异
比如输入 [3,2,1,2,3],中间 [2,1,2] 是个“小盆地”,双指针会把它当作独立单元处理;而单调栈会在遇到第二个 2 时,一次性把 1 和第一个 2 都弹出,再分别结算。这不是对错问题,而是抽象视角不同:双指针是“逐格推演”,单调栈是“按坑结算”。
性能上,双指针常数更小、缓存友好;单调栈多一次遍历+栈操作,但在需要记录每个坑的左右边界时(比如扩展成“最大矩形”),栈结构天然支持。
- 双指针无法直接回答“哪一段构成了某个积水区域”,它只累计总量
- 单调栈每 pop 一次,就确定了一个以弹出下标为底部、左右为边界的积水块
- 如果输入含大量平台(连续相等高度),双指针仍稳定;单调栈需注意相等时是否入栈——通常
>=才入栈,避免重复结算
别忽略 height 为空或单元素时的边界
几乎所有实现都会在开头加 if (height.size() ,但很多人写成 <code>height.empty() || height.size() == 1,漏了 size() == 2 的情况。两个柱子根本没法存水,必须排除。
另一个隐形坑是用 vector<int>::size_type</int> 做循环变量,和负数比较(比如 i >= 0)时触发无符号整数回绕,导致死循环。c++ 中优先用 int i 配合 static_cast<int>(height.size())</int>。
- 安全判断写法:
if (height.size() ,别拆开写 - 双指针初始化时,
left = 0,right = height.size() - 1,必须保证right是int类型,不是size_t - 单调栈循环中,
i用int,不要依赖auto i : ...,防止类型推导出错
事情说清了就结束。最常掉进去的地方不是算法逻辑,是下标越界、类型混用、还有把“能存水”和“当前格子存多少”搞混。