【Algorithm】Day-3
本篇文章主要讲解双指针与滑动窗口算法练习题
1 四数之和
链接:https://leetcode.cn/problems/4sum/description/?envType=problem-list-v2&envId=two-pointers
题目描述:
给你一个由
n
个整数组成的数组nums
,和一个目标值target
。请你找出并返回满足下述全部条件且不重复的四元组[nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]示例 2:
输入:nums = [2,2,2,2,2], target = 8 输出:[[2,2,2,2]]提示:
1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109
题目解析:
这道题目会给你一个整数数组 nums 与一个整数 target,该题目要求让你找到不同的四元组,使得 nums[a] + nums[b] + nums[c] + nums[d] == target,并将这个四元组作为 vector<int> 放到 vector<vector<int>> 中,然后找到所有满足要求的四元组放到 vector<vector<inbt>> 中,并将该二维 vector 返回,这里的不重复就是指两个四元组的元素不能完全相同。比如示例1:nums = [1, 0, -1, 0, -2, 2],target = 0,其中 -2-1+1+2 = 0,-2 +0+0+2 = 0,-1+0+0+1 = 0,所以返回的 vector<vector<int>> = [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]
算法讲解:
这道题目与之前做过的三数之和很像,三数之和我们可以看成是 target = 0,然后寻找三个数字,使得这三个数字的和为 target。在三数之和中我们采用的排序 + 双指针的算法解决的,所以四数之和依然可以采取这个方法。首先我们先对数组排序,然后从左到右依次固定第一个数,假设洗数为 a;然后在剩下的数字中再从左往右固定第二个数,假设为 b;然后在剩下的数字中采取双指针,首先定义 left 和 right,如果 nums[left] + nums[right] > target - a - b,那说明两个数字的和大了,如果 nums[left] + nums[right] < target - a - b,那说明和小了,那就 ++left,如果正好相等,那就将这四个数字尾插入 vector<vector<int>>,之后 ++left,--right 寻找下一轮。
当然在这个题目中我们依然需要去重,在这个题目中,由于是四个数字,所以我们需要进行四次去重,去重的方法依然是和三数之和中去重方法相同,这里就不做过多介绍了。
代码:
class Solution
{
public:vector<vector<int>> fourSum(vector<int>& nums, int target) {//先对数组排序sort(nums.begin(), nums.end());vector<vector<int>> vv;//四数之和,如果数字个数小于4个,直接返回vvif (nums.size() < 4) return vv;//首先固定第一个数字for (int i = 0; i < nums.size() - 3; ++i){long long a = nums[i];//再固定第二个数字for (int j = i + 1; j < nums.size() - 2; ++j){long long b = nums[j];//下面采用双指针来解决问题int left = j + 1, right = nums.size() - 1;while (left < right){long long sum = nums[left] + nums[right];if (sum < (long long)target - a - b) ++left; else if (sum > (long long)target - a - b) --right;else{vv.push_back({nums[i], nums[j], 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;}}//第二个数字去重while (j < nums.size() - 2 && nums[j] == nums[j + 1]) ++j;}//第一个数字去重while (i < nums.size() - 3 && nums[i] == nums[i + 1]) ++i;}return vv;}
};
2 水果成篮
链接:https://leetcode.cn/problems/fruit-into-baskets/description/?envType=problem-list-v2&envId=sliding-window
题目描述:
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组
fruits
表示,其中fruits[i]
是第i
棵树上的水果 种类 。你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
- 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
- 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
- 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组
fruits
,返回你可以收集的水果的 最大 数目。示例 1:
输入:fruits = [1,2,1] 输出:3 解释:可以采摘全部 3 棵树。示例 2:
输入:fruits = [0,1,2,2] 输出:3 解释:可以采摘 [1,2,2] 这三棵树。 如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。示例 3:
输入:fruits = [1,2,3,2,2] 输出:4 解释:可以采摘 [2,3,2,2] 这四棵树。 如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4] 输出:5 解释:可以采摘 [1,2,1,1,2] 这五棵树。提示:
1 <= fruits.length <= 10^5
0 <= fruits[i] < fruits.length
题目解析:
题目中会给你一个 fruits 数组,其中 fruits 数组中是一些整数,整数可以是 0 也可以是正整数,其中 fruits 数组中的数字代表着下标位置那棵树上的水果种类。比如 fruits = [0, 1, 2, 2, 3, 4],代表的意思是 0 号果树上的为 0 号水果,1 号果树上的为 1 号水果,2 号果树上的为 2 号水果,3 号果树上的为 2 号水果,4 号果树上的为 3 号水果,5 号果树上的为 4 号水果。你会有两个篮子,每个篮子可以装一种类型的水果,且装的水果数量不限,你可以选择从任意一棵果树开始采摘,只能连续采摘,并且当你的果篮无法盛下第三种水果时,就停下采摘,题目要求你求出能够采摘的树的最大棵树。比如 fruits = [0, 1, 2, 2, 3, 4],返回的就是 3,因为可以从 1 号果树开始采摘,最终采摘完 [1,2,2] 这三棵果树,停止采摘;也可以从 2 号果树开始采摘,最终采摘完 [2,2,3] 这三棵果树,停止采摘。其实这道题目就是让你在一个数组中寻找一个长度最长的子数组,要求该子数组中的数字种数不超过两种。
算法原理:
我们可以先采用一个暴力解法来解决问题,暴力解法很好想,就是枚举出所有的子数组,如果子数组满足要求,那么我们就计算出其长度然后求出所有满足条件的子数组的最大值就可以了。在枚举过程中,我们需要统计水果出现的种数,这时候我们需要用到一种数据结构,那就是哈希表,我们可以采用一个整数数组来统计每种水果出现的次数,再用一个 kinds 变量来统计水果的种数,如果水果出现过了,那我们就让 kinds++,如果水果出现次数变为了0,那就让 kinds--,这样就能统计出水果出现的次数了,显然这个算法的时间复杂度是 O(n^2) 的。
那么我们怎么优化呢?当我们在枚举出所有的子数组的过程中,我们会首先固定一个左端点 left,然后再从左端点开始,依次枚举出右端点 right,当枚举完所有以 left 为左端点的子数组的时候,我们 ++left,再从 left 开始,枚举出所有的right,但是当 left 在向右走的过程中,right 是可以不必回到 left 位置重新枚举的,因为我们可以直接由当前的 left 得出水果种类,我们可以先让 hash[ftuits[left]]--,如果 hash[fruits[left]] == 0了,说明 left 号树上的水果出现次数已经为 0 了,此时直接让 kinds-- 就可以了,这样我们就能直接得出水果种数,就不必让 right 回到 left 位置重新走了,left 与 right 同向移动,所以可以采用滑动窗口算法来解决问题。
采用滑动窗口,我们依然采取那四个步骤:
(1) left = 0, right = 0, kinds = 0, hash[100001] = { 0 }(因为 fruits[i] 最大为 99999),len = 0
(2) 进窗口:首先我们需要判断 hash[fruits[right]] 是否等于 0,如果等于0,说明该水果要出现一次了,我们需要让 kinds++,hash[fruits[right]]++,++right
(3) 判断:kinds > 2,出窗口:hash[fruits[left]]--,如果 hash[fruits[left]] == 0,说明次数篮子中已经没有 left 树上种类的水果了,需要让 kinds--,++left
(4) 更新结果:由于需要 kinds <= 2,所以我们在判断外更新结果
代码:
class Solution
{
public:int totalFruit(vector<int>& f) {int hash[100001] = { 0 };//用数组来模拟哈希表,记录每种类型水果的个数int len = 0;for (int left = 0, right = 0, kinds = 0;right < f.size(); ++right){if (hash[f[right]] == 0) kinds++;//如果水果个数为0,种类加1//进窗口hash[f[right]]++;//判断while (kinds > 2){//出窗口hash[f[left]]--;if (hash[f[left]] == 0) kinds--;++left;}//更新结果len = max(len, right - left + 1);}return len;}
};
注意:代码中将原来的 fruits 数组改为了 f 数组
显然,该算法时间复杂度为 O(n)
3 找到字符串中的所有字母异位词
链接:https://leetcode.cn/problems/VabMRr/?envType=problem-list-v2&envId=sliding-window
题目描述:
给定两个字符串
s
和p
,找到s
中所有p
的 变位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。变位词 指字母相同,但排列不同的字符串。
示例 1:
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的变位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的变位词。示例 2:
输入: s = "abab", p = "ab" 输出: [0,1,2] 解释: 起始索引等于 0 的子串是 "ab", 它是 "ab" 的变位词。 起始索引等于 1 的子串是 "ba", 它是 "ab" 的变位词。 起始索引等于 2 的子串是 "ab", 它是 "ab" 的变位词。提示:
1 <= s.length, p.length <= 3 * 104
s
和p
仅包含小写字母
题目解析:
题目中会给你两个字符串 p 和 s,题目的要求是让你返回 s 中所有 p 的变位词的子串的起始位置,虽然这里的变位词题目中是指与 p 中字符相同,排列不同的子串,但是排列相同也算是变位词。比如 s = "abcbbcabbcc",p = "bc",返回的就是1["bc"],2["cb"],4["bc"],8["bc"],返回的只有数字,数字后面的是其对应的字符串。
算法讲解:
该题目依然可以采用暴力解法来解决,首先枚举出所有长度为 p.size() 的子串,如果是 p 的变位词,那就返回子串的起始位置。其中判断是否是变位词我们可以采用判断有效字符个数的方法,因为只要有效字符个数相同,那么两个字符串就算是变位词:首先我们建立两个哈希表 hash1、hash2,由于 s 和 p 中只有小写字母,那么我们可以创建两个长度为 26 的整数数组,hash1 用来记录 p 字符串中的有效元素个数,也就是遍历一遍 p 字符串,让 hash[p[i] - 'a']++;用 hash2 来统计 s 的子串出现有效字符的个数,如果 hash1 与 hash2 所有对应位置元素都相等,那就说明该子串是 p 的变位词。显然该时间复杂度是 O(mn^2) 的(p 长度为 m,s 长度为 n)。
当然我们是可以优化这个算法的,首先我们先优化判断有效字符的这个算法:我们可以用一个 count 变量来记录 s 子串中有效字符的个数,如果 hash1[s[i] - 'a'] >= hash2[s[i] - 'a'],那就说明当前字符其实是有效字符,那就让 count++,这样就可以判断出有效字符的个数了。有了这个算法之后我们就可以用滑动窗口算法来优化该算法了:
(1) 首先填 hash1,然后定义滑动窗口算法需要的变量,left =0, right = 0, count = 0, hash2[26] = { 0 }
(2) 进窗口:如果 hash1[s[right] - 'a'] >= ++hash2[s[right] - 'a'],那就++count,然后 ++right
(3) 判断:如果子串的长度 > p.size(),那就出窗口:如果 hash1[s[left] - 'a'] >= --hash2[s[left] - 'a'],那就 --count,然后 ++left
(4) 更新结果:如果 count == p.size(),就更新结果
代码:
class Solution
{
public:vector<int> findAnagrams(string s, string p) {vector<int> hash1(26, 0);//先统计p里面各个字符个数for (auto& e : p){hash1[e - 'a']++;}vector<int> hash2(26, 0);vector<int> v;//用滑动窗口解决问题//count为有效字符的个数for (int left = 0, right = 0, count = 0;right < s.size();++right){if (hash1[s[right] - 'a'] >= ++hash2[s[right] - 'a']) count++;//hash2中有效字符个数+1//判断if (right - left + 1 > p.size()){if (hash1[s[left] - 'a'] >= hash2[s[left] - 'a']--) count--;//hash2里有效字符个数-1//出窗口++left;}//更新结果//if (hash1 == hash2)if (count == p.size())v.push_back(left);}return v;}
};