LeetCode454 四数相加
题目链接:454. 四数相加 II - 力扣(LeetCode)
其实我感觉做这种类型的算法题做多了,题感也会自然而然地出现,跟英语的语感差不多意思。那这个题举例,第一眼,先大体浏览一遍题目的意思给出的数据,第二眼直接就奔着提示中的n的范围和数组中元素值的大小范围了,直接超过2的28次方,这么大的数值,用数组方式表示的哈希表绝对不可以用了,基于所学知识,不难想到,必须得用各种语言封装好的容器了,这个不是集合,可以重复,那么就直接选择map中的unodered_map(它的时间复杂度低,为O(1),有兴趣的可以自己再去拓展一下关于map)。
以上可以说是做这道题的基本——先确定好合适数据结构,这一直是我认为的算法题的第一步。接下来讲一下这道题具体的解题思路,其实我钻研了好长一会,一直想着有没有更简单的解题方法能让他的时间复杂度降到O(n)或者O(log n)。想来想去,真的没想出来更好的办法。在思考解决完这道题之后,也似乎有了一些经验,想这种可能存在各种可能的情况,并且有多个数组集合(大于2个),没有更简单的办法,只有尽量的少些次方的时间复杂度。
具体思路很简单(重要的是在开始时能想到去构造哈希查找的关系):
(1)先将nums1 和 nums2 相加的所有的情况存入哈希表的数据结构(unordered_map),遇到相同的值将键值加一。
(2)再依次判断nums3和nums4相加负值在哈希表中查找,查找到,计数的可以直接加key键值(不加一的原因是,每一种键值都是nums1和nums2的一种情况)。
(3)最后返回计数。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> umap;
for(int a : nums1){
for(int b : nums2){
umap[a+b]++;
}
}
int count = 0;
for(int c : nums3){
for(int d : nums4){
if(umap.find(0 - (c + d)) != umap.end()){
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
这个题还是用C++写的,深感方便。
LeetCode383 救赎金
很简单,跟昨天有效的字母异位词思路大致相同,不做过多解释,可以做个拓展题。
bool canConstruct(char* ransomNote, char* magazine) {
int *hash_table;
hash_table = (int *)malloc(sizeof(int) * 26);
memset(hash_table, 0, sizeof(int) * 26);
int i = 0;
while(magazine[i] != '\0'){
hash_table[magazine[i++] - 'a']++;
}
i = 0;
while(ransomNote[i] != '\0'){
int index = ransomNote[i] - 'a';
hash_table[index]--;
if(hash_table[index] < 0){
return false;
}
i++;
}
return true;
}
LeetCode15 三数之和
题目链接:15. 三数之和 - 力扣(LeetCode)
这道题花了我很长时间,但是很有收获,接下来,我将详细简述关于 三数之和 这道题我的解题过程。
首先,一看这道题跟以前做过的四数之和的题看似相同,就直接有了思路,先把这个数组用哈希表储存,然后两个嵌套循环,遍历每种情况,在哈希表中查找符合条件的元素。看似思路没问题,但是在最后写完的时候,我才发现一个非常棘手的问题:三元组去重问题,在苦思冥想了很长时间后,还是没想到怎样解决,在又经过挺长时间的学习中,了解到,这道题用哈希表法并不是最优解,双指针的方法甚至更快。
这是示例的错误代码(没有解决去重问题):
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
unordered_map<int, int> umap;
unordered_set<int> uset;
vector<vector<int>> target;
int size = nums.size();
for(int i = 0; i < size; i++){
umap[nums[i]] = i;
}
for(int i = 0; i < size; i++){
for(int j = i + 1; j < size; j++){
int newIndex = 0 - (nums[i] + nums[j]);
if(umap.find(newIndex) != umap.end()){
int a = nums[umap[newIndex]], b = nums[i], c = nums[j];
target.push_back({a, b, c});
}
}
}
return target;
}
};
提示:可以重点掌握双指针法,有兴趣的读者可以尝试哈希表法。
接下来我先讲解一下我对哈希表法的理解(最重要的是去重的方法):
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result; // 初始化用于存储结果三元组的向量。
sort(nums.begin(), nums.end()); // 将输入数组按升序排序。
// 遍历数组以找到三元组。
for (int i = 0; i < nums.size(); i++) {
// 如果当前元素大于零,则无法形成三元组。
if (nums[i] > 0) {
break;
}
// 跳过三元组的第一个元素(a)的重复元素。
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
unordered_set<int> set; // 使用集合存储三元组的第二个元素(b)的唯一值。
// 遍历剩余元素以找到三元组的第三个元素(c)。
for (int j = i + 1; j < nums.size(); j++) {
// 跳过三元组的第二个元素(b)的重复元素。
if (j > i + 2 && nums[j] == nums[j - 1] && nums[j - 1] == nums[j - 2]) {
continue;
}
int c = 0 - (nums[i] + nums[j]); // 计算三元组所需的第三个元素(c)。
// 如果集合中找到第三个元素,则将三元组添加到结果中。
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c); // 从集合中删除第三个元素,以避免重复。
} else {
set.insert(nums[j]); // 将第二个元素添加到集合中以便将来匹配。
}
}
}
return result; // 返回包含所有唯一三元组的向量。
}
};
想去掉重复的元素,我试过,不能通过对最终的vector容器操作,那样时间复杂度太高,不是明智的选择。那只能在开始判断前,对数组元素进行去重。依然是两个for嵌套循环,多了三个分别对a、b、c元素去重的操作。
大体代码我很快就看懂了,就是在对b去重的操作,我非常疑惑,为什么判断条件要有个j > i + 2呢?我原本以为,在对a去重的操作中,取得是第一个值,去掉后面相同的值,当结束去重时,最终i指向的是相当于第二个不相同的元素吗?下面 j > i + 2 不就跳过了下标为 i 的不相同元素吗?最终我看懂了,第一次确实是对第一个元素取得值(把它当作第一个不相同的元素),但是在这次的循环里,没有进行去重操作,而是在下面的第二重循环遍历完在第二次循环回来的时候,才进行的去重,以后都是先去重再去不相同元素作为a,这样,除了第一次循环,其他的所有循环结束时的 i ,都不是第二个不相同元素,所以j > i + 2 是正确的(第一次不会进入);
我在写博客时突然发现了非常重要的我以前没发现的一点,这个代码允许a、b两个元素相同,只是不允许a、b两个元素再次出现(再被判断),这是值得思考的一点。所以当第一次进入二重循环(对 j 的循环)时,并没有对b进行去重操作,这也是有 j > i + 2这一条判断语句的原因。
可能看这么长的文字脑子肯定蒙,建议读者先去认真思考这道题,而不是直接看答案,思考跟最终解决同样重要。
接下来是更优质的方法,双指针法(极大的降低了时间复杂度):
我感觉对排序好的vector容器中去重操作,跟前面数组中的用双指针方法来寻找不同数值的元素,也是左右两个指针,同时收缩,感觉很熟悉。去重思路跟哈希表差不多,不再做详细的讲解。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
//还是得先排序,容易去重
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++){
//由于是从小到大排序好的,第一个元素大于0,整体元素不可能等于0
if(nums[i] > 0) break;
//对a去重,跟哈希表法相同分为第一次的循环(i == 0)和其余循环
if(i >= 1 && nums[i] == nums[i - 1])
continue;
//定义双指针
int left = i + 1;
int right = nums.size() - 1;
while(left < right){
if(nums[i] + nums[left] + nums[right] > 0)
right--;
else if(nums[i] + nums[left] + nums[right] < 0)
left++;
else{
result.push_back({nums[i], nums[left], nums[right]});
//left去重
while(left < right && nums[left] == nums[left + 1]) left++;
//right去重
while(left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
}
}
}
return result;
}
};
LeetCode18 四数之和
题目链接:18. 四数之和 - 力扣(LeetCode)
这道题的思路大致与三数之和相同,稍微麻烦的一点就是变成四个数,要多一层嵌套循环,所以再进行思路的解析,需要注意的点我都注释在代码上了。
class Solution
{
public:
vector<vector<int>> fourSum(vector<int>& nums, int target)
{
vector<vector<int>> result;
//特殊情况,直接返回,原理相当于剪枝
if(nums.size() < 4)
return result;
int nsize = nums.size();
//进行排序处理
sort(nums.begin(), nums.end());
for(int i = 0; i < nsize; i++)
{
//剪枝处理
//多加一个判断条件的原因:
//target不确定,可能为负数,但负数越加越小(大于一个负数的多个负数相加可能等于这个负数),
if(nums[i] > target && nums[i] >= 0){
break;
}
//第一个元素去重
if(i > 0 && nums[i] == nums[i - 1])
continue;
for(int j = i + 1; j < nsize; j++)
{
//二层剪枝处理
if(nums[i] + nums[j] > target && nums[i] + nums[j] >= 0)
break;
//第二个元素去重
if(j > i + 1 && nums[j] == nums[j - 1])
continue;
//左右指针分别指向两端
int left = j + 1;
int right = nsize - 1;
while(left < right)
{
//四数相加可能会溢出
if((long) nums[i] + nums[j] + nums[left] + nums[right] > target){
right--;
}
else if((long) nums[i] + nums[j] + nums[left] + nums[right] < target){
left++;
}
else{
result.push_back({nums[i], nums[j], nums[left], nums[right]});
//左右指针去重,定位到最后一个相同的值
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
//向中间靠拢,并且定位到不同的值
right--;
left++;
}
}
}
}
return result;
}
};
在这里再解释一下在第一层for循环内进行剪枝操作时,三数之和没有附加判断条件的原因:四树之和没有固定的target值,而三数之和固定target值为0,nums[ i ] > target 就能确定nums[ i ]是正数,不可能为负数;而当target值不确定是,不能保证nums[ i ]为正。(为负不可行的原因见注释)
哈希表总结
还是那句话,当我们遇到了一个需要快速判断一个元素是否出现在集合里的时候就用哈希法。但是,这学习过程中,我们也遇到了,哈希表并不是最优解的办法,因为哈希表里并没有特别好的办法可以快速去重(去重方法难以理解,并且容易出错),所以引入了双指针法,双指针法的时间复杂度虽然不是特别低,但是已经是能达到的极限。
而且,如果遇到的数据特别大的时候,C语言里面的数组就很难解决,由此引入了C++里的set、map封装类来快速实现哈希表的功能。