leetcode 刷题 - 最大连续1的个数 - 将 x 减到 0 的最小操作数 - 水果成篮 - 找到字符串中所有字母异位词

I1004. 最大连续1的个数 III - 力扣(LeetCode)

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。

示例 1:

输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。
示例 2:

输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。

不要去想怎么翻转,不要把问题想的很复杂,这道题的结果⽆⾮就是⼀段连续的 1 中间塞了 k
个 0 嘛。
因此,我们可以把问题转化成:求数组中⼀段最⻓的连续区间,要求这段区间内 0 的个数不超
过 k 个。


 既然是连续区间,可以考虑使⽤「滑动窗⼝」来解决问题。

利用滑动窗口算法,left 和 right 指向开始之时,都指向 数组的开始位置。right 向后迭代,遇到1 不管,继续迭代,遇到 0 ,zero 计数器++;

当 zero 计数器 > K 之时,left开始向后迭代,如果 left 遇到 1 不管,继续迭代,遇到 0 ,zero 计数器--;

整个程序的结束条件,当 right 执行到 nums 数组的最后一个元素之时,说明已经执行完毕。

代码实现:

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int ret = 0;
        for(int left = 0, right = 0, zero_count = 0;right < nums.size();right++)
        {
            if(nums[right] == 0) zero_count++;
            while(zero_count > k) {
                if(nums[left++] == 0) zero_count--;
            }
            ret = max(ret, right - left + 1);
        }
        return ret;
    }
};

I1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。

如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。

示例 1:

输入:nums = [1,1,4,2,3], x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。
示例 2:

输入:nums = [5,6,7,8,9], x = 4
输出:-1
示例 3:

输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。

题⽬要求的是数组「左端+右端」两段连续的、和为 x 的最短数组,信息量稍微多⼀些,不易理清
思路;我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最⻓数组。此时,就是熟
悉的「滑动窗⼝」问题了。

就像是反证法一样,虽然求两个分散的 区间不好求,但是两个分散的区间中间夹着的是一个连续的区间。所以是可以。

而且,题目规定了

  • 1 <= nums[i] <= 104

所以,此时才可以使用 滑动窗口,如果 数组当中的元素,结果有 负数的话,两个区间的中间就不在连续了,就不能使用滑动窗口了,因为滑动窗口虽然时间复杂度是 O(N),但是,要求滑动的区间必须是连续的。

像下述,我们可以计算出的整个 数组 全部元素之和;

然后 计算出 中间区间的 元素值之和 也就是 nums_sum - x = target。

right 一直向后迭代,把 right 遍历过的 元素值 都加载一起(用 sum存储 ),如果 sum 值 大于了target,说明 当前区间,right 跑过了,此时 就要让 left ++ ,同时 让 sum 减去 left 遍历过的元素,直到  sum 值 小于或者等于了target。

同时,如果 sum 等于 target,说明此时满足条件,就计算一下 中间连续区间的 元素个数,最后 用 数组总元素 减去 中间连续区间的 元素个数,就是我们想要拿到的 操作数。

注意的是,如果 在开始计算出的 target < 0,说明,当前数组不可能有 满足条件的 子区间。

完整代码:
 

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int nums_sum = 0;
        // 计算整个 数组 全部元素之和
        for(auto& e : nums) nums_sum += e;

        int ret = -1; // 因为如果没有找到,返回 -1,所以初始化为 -1
        int target = nums_sum - x; // 存储 中间连续区间 元素值之和

        // 细节问题
        if(target < 0) return ret; // 说明此时不存在

        for(int left = 0, right = 0, sum = 0; right < nums.size(); right++)
        {
            sum += nums[right];  // right 只需要往后移动 ,不需要往前移动
            while(sum > target)  sum -= nums[left++];
            if(sum == target) 
                ret = max(ret, right - left + 1);
        }
        if(ret == -1) return ret;
        else return nums.size() - ret;// 因为上述计算的是 中间连续区间的 元素个数,所以在最后要减
    }
};

I904. 水果成篮 - 力扣(LeetCode)

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 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] 这五棵树。
 

 上述可以使用滑动窗口,我们发现,其实 上述的一大串题目,翻译过来就是 : 在数组当中找一个 连续的子串,这个字符串满足 只存储了 两种水果。至于这两种说过在 子串当中存储多少个,不用管。

当然暴力解法就是 使用 left 和 right 指针,从开始位置开始一个一个区间的寻找,把全部的满足条件的子串都找出来,然后返回最大子串的 元素个数即可。

对于如何 存储 区间当中 已经存在的水果种类,可以使用 hash 表来存储, 在 C++ 当中就使用       unordered_map 这个 K-V 结构的 哈希表来实现。可以表示 某一种水果在 当前子区间当中存储了多少个。

但是,暴力始终还是太满了,这题,因为是连续的区间,right 和 left 都是向后走的,我们可以使用滑动窗口来解决。

让滑动窗⼝满⾜:窗⼝内⽔果的种类只有两种。

做法:右端⽔果进⼊窗⼝的时候,⽤哈希表统计这个⽔果的频次。这个⽔果进来后,判断哈希表的
⼤⼩:

  • ▪ 如果⼤⼩超过2:说明窗⼝内⽔果种类超过了两种。那么就从左侧开始依次将⽔果划出窗
  • ⼝,直到哈希表的⼤⼩⼩于等于2,然后更新结果;
  • ▪ 如果没有超过2,说明当前窗⼝内⽔果的种类不超过两种,直接更新结果ret。
     

同样是 left 和 right 向后遍历,right先向后迭代,把right遍历过的元素都 进窗口,也就是 把 这个right 变量的元素一个一个的 插入到 hash 表当中;

然后,right 可能会 遍历到 第三种类型的 水果,此时,我们只需要判断 hash 当中有多个 元素(每一个元素代表一个类型的水果)。当 hash 元素大于 2 ,说明此时窗口已经不合格了。

此时需要出窗口,left ++,把left 变量的每一个元素 都从 hash 的元素 记录的 对应类型的水果的计数器 -1 即可。

上述是一个循环,来一直 left++ ,知道 hash 当中的元素个数 == 2,也就是当中窗口合法,那么就可以了。

最后,在每一次 right 向后变量的一次循环当中都更新一下 结果。

完整代码:

使用 unordered_map 容器实现:

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int ,int> uom;
        int ret = -1; // 因为 如果没有应该返回 -1,所以初始化为 -1

        for(int left = 0, right = 0; right < fruits.size(); right++)
        {
            // right 向后迭代,把遍历过的 元素加入到 hash表当中
            std::unordered_map<int, int>::iterator it = uom.find(fruits[right]);
            if(it != uom.end()) it->second++;
            else uom.insert({fruits[right], 1});

            // 此时的窗口已经不合格了 -- 出窗口
            while(uom.size() > 2)
            {
                it = uom.find(fruits[left]);
                it->second--;
                if(it->second == 0) uom.erase(it);
                left++;
            }

            ret = max(ret , right - left + 1); // 每走一步都更新一下 结果
                                                // 之所以不同判断,是因为 在上述while 循环当中
                                                // 已经把 不合格的滑动窗口修改为合格的滑动窗口了

        }
        return ret;
    }
};

使用数组模拟哈希表来实现:

class Solution
{
public:
    int totalFruit(vector<int>& f)
    {
        int hash[100001] = { 0 }; // 统计窗⼝内出现了多少种⽔果
        int ret = 0;
        for (int left = 0, right = 0, kinds = 0; right < f.size(); right++)
        {
            if (hash[f[right]] == 0) kinds++; // 维护⽔果的种类
            hash[f[right]]++; // 进窗⼝
            while (kinds > 2) // 判断
            {
                // 出窗⼝
                hash[f[left]]--;
                if (hash[f[left]] == 0) kinds--;
                left++;
            }
            ret = max(ret, right - left + 1);
        }
        return ret;
    }
};

I438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

给定两个字符串 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" 的异位词。

 首先是使用暴力解法来实现,我们大可以,从数组的第一个字符开始,一个字符一个字符的向后寻找 满足条件的子串,当然,我们要判断两个 我们找出的子串,和目标子串是否满足条件,可以用两个hash表,一个存储我们找出的子串当中 各个字符出现次数;另一个 hash表 存储 目标子串当中 各个字符出现次数。

然后,比较两个hash 表当中 存储的 各个 字符 和 字符出现的次数是否一样,来判断 我们找出的子串 和 目标子串是否满足条件。

暴力解法当中,每遍历到一个 子串 ,就要循环遍历这个子串,计算出这个子串 各个 字符出现的次数。然后在和 记录 目标子串的 hash 表进行比较。

当然,暴力解法时间复杂度 肯定是不可观的。

其实我们发现,我们向后 一个字符一个字符的向后寻找 满足条件的子串,这个子串的 的长度是不变的,而且,管理  左右区间的 left 和 right 都是一直往后迭代的。所以其实这题还可以用  滑动窗口来解决:
 

滑动窗口解决:

不需要再像 暴力解法当中 一个子串一个子串的 暴力枚举,因为 在包里枚举的过程当中,区间是一直向后走的,在暴力解法子串区间向后迭代的过程当中,我们发现,left 向后迭代之前 指向的 元素被 移除的,而 right 新变量的 字符是被添加的,而在 left 和 right 之间的字符 和 字符出现次数(也就是 在 left 和 right 之间 没有被 left 丢弃 和 right 变量的字符 和 字符出现次数) 是不变的 。

所以,在滑动窗口当中,我们每遍历一个 子串,其实没有必要 再 重新 计算一次这个 子串当中的 字符出现的个数。只需要 更新 left  和 right 新 丢弃 或者 变量的 字符 ——> 移除 或者 添加到 hash 表当中即可。

所以,进窗口 其实就是 hash 表当中 对应映射位置的 计数器++;出窗口 就是 hash 表当中 对应映射位置的 计数器--;而出窗口的时机 就是 当 right 向后移动之时,也就是 right - left +  1 > 目标子串.size().

j接下来,是判断 两个哈希表之间的相等关系,可以直接硬比较,因为全部都是小写的 字符,没有大写的,也不是字符串,所以,最多就比较 26 次,也就是常数次就行。

两个两个数组来模拟hash 来进行两个 hash 表的比较。

其实不用,我们还有更好的解决方式:
我们可以用一个 count 记录 有效字符。什么是 有效字符呢?就是在我们所寻找的子串当中,有多少 种类 的字符,是和 目标字符是 一样的。如果有一个 种类 是一样的,count++。

所以,在 存储我们找出的子串当中 各个字符出现次数 的 hash 表当中,依旧按照 滑动窗口 当中的逻辑,进行 计数,只不过,如果是  和 目标字符 hash 当中有重复的,也就是当前的 计数器++ 的是 有效字符,那么就 count++。

当 count == 目标子串.size() 之时,采取更新结果,否则,当前找出的 这个子串 就不满足条件。

完成代码:
 

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int hash1[26] = {0}; // s 当中 各个字符出现的个数 计数器
        int hash2[26] = {0}; // p 当中 各个字符出现的个数的计数器
        vector<int> ret; // 用于记录最大结果,用于返回

        for(auto& e : p) hash2[e - 'a']++; // 计算 p 子串当中的 字符出现的个数

        for(int left = 0, right = 0, count = 0; right < s.size(); right++)
        {
            // 入窗口
             char right_str = s[right]; // 用一个 字符变量存储 当前right指向的字符
             hash1[right_str - 'a']++; // 字符ascll 值 映射 数组下标,计数器++
             if(hash2[right_str - 'a'] >= hash1[right_str - 'a']) count++;

            // 出窗口
            if(right - left + 1 > p.size())
            {
                char left_str = s[left++]; //用一个字符变量存储 当前left指向的字符,left 向后迭代
                if(hash2[left_str - 'a'] >= hash1[left_str - 'a']) count--;
                hash1[left_str - 'a']--; // 计数器-- 
            }

            if(count == p.size())
                ret.push_back(left);
        }

        return ret;

    }
};

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chihiro1122

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值