笔试算法 - 双指针篇(二):四大经典求和题型 + 有效三角形计数问题

目录

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》《笔试算法

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、有效三角形的个数

611.有效三角形的个数

解法一(暴力求解)(会超时)
算法思路

三层 for 循环枚举出所有的三元组,并且判断是否能构成三角形。

虽然说是暴力求解,但是还是想优化一下:

判断三角形的优化:

  • 如果能构成三角形,需要满足任意两边之和要大于第三边。但是实际上只需让较小的两条边之和大于第三边即可。
  • 因此我们可以先将原数组排序,然后从小到大枚举三元组,一方面省去枚举的数量,另一方面方便判断是否能构成三角形。


解法二(排序 + 双指针)
算法思路

先将数组排序。

根据「解法一」中的优化思想,我们可以固定一个「最长边」,然后在比这条边小的有序数组中找出一个二元组,使这个二元组之和大于这个最长边。由于数组是有序的,我们可以利用「对撞指针」来优化。

设最长边枚举到 i 位置,区间 [left, right] 是 i 位置左边的区间(也就是比它小的区间):

  • 如果 nums[left] + nums[right] > nums[i]:
    • 说明 [left, right - 1] 区间上的所有元素均可以与 nums[right] 构成比 nums[i] 大的二元组
    • 满足条件的有 right - left 种
    • 此时 right 位置的元素的所有情况相当于全部考虑完毕,right减减,进入下一轮判断
  • 如果 nums[left] + nums[right] <= nums[i]:
    • 说明 left 位置的元素是不可能与 [left + 1, right] 位置上的元素构成满足条件的二元组
    • left 位置的元素可以舍去,left++ 进入下轮循环
cpp 复制代码
class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        int ret = 0;
        for(int cur = n - 1; cur >= 2; cur--)
        {
            //left 和 right要初始化在循环内部,left和right的位置是随cur的位置动态变化的
            int left = 0, right = cur - 1;
            while(left < right)
            {
                if(nums[left] + nums[right] > nums[cur])
                {
                    ret += right - left;
                    right--;
                }else{
                    left++;
                }
            }
        }
        return ret;
    }
};

二、查找总价值为目标值的两个商品

LCR 179.查找总价值为目标值的两个商品

该题目就不写题解了,因为确实比较简单,若是你把前一篇文章的题目笔试算法 - 双指针篇(一):移动零、复写零、快乐数与盛水容器都自己好好写过的话,我相信你大概率是能自己做出来这道题目的

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) {
        int n = price.size();
        int left = 0, right = n - 1;
        while(left < right)
        {
            int sum = price[left] + price[right];
            if(sum > target) right--;
            else if(sum < target) left++;
            else return {price[left], price[right]};
        }
        //过编译器的语法检测,在该题目中并无此种结果
        return {-1, -1};
    }
};

三、三数之和

LeetCode 15. 三数之和



解法(排序 + 双指针)
算法思路

本题与两数之和类似,是非常经典的面试题。

与两数之和稍微不同的是,题目中要求找到所有「不重复」的三元组。那我们可以利用在两数之和那里用的双指针思想,来对我们的暴力枚举做优化:

i. 先排序;

ii. 然后固定一个数 a;

iii. 在这个数后面的区间内,使用「双指针算法」快速找到两个数之和等于 −a 即可。

但是要注意的是,这道题里面需要有「去重」操作~

i. 找到一个结果之后,left 和 right 指针要「跳过重复」的元素;

ii. 当使用完一次双指针算法之后,固定的 a 也要「跳过重复」的元素。

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ret;
        //排序
        sort(nums.begin(), nums.end());

        //利用双指针解决问题
        int n = nums.size();
        for(int i = 0; i < n; )
        {
            if(nums[i] > 0) break;//小优化
            int left = i + 1, right = n - 1, target = -nums[i];
            while(left < right)
            {
                int sum = nums[left] + nums[right];
                if(sum > target) right--;
                else if(sum < target) left++;
                else{
                    ret.push_back({nums[i], nums[left], nums[right]});
                    //不漏
                    left++, right--;
                    //对 left 和 right 去重 + 避免越界
                    while(left < right && nums[left] == nums[left - 1]) left++;
                    while(left < right && nums[right] == nums[right + 1]) right--;
                }
            }
            //对 i 去重 + 避免越界
            i++;
            while(i < n && nums[i] == nums[i - 1]) i++;
        }
        return ret;
    }
};

这是一种对 i 去重 + 避免越界,还有另外一种写法

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ret;
        //排序
        sort(nums.begin(), nums.end());

        //利用双指针解决问题
        int n = nums.size();
        for(int i = 0; i < n; i++)
        {
            if(nums[i] > 0) break;//小优化
            int left = i + 1, right = n - 1, target = -nums[i];
            while(left < right)
            {
                int sum = nums[left] + nums[right];
                if(sum > target) right--;
                else if(sum < target) left++;
                else{
                    ret.push_back({nums[i], nums[left], nums[right]});
                    //不漏
                    left++, right--;
                    //对 left 和 right 去重 + 避免越界
                    while(left < right && nums[left] == nums[left - 1]) left++;
                    while(left < right && nums[right] == nums[right + 1]) right--;
                }
            }
            //对 i 去重 + 避免越界
            while(i + 1 < n && nums[i + 1] == nums[i]) i++;
        }
        return ret;
    }
};

我个人还是更喜欢第二种写法,for循环少写一个逻辑看着有点难受,按照你的代码风格来写就行了,但是千万要注意第二种写法不能写为while(i < n && nums[i + 1] == nums[i]) i++;这里nums[i + 1]是会触发越界访问的,因为有这种情况

  1. 外层for(int i=0; i<n; i++);进入循环体时,i 一定满足 0 ≤ i ≤ n-1(i 永远合法)
  2. 当 i = n-1(最后一个元素,数组最大合法下标):
    左边条件:i < n → n-1 < n → 结果为 true
    短路失效,程序会立刻执行右边:nums[i+1] → nums[n]
    数组下标范围是0~n-1,访问nums[n] → 直接越界非法访问!程序崩溃 / 报错

举个直接触发越界的测试用例

cpp 复制代码
nums = {-2,-1,-1}; // 排序后不变,n=3

数组全负数,触发不了nums[i]>0 break优化

循环 i 会依次走到 0、1、2(i=2 就是 n-1=2)

执行while(i<n && nums[i+1]==nums[i]):i=2<3 成立,直接访问 nums [3] → 越界

还可以再优化一个点,因为三数之和需要凑 3 个数字,取a这个数字的时候,当数组全为负数和0的时候,i最多也只能指向 n - 3的位置,这样就是极致优化了

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ret;
        //排序
        sort(nums.begin(), nums.end());

        //利用双指针解决问题
        int n = nums.size();
        for(int i = 0; i < n - 2; i++)
        {
            if(nums[i] > 0) break;//小优化
            int left = i + 1, right = n - 1, target = -nums[i];
            while(left < right)
            {
                int sum = nums[left] + nums[right];
                if(sum > target) right--;
                else if(sum < target) left++;
                else{
                    ret.push_back({nums[i], nums[left], nums[right]});
                    //不漏
                    left++, right--;
                    //对 left 和 right 去重 + 避免越界
                    while(left < right && nums[left] == nums[left - 1]) left++;
                    while(left < right && nums[right] == nums[right + 1]) right--;
                }
            }
            //对 i 去重 + 避免越界
            while(i + 1 < n && nums[i + 1] == nums[i]) i++;
        }
        return ret;
    }
};

这是使用了哈希表的解法,我个人认为看一看知道这个思路可以举一反三即可,自然是双指针更优的

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ret;
        sort(nums.begin(), nums.end());  // 代码:排序,保留
        int n = nums.size();

        for(int i = 0; i < n - 2; i++)  // 代码:固定第一个数i,保留
        {
            if(nums[i] > 0) break;       // 代码:优化,保留
            int target = -nums[i];       // 代码:要找的两数之和=target,保留

            // ===================== 哈希核心代码(重点讲解) =====================
            unordered_set<int> hash;     // 1. 创建一个空哈希表,用来存「已经遍历过的数字」
            
            // 2. 遍历i后面的所有数j(相当于双指针的遍历)
            for(int j = i + 1; j < n; j++){
                // 3. 计算:需要找的第三个数字 need
                // 公式:need + nums[j] = target  →  need = target - nums[j]
                // 最终三数和:nums[i] + need + nums[j] = 0
                int need = target - nums[j]; 

                // 4. 关键:用count查need在不在哈希表里
                if(hash.count(need)){
                    // 找到了!need是之前遍历过的数,凑成三元组
                    ret.push_back({nums[i], need, nums[j]});
                    
                    // 5. 去重:跳过重复的j,避免输出重复结果
                    while(j+1 < n && nums[j] == nums[j+1]) j++;
                }

                // 6. 把当前nums[j]放进哈希表(给后面的j用)
                hash.insert(nums[j]);
            }
            // ==================================================================

            while(i + 1 < n && nums[i + 1] == nums[i]) i++;  // 你的代码:i去重,保留
        }
        return ret;
    }
};

其实我个人觉得这道题目完全可以算得上是困难题目了,力扣上的题目难度划分还是不太严谨

四、四数之和

LeetCode 18.四数之和

cpp 复制代码
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> ret;
        //排序
        sort(nums.begin(), nums.end());
        int n = nums.size();
        //查找四元组
        for(int i = 0; i < n - 3; i++)//固定a
        {
            for(int j = i + 1; j < n - 2; j++)//固定b
            {
                int left = j + 1, right = n - 1;
                long long aim = (long long)target - nums[i] - nums[j];
                while(left < right)
                {
                    long long sum = (long long)nums[left] + nums[right];
                    if(sum > aim) right--;
                    else if(sum < aim) left++;
                    else{
                        ret.push_back({nums[i], nums[j], nums[left], nums[right]});
                        left++, right--;
                        //去重
                        while(left < right && nums[left] == nums[left - 1]) left++;
                        while(left < right && nums[right] == nums[right + 1]) right--;
                    }
                }
                //去重j
                while(j + 1 < n && nums[j + 1] == nums[j]) j++;
            }
            //去重i
            while(i + 1 < n && nums[i + 1] == nums[i]) i++;
        }
        return ret;
    }
};

解法(排序 + 双指针)
算法思路

a. 依次固定一个数 a;

b. 在这个数 a 的后面区间上,利用「三数之和」找到三个数,使这三个数的和等于 target−a 即可。

这里还有一个要注意的点

C++ 算术运算规则:只要有一个操作数是 long long,所有数会自动提升为 long long 计算,结果也是 long long→ 不需要给每一个变量都加 (long long)


结语

相关推荐
十五年专注C++开发2 小时前
WaitingSpinnerWidget: 一个高度可配置的自定义Qt等待加载动画组件
开发语言·c++·qt·waitingspinner
qeen873 小时前
【数据结构】树的基本概念及存储
c语言·数据结构·c++·学习·
刀法如飞3 小时前
【合并已排序数组的三种实现策略,哪一种更可取?】
算法·程序员
王老师青少年编程3 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:种树
c++·算法·贪心·csp·信奥赛·区间贪心·种树
hi_ro_a3 小时前
C++ 哈希表封装 unordered_map /unordered_set
数据结构·c++·算法·哈希算法
c++之路3 小时前
C++ 动态内存
java·jvm·c++
Jasmine_llq7 小时前
《B4447 [GESP202512 二级] 环保能量球》
数据结构·算法·数学公式计算(核心)·整数除法算法·多组数据循环处理·输入输出算法·简单模拟算法
蔡大锅7 小时前
🔥 在线学习算力平台推荐-Hyper.AI
人工智能·算法
老唐7777 小时前
常见经典十大大机器学习算法分类与总结
人工智能·深度学习·神经网络·学习·算法·机器学习·ai