LeetCode刷题

本文介绍了贪心算法在解决分配问题(如分发饼干、糖果和种花)、区间问题(如无重叠区间和箭引爆气球)、以及使用双指针(包括两数之和、归并有序数组、环形链表和滑动窗口)和二分查找(如开方和查找区间)中的应用。通过实例解析展示了如何巧妙利用贪心策略和特定数据结构优化解决方案。
摘要由CSDN通过智能技术生成

第一章 贪心算法

算法解释:顾名思义,贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。

1.1 分配问题

455.分发饼干

有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃 一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子 可以吃饱。

输入输出样例:

输入两个数组,分别代表孩子的饥饿度和饼干的大小。输出最多有多少孩子可以吃饱的数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子。为了尽量使得剩下的饼干可以满足饥饿度更大的孩子,所以我们应该把大于等于这个孩子饥饿度的、且大小最小的饼干给这个孩子。满足了这个孩子之后,我们采取同样的策略,考虑剩下孩子里饥饿度最小的孩子,直到没有满足条件的饼干存在。

简而言之,这里的贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。

因为我们需要获得大小关系,一个便捷的方法就是把孩子和饼干分别排序。 这样我们就可以从饥饿度最小的孩子和大小最小的饼干出发,计算有多少个对子可以满足条件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

135.分发糖果

一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比自己身旁的一个孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少个糖果。

输入输出样例:

输入是一个数组,表示孩子的评分。输出是最少糖果的数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个样例中,最少的糖果分法是 [2,1,2]。

题解

虽然这一 道题也是运用贪心策略,但我们只需要简单的两次遍历即可:

  1. 把所有孩子的糖果数初始化为 1;
  2. 先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1。
  3. 再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。

通过这两次遍历,分配的糖果就可以满足题目要求了。这里的贪心策略即为,在每次遍历中,只考虑并更新相邻一 侧的大小关系。在样例中,我们初始化糖果分配为 [1,1,1],第一次遍历更新后的结果为 [1,1,2],第二次遍历 更新后的结果为[2,1,2]。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

605.种花问题

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组flowerbed 表示花坛,由若干 01 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

这里在一次循环遍历中判断能否种花。

如果一个地方可以种花,意味着该地方周围都没有种植花,也就是数组中可以种花的位置,左右两边也都是0。

所以在循环中一次性判断三个条件,当前下标,当前下标-1,当前下标+1。但是数组的第一个和最后一个元素也要判断,边界情况只需要判断两个值即可。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.2 区间问题

435.无重叠区间

给定多个区间,计算让这些区间互不重叠所需要移除区间的最少个数。起止相连不算重叠。

输入输出样例:

输入是一个数组,数组由多个长度固定为 2 的数组组成,表示区间的开始和结尾。输出一个整数,表示需要移除的区间数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个样例中,我们可以移除区间 [1,3],使得剩余的区间 [[1,2], [2,4]] 互不重叠。

题解

求最少的移除区间个数,等价于尽量多的保留不重叠的区间。

因此在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。

因此,我们采取的贪心策略为:优先保留结尾小且不相交的区间。 具体实现方法为:

先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。我们这里使用 C++ 的 Lambda,结合 std::sort() 函数进行自定义排序。

在样例中,排序后的数组为 [[1,2], [1,3], [2,4]]。按照我们的贪心策略,首先初始化为区间 [1,2];由于 [1,3] 与 [1,2] 相交(这里[1,3]区间的起始位置1小于[1,2]区间的结尾位置2,因此删除),我们跳过该区间;由于 [2,4] 与 [1,2] 不相交,我们将其保留。因此最终保留的区间为 [[1,2], [2,4]]。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

452.用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend,且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

这道题其实和435.无重叠区间相似,那道题是判断区间是否重叠,如果重叠就删去。这道题则是判断区间是否重叠,如果重叠说明可以用一支箭射穿气球。

那么思路还是一样,首先我需要把区间按照右端排序,在开始选择排序后的第一个区间作为起始判断依据。

判断条件是,如果遍历的当前区间的 左端点 <= 我选择的区间的右端点 ,说明这两个区间有重叠部分,那么continue。

如果不满足条件,那么count++,同时当前判断依据更新为当前所遍历的区间的右端点值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

763.划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

返回一个表示每个字符串片段的长度的列表。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

aabbccddeeffggiijjjkkkll

122.买卖股票的最佳时机II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

第二章 双指针

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。

也可以延伸到多个数组的多个指针。

若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

对于 C++ 语言,指针还可以玩出很多新的花样。一些常见的关于指针的操作如下:

指针与常量

int x;
int* p1 = &x; // 指针可以被修改,值也可以被修改
const int * p2 = &x; // 指针可以被修改,值不可以被修改(const int)
int* const p3 = &x; // 指针不可以被修改(* const),值可以被修改
const int* const p4 = &x; // 指针不可以被修改,值也不可以被修改

指针函数与函数指针

// addition是指针函数,一个返回类型是指针的函数
int* addition(int a, int b) 
{
    int* sum = new int(a + b);
    return sum;
}
//subtraction是普通函数,返回两个变量的减法
int subtraction(int a, int b) 
{
	return a - b;
}
//operation的第三个参数是一个返回值为int,参数为两个int的函数指针
int operation(int x, int y, int (*func)(int, int)) 
{
	return (*func)(x,y);
}
// minus是函数指针,指向函数的指针
int (*minus)(int, int) = subtraction;
int* m = addition(1, 2);
int n = operation(3, *m, minus);

2.1 Two Sum

167.两数之和

在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。

输入输出样例:

输入是一个数组(numbers)和一个给定值(target)。输出是两个数的位置,从 1 开始计数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

因为数组已经排好序,我们可以采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。

指针移动条件:

如果两个指针指向元素的和等于给定值,那么它们就是我们要的结果。如果两个指针指向元素的和小于给定值,我们把左边的指针右移一位,使得当前的和增加一点。如果两个指针指向元素的和大于给定值,我们把右边的指针左移一位,使得当前的和减少一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.2 归并两个有序数组

88.归并两个有序数组

给定两个有序数组,把两个数组合并为一个。

输入输出样例:

输入是两个数组和它们分别的长度 m 和 n。其中第一个数组的长度被延长至 m + n,多出的 n 位被 0 填补。题目要求把第二个数组归并到第一个数组上,不需要开辟额外空间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即nums1的m − 1位和nums2的n − 1位。每次将较大的那个数字复制到nums1的后边,然后向前移动一位。 因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.3 快慢指针

142.环形链表II

给定一个链表,如果有环路,找出环路的开始点

输入输出样例:

输入是一个链表,输出是链表的一个节点。如果没有环路,返回一个空指针。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd 判圈法)。给定两个指针, 分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast 可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并 让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.4 滑动窗口

76.最小覆盖子串

给定两个字符串 S 和 T,求 S 中包含 T 所有字符的最短连续子字符串的长度,同时要求时间 复杂度不得超过 O(n)。

输入输出样例:

输入是两个字符串 S 和 T,输出是一个 S 字符串的子串。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个样例中,S 中同时包含一个 A、一个 B、一个 C 的最短子字符串是“BANC”。

题解
string minWindow(string s, string t)
{
    vector<int>chars(128,0);
    vector<bool>exist(128,false);
    //初始化统计字符情况的数组
    for (int i = 0; i < t.size(); i++)
    {
        chars[t[i]]++;
        exist[t[i]] = true;
    }
    //移动滑动窗口
    int l = 0, r = 0, min_len = s.size() + 1;
    int min_l = 0;
    int count = 0;
    for (; r < s.size(); r++)
    {
        if (exist[s[r]])
        {
            if (--chars[s[r]] >= 0)
            {
                count++;
            }
        }
        // 若目前滑动窗口已包含t中全部字符,
        // 则尝试将l右移,在不影响结果的情况下获得最短子字符串
        while (count == t.size())
        {
            if ((r - l + 1) < min_len)
            {
                min_len = r - l + 1;
                min_l = l;
            }
            if (exist[s[l]] && ++chars[s[l]]> 0)
            {
                --count;
            }
            l++;
        }
    }
    return min_len < s.size() + 1 ? s.substr(min_l, min_len) : "";
}

第三章 二分查找

二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复 杂度为 O(log n)。

举例来说,给定一个排好序的数组 {3,4,5,6,7},我们希望查找 4 在不在这个数组内。第一次 折半时考虑中位数 5,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一 半。于是我们的查找区间变成了 {3,4,5}。第二次折半时考虑新的中位数 4,正好是我们需要查找的数字。于是我们发现,对于一个长度为5的数组,我们只进行了2次查找。如果是遍历数组,最坏的情况则需要查找5次。

具体到代码上,二分查找时 区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍,第一是尝试熟练使用 一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件), 尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。

3.1 求开方

69.X的平方根

给定一个非负整数,求它的开方,向下取整。

输入输出样例:

输入一个整数,输出一个整数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8 的开方结果是 2.82842…,向下取整即是 2。

题解

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.2查找区间

34.在排序数组中查找元素第一个和最后一个位置

给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。

输入输出样例:

输入是一个数组和一个值,输出为该值第一次出现的位置和最后一次出现的位置(从0开始);如果不存在该值,则两个返回值都设为-1。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题解

整数,输出一个整数。

[外链图片转存中…(img-KOpdToHB-1709605102902)]

8 的开方结果是 2.82842…,向下取整即是 2。

题解

[外链图片转存中…(img-XlauXEQg-1709605102902)]

3.2查找区间

34.在排序数组中查找元素第一个和最后一个位置

给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。

输入输出样例:

输入是一个数组和一个值,输出为该值第一次出现的位置和最后一次出现的位置(从0开始);如果不存在该值,则两个返回值都设为-1。

[外链图片转存中…(img-7A3J7YKO-1709605102902)]

题解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值