算法精讲:巧用双指针与单调性,高效求解「有效三角形个数」

在算法面试和日常开发中,如何高效地处理数组并找出满足特定条件的元素组合,是衡量开发者基本功的重要标尺。今天,我们将深入探讨一道经典题目——「有效三角形个数」,并揭示如何利用数组的单调性双指针技巧,将时间复杂度从 O(N³) 优化至 O(N²),实现性能的飞跃。掌握此技巧,不仅能解决此类问题,更能深刻理解双指针算法在有序数据上的强大威力。

一、问题定义与暴力解法剖析

题目要求很简单:给定一个包含非负整数的数组,返回可以组成三角形三条边的不同三元组个数。这里的关键是“不去重”,即值相同的不同索引元素被视为不同的组合。

最直观的解法是三层嵌套循环,穷举所有三元组 (a, b, c),然后判断其能否构成三角形。三角形的判定依据是任意两边之和大于第三边,这需要三次判断(a+b>c, a+c>b, b+c>a)。

这种暴力解法的时间复杂度高达 O(N³),在数据量稍大时(如 N>1000)便难以承受。其核心瓶颈在于无差别的穷举和重复的判断逻辑。此时,一个关键的优化思路浮出水面:如果数组是有序的,判定条件能否简化?答案是肯定的。当我们固定最长边 c 时,只需判断较小的两边 a 和 b 之和是否大于 c 即可(a ≤ b ≤ c)。这得益于有序带来的单调性:若 a+b > c 成立,那么对于固定的 b,所有比当前 a 大的元素与 b 之和也必然大于 c。这正是我们优化的突破口。

二、排序奠基:引入单调性优化

优化第一步,对数组进行排序。排序的时间复杂度为 O(N log N),这是一笔非常划算的“投资”。排序后,数组呈现非递减的单调性,这为我们带来了两大好处:

  • 简化判定逻辑:只需判断nums[left] + nums[right] > nums[i]这一个条件。
  • 启用双指针扫描:利用单调性,我们可以用两个指针在线性时间内扫描出所有满足条件的二元组,替代一层循环。

这种“先排序,后处理”的模式,在解决许多数组组合问题(如两数之和、三数之和)时都非常有效。它不仅适用于C++,在JavaPythonGoTypeScript中,利用语言内置的高效排序函数,都能轻松实现这一预处理步骤。

[AFFILIATE_SLOT_1]

三、双指针算法的核心逻辑与演绎

排序之后,进入算法的核心:外层循环固定最长边,内层使用双指针寻找较短的两边。我们设定外层循环变量 i 从数组末尾开始遍历,将 nums[i] 视为当前三角形的最大边(第三边)。

对于每个固定的 i,初始化两个指针:left = 0, right = i - 1。我们的目标是在 [left, right] 区间内,找到所有满足 nums[left] + nums[right] > nums[i](left, right) 对。

指针移动的贪心策略如下:

  1. 如果 nums[left] + nums[right] > nums[i]这是一个关键发现!由于数组有序(单调递增),此时对于固定的 right,所有从 leftright-1 的索引与 right 组合都满足条件。因此,一次性可以计入 right - left 个有效三元组。然后,将 right 左移一位,尝试更小的“第二大边”。
  2. 如果 nums[left] + nums[right] <= nums[i]:说明当前的两边之和太小,需要增大最小值,因此将 left 右移一位。

这个过程直到 leftright 相遇结束。对于每个 i,双指针的扫描是线性的 O(N),因此整体算法复杂度为 O(N²)。

四、代码实现与逐行解析

理解了算法思想,我们来看具体的代码实现。以下是基于上述思路的C++核心代码:

class Solution {
public:
    int maxArea(vector& height) {
        int left=0,right=height.size()-1,vm=0;//定义双指针
        while(leftvm?v:vm;//如果算出来的体积比vm大,二者交换
            if(height[left]

让我们对上期参考代码进行解析:首先进行排序,然后外层循环固定最大数。内层的 while 循环是双指针的经典模板。当满足条件时,通过 right - left 快速计数并移动 right;不满足时则移动 left。这段代码清晰体现了如何利用单调性避免无效计算。

为了加深理解,我们也回顾一下暴力解法作为对比:

class Solution {
public:
    int triangleNumber(vector& nums) {
        int n=nums.size(),sum=0;
        sort(nums.begin(),nums.end());//这里先排序,不排序过不了
        for(int i=0;inums[k])//排序之后只需要进行一次逻辑判断。
        sum++;
        return sum;
    }
};

对比两者,高下立判。双指针解法通过排序和巧妙的指针移动,去除了大量不必要的枚举和判断。

[AFFILIATE_SLOT_2]

五、视觉化演示与常见误区

一图胜千言。下面的示意图清晰地展示了当固定最大边 nums[i] = 9 时,双指针 leftright 是如何协作,快速定位所有满足 nums[left] + nums[right] > 9 的组合的。

在实现和面试中,需要特别注意以下几个要点:

  • ⚠️ 排序是前提:务必先排序,否则单调性假设不成立,算法失效。
  • ⚠️ 指针移动的逻辑:理解为什么满足条件时是累加 right-left 并移动 right,这是利用单调性的精髓。
  • ⚠️ 去重问题:本题明确要求不去重。若题目要求去重,则需要在指针移动时跳过重复值,这是另一个常见的变体。
  • 溢出处理:在类似题目中,若数值很大,相加可能导致整数溢出,在某些语言中需要考虑使用长整型。

六、总结与延伸思考

通过「有效三角形个数」这道题,我们深入掌握了“排序 + 双指针”这一经典范式。其核心在于利用有序序列的单调性,将原本需要多重循环枚举的问题,转化为指针的线性扫描,从而大幅降低时间复杂度。

这个技巧的应用范围极广,例如:

  • 两数之和(有序数组版):一左一右指针向中间逼近。
  • 盛最多水的容器:同样是利用高度和宽度的单调性移动指针。
  • 三数之和:固定一个数,转化为两数之和问题。

掌握其本质——通过单调性确定决策的单调方向,从而安全地缩小搜索空间——你就能在面对更多复杂问题时游刃有余。下期,我们将探讨另一道利用类似思想的题目:LCR 179. 查找总价格为目标值的两个商品,敬请期待!

最后,再次强调:“光看不练假把式”。强烈建议你关闭文章后,独立在代码编辑器(无论是VS Code、IntelliJ还是在线平台)中手敲一遍代码,并尝试用JavaPython等语言复现,才能真正内化这种重要的算法思维。

posted on 2026-02-26 19:12  blfbuaa  阅读(6)  评论(0)    收藏  举报