目录


🎬 云泽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;
}
};
二、查找总价值为目标值的两个商品
该题目就不写题解了,因为确实比较简单,若是你把前一篇文章的题目笔试算法 - 双指针篇(一):移动零、复写零、快乐数与盛水容器都自己好好写过的话,我相信你大概率是能自己做出来这道题目的
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};
}
};
三、三数之和
解法(排序 + 双指针) :
算法思路 :
本题与两数之和类似,是非常经典的面试题。
与两数之和稍微不同的是,题目中要求找到所有「不重复」的三元组。那我们可以利用在两数之和那里用的双指针思想,来对我们的暴力枚举做优化:
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]是会触发越界访问的,因为有这种情况
- 外层
for(int i=0; i<n; i++);进入循环体时,i 一定满足 0 ≤ i ≤ n-1(i 永远合法) - 当 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;
}
};
其实我个人觉得这道题目完全可以算得上是困难题目了,力扣上的题目难度划分还是不太严谨
四、四数之和
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)
结语






