Leetcode 101 刷题记录
1.1 贪心算法
455. 分发饼干 (简单)
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。所以你应该输出1。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
int i=0;int j=0;
sort(g.begin(),g.end());
sort(s.begin(),s.end());
while(i<g.size()&&j<s.size())
{
if(g[i]<=s[j])i++;
j++;
}
return i;
}
};
135.分发糖果(困难)
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:
Ⅰ、每个孩子至少分配到 1 个糖果。
Ⅱ、相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
示例 1:
输入: [1,0,2]
输出: 5
解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。
示例 2:
输入: [1,2,2]
输出: 4
解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这已满足上述两个条件。
解题思路:把所有孩子的糖果数初始化为 1; 先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的 糖果数加1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数 不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加1。
class Solution {
public:
int candy(vector<int>& ratings) {
int n=ratings.size();
vector<int>candy(n,1);
int sum=0;
for(int i=0;i<n-1;i++)
{
if(ratings[i]<ratings[i+1])candy[i+1]=candy[i]+1;
}
for(int i=n-1;i>0;i--)
{
if(ratings[i]<ratings[i-1])candy[i-1]=max(candy[i-1],candy[i]+1);
}
for(int i=0;i<n;i++)
{
sum+=candy[i];
}
return sum;
}
};
435. 无重叠区间(中等)
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
1、可以认为区间的终点总是大于它的起点。
2、区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
解题思路:把数组按照右边界大小排序,判断前一个数组的右边界小于后一个数组的左边界即需要移除前一个数组,result++。
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.empty())return 0;
sort(intervals.begin(),intervals.end(),[](vector<int>a,vector<int>b){return a[1]<b[1];});
int n=intervals.size();
int prev=intervals[0][1];
int result=0;
for(int i=1;i<n;++i)
{
if(intervals[i][0]<prev)
{
result++;
}
else prev=intervals[i][1];
}
return result;
}
};
605、种花问题(简单)
假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。
示例 1:
输入: flowerbed = [1,0,0,0,1], n = 1
输出: True
示例 2:
输入: flowerbed = [1,0,0,0,1], n = 2
输出: False
注意:
1、数组内已种好的花不会违反种植规则。
2、输入的数组长度范围为 [1, 20000]。
3、n 是非负整数,且不会超过输入数组的大小。
class Solution {
public:
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
int num=0;
for(int i=0;i<flowerbed.size();i++)
{
if((i==0||flowerbed[i-1]==0)&&(i==flowerbed.size()-1||flowerbed[i+1]==0)&&flowerbed[i]==0)
{
num++;
flowerbed[i]=1;
}
}
return num>=n;
}
};
452、 用最少数量的箭引爆气球(中等)
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解题思路:同435
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
if(points.empty())return 0;
sort(points.begin(),points.end(),[](const vector<int>&a,const vector<int>&b)
{return a[1]<b[1];});//加上const &会大大降低运行时间
int m=1,prev=points[0][1];
for(int i=1;i<points.size();i++)
{
if(points[i][0]>prev)
{
prev=points[i][1];
m++;
}
}
return m;
}
};
763、 划分字母区间(中等)
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。
提示:
S的长度在[1, 500]之间。
S只包含小写字母 ‘a’ 到 ‘z’ 。
解题思路:先对每一个字母的最后一次出现的位置进行统计,记录在哈希表中,当i移动到前历字母末位值中最大值时,记录为一段无重复片段
class Solution {
public:
vector<int> partitionLabels(string S) {
unordered_map<char,int>LastIndex;//哈希表字符串末位统计
vector<int>res;
for(int i=0;i<S.size();i++)
{
LastIndex[S[i]]=i;
}
int start=0,end=0;
for(int i=0;i<S.size();i++)
{
end=max(end,LastIndex[S[i]]);
if(end==i)
{
res.push_back(end-start+1);
start=end+1;
}
}
return res;
}
};
122、买卖股票的最佳时机 II (简单)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ans = 0;
int n = prices.size();
for (int i = 1; i < n; ++i) {
ans += max(0, prices[i] - prices[i - 1]);
}
return ans;
}
};
406、 根据身高重建队列(中等)
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
解题思路:从低往高排
class Solution {
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(),people.end(),[](const vector<int>&a,const vector<int>&b){return a[0]<b[0]||(a[0]==b[0]&&a[1]>b[1]);});
int n=people.size();
vector<vector<int>>ans(n);
for(const vector<int>&person:people)
{
int count=person[1]+1;
for(int i=0;i<n;i++)
{
if(ans[i].empty())
{
count--;
if(count==0)
{
ans[i]=person;
break;
}
}
}
}
return ans;
}
};
解题思路:从高往低排
class Solution {
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(),people.end(),[](const vector<int>&a,const vector<int>&b){return a[0]>b[0]||(a[0]==b[0]&&a[1]<b[1]);});
vector<vector<int>>ans;
for(const vector<int>&person:people)
{
ans.insert(ans.begin()+person[1],person);
}
return ans;
}
};
665、非递减数列
给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。
我们是这样定义一个非递减数列的: 对于数组中所有的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。
示例 1:
输入: nums = [4,2,3]
输出: true
解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。
示例 2:
输入: nums = [4,2,1]
输出: false
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。
双指针
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多 个数组的多个指针。 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的 区域即为当前的窗口),经常用于区间搜索。 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是 排好序的。
167、 两数之和 II - 输入有序数组
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int index1=0,index2=numbers.size()-1;
while(index1<index2)
{
if(numbers[index1]+numbers[index2]==target)
{
break;
}
else if(numbers[index1]+numbers[index2]>target)
{
index2--;
}
else {
index1++;
}
}
vector<int>ans={index1+1,index2+1};
return ans;
}
};
88、合并两个有序数组
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明:
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例:
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int l=0,h=0;
vector<int>nums3(m+1,0);
for(int i=0;i<m;i++)
{
nums3[i]=nums1[i];
}
while(l<m&&h<n)
{
if(nums2[h]<nums3[l])
{
nums1[l+h]=nums2[h];
h++;
}
else
{
nums1[l+h]=nums3[l];
l++;
}
}
while(l<m)
{
nums1[l+h]=nums3[l];
l++;
}
while(h<n)
{
nums1[l+h]=nums2[h];
h++;
}
}
};
76、最小覆盖子串(困难)
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
示例 2:
输入:s = “a”, t = “a”
输出:“a”
class Solution {
public:
string minWindow(string s, string t) {
vector<int> chars(128,0);
vector<bool>flag(128,false);
for(int i=0;i<t.size();i++)
{
flag[t[i]]=true;
++chars[t[i]];
}
int l=0,min_l=0,cnt=0,min_size=s.size()+1;
for(int r=0;r<s.size();r++)
{
if(flag[s[r]])
{
if(--chars[s[r]]>=0)cnt++;
}
while(cnt==t.size())
{
if(r-l+1<min_size)
{
min_l=l;
min_size=r-l+1;
}
if(flag[s[l]]&&++chars[s[l]]>0)
{
--cnt;
}
++l;
}
}
return min_size>s.size()? "":s.substr(min_l,min_size);
}
};
二分法
二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取
一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。
举例来说,给定一个排好序的数组 {3,4,5,6,7},我们希望查找 4 在不在这个数组内。第一次折半时考虑中位数 5,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一半。于是我们的查找区间变成了 {3,4,5}。(注意,根据具体情况和您的刷题习惯,这里的 5 可以保留也可以不保留,并不影响时间复杂度的级别。)第二次折半时考虑新的中位数 4,正好是我们需要查找的数字。于是我们发现,对于一个长度为 5 的数组,我们只进行了 2 次查找。如果是遍历数组,最坏的情况则需要查找 5 次。
我们也可以用更加数学的方式定义二分查找。给定一个在 [a, b] 区间内的单调函数 f (x),若f (a) 和 f (b) 正负性相反,那么必定存在一个解 c,使得 f © = 0。在上个例子中,f (x) 是离散函数f (x) = x +2,查找 4 是否存在等价于求 f (x) −4 = 0 是否有离散解。因为 f (1) −4 = 3−4 = −1 < 0、f (5) − 4 = 7 − 4 = 3 > 0,且函数在区间内单调递增,因此我们可以利用二分查找求解。如果最后二分到了不能再分的情况,如只剩一个数字,且剩余区间里不存在满足条件的解,则说明不存在离散解,即 4 不在这个数组内。
具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍,第一是尝试熟练使用一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。
69、x 的平方根
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
class Solution {
public:
int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long long)mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
};
34、 在排序数组中查找元素的第一个和最后一个位置(中等)
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.size()==0)return {-1,-1};
int lo=0,hi=nums.size()-1;
int mid=0;
while(lo<hi)
{
mid=(lo+hi-1)/2;
if(nums[mid]<target)lo=mid+1;
else if(nums[mid]>target)hi=mid-1;
else hi=mid;
}
int l_left=lo;
lo=0,hi=nums.size()-1;
while(lo<hi)
{
mid=(lo+hi+1)/2;
if(nums[mid]<target)lo=mid+1;
else if(nums[mid]>target)hi=mid-1;
else lo=mid;
}
int h_right=lo;
if(nums[l_left]==target&&nums[h_right]==target)
{
return {l_left,h_right};
}
else return {-1,-1};
}
};
链表
(单) 链表是由节点和指针构成的数据结构,每个节点存有一个值,和一个指向下一个节点
的指针,因此很多链表问题可以用递归来处理。不同于数组,链表并不能直接获取任意节点的值,必须要通过指针找到该节点后才能获取其值。同理,在未遍历到链表结尾时,我们也无法知道链表的长度,除非依赖其他数据结构储存长度。 LeetCode 默认的链表表示方法如下。
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
由于在进行链表操作时,尤其是删除节点时,经常会因为对当前节点进行操作而导致内存或
指针出现问题。有两个小技巧可以解决这个问题:一是尽量处理当前节点的下一个节点而非当前节点本身,二是建立一个虚拟节点 (dummy node),使其指向当前链表的头节点,这样即使原链表所有节点全被删除,也会有一个 dummy 存在,返回 dummy->next 即可。
206、反转链表(简单)
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
非递归:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head){return head;}
ListNode* cur=NULL;
ListNode* pre=head;
while(pre!=NULL)
{
ListNode* t=pre->next;
pre->next=cur;
cur=pre;
pre=t;
}
return cur;
}
};
递归:
class Solution {
public:
ListNode* reverse(ListNode* head,ListNode* pre)
{
if(!head){return pre;}
ListNode* next=head->next;
head->next=pre;
return reverse(next,head);
}
ListNode* reverseList(ListNode* head) {
return reverse(head,NULL);
}
};
21、合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1==NULL){return l2;}
else if(l2==NULL){return l1;}
if(l1->val<l2->val)
{
l1->next=mergeTwoLists(l1->next,l2);
return l1;
}
else{
l2->next=mergeTwoLists(l1,l2->next);
return l2;
}
}
};