1. 三数之和
关键点 去重的情况, 以及去重元素的意义;
理解以下两点:
-
第一类情况: 遍历数值中, 初始 i i i位置上的 元素去重;
-
第二类情况, 基于当前 i i i 位置上的元素, 对左指针和右指针位置上的元素去重;
而在第二种情况下, 每次去重 元素的意义不一样;
– 去重的意义:
理解以下两种情况:
在等于0 和不等于0的情况下, 两种情况下的去重,意义是不一样的:
-
不等于0 时的去重:
此时去重的意义在于:去除的相同元素是不满足和为零条件的元素; (即这两个元素都是不满足条件的重复元素,所以继续移动;具体而言,便是:
1.1 三数之和大于0:右指针往左移动后,
此时判断的是: 右指针当前指针位置上的元素是否和移动前位置上的元素是否相同, 若相同,表明当前指针位置上的元素,仍然不满足和为0 的条件, 所以右指针继续左移;
1.2 三数之和大于0:左指针往右移动后,
原理同上,请自行理解; -
等于0 时的去重:
此时的去重, 去除的元素是满足和为零条件的元素:
2.1 此时右指针比较重复元素时, 当前指针位置上的元素和移动后指针位置上的元素是否相同, 若相同, 表明他们是满足和为零条件的重复元素; 去除重复元素;
2.2 此时左指针比较重复元素时, 当前指针位置上的元素和移动后指针位置上的元素是否相同, 若相同, 表明他们是满足和为零条件的重复元素; 去除重复元素;
去除重复元素后, 在当前 i i i 基础上, 左右指针同时往中间移动;
1.1 解题步骤:
-
先对数组排序;
-
对排序后的数组开始遍历:每一次左指针, 右指针都是建立在当前 i i i 所在的相对位置上;
排序之后, 如果i 的第一位置 大于0 , 则直接返回结果;2.1. 三数之和大于0:右指针往左移动时, 判断右边重复项,并去除右边重复元素;
2.2. 三数之和小于0:左指针往右移动时, 判断左边重复项, 并去除左边重复元素;
2.3 否则,当前位置便是,三数之和 = 0: 将当前的三元组压入到结果集中, 继续去重复:
2.3.1 此时继续判断去重, 右指针元素是否和前一个元素相同, 相同, 右指针左移;
2.3.2 左指针元素, 是否和后一个元素相同, 相同, 左指针右移;
1.2 code
#include "vector"
#include "algorithm"
using namespace std;
class Solution{
public:
vector<vector<int>> threeSum(vector<int>& nums){
vector<vector<int>> result; // 用于存放三数之和为零的下标;
// 1.先对数组排序;
sort(nums.begin(), nums.end());
// 遍历排序后的数组, i 从0开始, 在i的基础上建立左指针, 右指针从末尾开始;
for( int i = 0; i < nums.size(); i++){
// 排序后数组的第一个元素,如果大于0 直接返回result;
if(nums[i] > 0) return result;
// 1. 先对i 位置上的元素 去重
if( i > 0 && nums[i] == nums[i - 1]) // 退出当前循环。 开始下一次循环;
continue;
// 2. 开始赋予左指针,右指针;
int left = i +1, right = nums.size() -1;
while(left < right){
// 大于零时, 去除不满足条件的重复元素;
if( nums[i] + nums[left] + nums[right] > 0){
// 移动右指针, 将数值减小;
right--;
// 只要 left < right, 并且更新后的右指针位置上的数字 和 右指针更新之前位置上的数字相同, 则继续移动,因为重复的元素同样是不满足和为零的条件;
while(left < right && nums[right] == nums[right + 1]) right--;
} else if (nums[i] + nums[left] + nums[right] < 0 ){
left++;
while(left < right && nums[left] == nums[left - 1]) left++;
}
else { // 否则的化, 便是等于= 0, 满足三数之和的情况; 此时去除的是满足条件的重复元素;
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 此时去重元素, 是当前元素和 下一个待更新位置上的元素;
while(left < right && nums[right] == nums[right - 1]) right--;
while (left < right && nums[left] == nums[left + 1] ) left++;
// 在去重之后, 在当前的i 的基础上, 继续移动 left, right , 寻找三数之和 = 0 的 三元组;
left++;
right--;
}
}
}
return result;
}
};
2. 四数之和
题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:
答案中不可以包含重复的四元组。
示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]
2.1 思路:
三重循环 , 两层for 循环 + 一层 while 循环;
第一层for 循环, s1 从数组下标0 开始;
第二层for 循环, s2 从数组下标 s1 + 1 开始;
第三层while 循环, while( left < right),
left 从数组下标 s2 +1 开水, right 从数组下标 nums.size() -1 开始;
四数之和的三指针解法是两层for循环nums[s1] + nums[s2]为确定值,
第三层循环内有left和right下标作为双指针,
找出nums[s1] + nums[s2] + nums[left] + nums[right] == target的情况,
三数之和的时间复杂度是 O ( n 2 ) O(n^2) O(n2),四数之和的时间复杂度是 O ( n 3 ) O(n^3) O(n3) 。
2.2 解题步骤
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target){
// 新建向量容器, 用于存放四元数组;
vector<vector<int>> result;
// 排序;
sort(nums.begin(), nums.end());
// 开始第一层循环
for(int s1 = 0; s1 < nums.size() ; s1 ++){
// 第一层循环时,遇到重复元素,跳出本次循环, 继续下一轮循环;
if(s1 > 0 && nums[s1] == nums[s1 - 1]) continue;
// 开始第二层循环,起始位置 s1 + 1;
for(int s2 = s1 + 1; s2 < nums.size(); s2++){
// 遇到重复元素,跳出本次循环,继续下一轮循环;
if(s2 > s1 +1 && nums[s2] == nums[s2 - 1]) continue;
// 开始第三重循环, left < right; 找出满足 target 条件的 四元数组,
// 起始位置, left = s2 + 1; right = nums.size() -1;
int left = s2 + 1, right = nums.size() - 1;
while(left < right){
// 去重两中类型的重复元素, 不满足target 条件的重复元素, 以及满足target条件的重复元素;
//if(nums[s1] + nums[s2] + nums[left] + nums[right] > target )
// 这种表达形式会 导致溢出 错误;
if(nums[s1] + nums[s2] > target - (nums[left] + nums[right]) ){
// 大于target, 右指针左移;
right--;
// 移动后指针位置上的元素, 如果和移动之前指针位置上的元素重复,去重;
while(left < right && nums[right]== nums[right + 1] ){ right--;}
}else if(nums[s1] + nums[s2] < target - (nums[left] + nums[right]) ){
// 小于 target, 左指针右移动;
left++;
while(left < right && nums[left]== nums[left - 1] ){ left++;}
}else { // 此时是满足target 条件的 四元数,
result.push_back({nums[s1], nums[s2], nums[left], nums[right]});
// 此时的去重元素, 是满足target 条件的元素去重;
while (left < right && nums[right] == nums[right -1]) {right--;}
while (left < right && nums[left] == nums[left + 1 ]) {left++;}
// 去重重复元素后, 在当前的 s1, s2 基础上, 去寻找满足条件的 四元数;
left++;
right--;
}
}
}
}
return result;
}
};
3. 溢出问题
上述代码中
//if(nums[s1] + nums[s2] + nums[left] + nums[right] > target )
// 这种表达形式会 导致溢出 错误
3.1 整型溢出
#include <stdio.h>
int main(int argc, char **argv)
{
unsigned short int a;
signed short int b;
a = 50000;
b = 50000;
printf("\n a = %d \t b = %d \n", a, b);
return 0;
}
在不编译运行的情况下,请写出这段代码的输出。写好以后,那么编译运行吧,答案可能会出乎你的意料!
a = 50000 b = -15536
为什么明明给a, b两个数赋值都是50000,而输出的结果却不同呢?
这里涉及到整数溢出的问题!我们知道,无论是钟表还是汽车的里程表,一旦达到或者超过它能表示的最大值时,就会从起点开始重新计数。C语言中的整数溢出问题和这类问题如出一辙。简单来说,C语言中各个数据类型所能表示的数都是有一定的取值范围的,一旦要表示的数超出了该数据类型的取值范围,就会从起点开始重新计数。
针对本例的分析:"short int"类型,在计算机中一般用2个字节(Byte)来存储,每个字节由8位(bit)组成(1 Byte = 8 bit),每位表示一个二进制数0或1,所以,“short int"类型能表示 28∗2=216=6553628∗2=216=65536 个整数,以整数0为中点划分为(-32768 ~ 32767),其表示范围就是(-32768 ~ 32767)。而"unsigned short int"类型,因为声明了"unsigned”(无符号),所以其表示范围是(0 ~ 65535)。
由以上分析可知,"unsigned short int"类型,可以表示整数50000,并且没有溢出;而用"signed short int"类型来表示整数50000,就会出现整数溢出现象。
既然溢出了,那么溢出后原来的数会变为多少呢?分析如下:"signed short int"类型最大能表示的整数为32767,一旦超过了32767,就会像钟表或者汽车的里程表一样,从起点重新开始计数,那么,"signed short int"类型的起点是多少呢?是-32768!所以,32768就被表示为-32768了,那么50000会被表示为多少呢?总共有(50000 - 32767) = 17233个数超过了范围,这17233个数要从起点-32768开始计数,相当于从-32768开始往大了数17233个数。好了,下面我们开始来数数,第一个数1对应于-32768,第二个数2对应于-32767,…,依次类推下去,最后答案是(-32768 + 17233) - 1 = -15536,之所以最后要减一是因为-32768对应的数是1而不是0。