1. 两数之和(哈希表)
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出和为目标值
的那两个
整数,并返回它们的数组下标
。数据保证有且仅有一组解。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 103
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案
这个题目保证一定有解,且只有一个答案。
c++中map的底层实现原理是平衡树,插入删除操作时间复杂度是O(logn);
而unordered_map的底层实现原理是哈希表,插入删除操作时间复杂度是O(1);
map说明参考
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> heap; //定义一个c++哈希表,第一个int代表数值,第二个代表下标,即完成数值到下标的映射。
for(int i=0;i<nums.size();i++){ //从数组第一个数开始遍历第二个数,
int r=target-nums[i]; //令r=target-第二个数数值,即r代表的是我们需要的第1个数的数值。
if(heap.count(r))return{heap[r],i}; //heap.count(r)代表如果数组中所需的第一个数的数量不空,即存在第一个数,我们就返回heap[r],即代表r数值对应的下标;i代表第二个数的下标。
else heap[nums[i]]=i; //否则我们就把这个数插入到哈希表中,nums[i]代表数值,heap[nums[i]]=i即把nums[i]对应的下标映射到i。
}
return {}; //如果不存在就返回空,其实没必要写,但是力扣题目不写就会有警告。
}
};
2021年8月14日21:34:10:
//把数组中的数放到哈希表中,我们枚举第二个数,当扫描到Si的时候,前面的S0,S1...Si-1均已经存在于哈希表中了,我们访问Si的时候,
//我们看一下哈希表中是否有target-Si这个数,如果存在,就找到了答案,否则就把Si放到哈希表中,哈希表中记录的是数值与位置的对应关系
//注意哈希表中记录的就是数组中数值与其下标位置的对应,注意题目已经保证了只存在一组答案,所以不会存在两个相同的数使得后一个把前一个数的位置顶替的情况,这一点太重要了
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map=new HashMap<>(); //记录数组中的数值与其下标位置的对应关系
for(int i=0;i<nums.length;i++){
int a=target-nums[i]; //a是要寻找的前一个数
if(map.containsKey(a)){
return new int[]{i,map.get(a)}; //返回答案
}else{
map.put(nums[i],i); //否则就把当前数加到哈希表中
}
}
return new int[]{}; //返回空数组的情况
}
}
4. 寻找两个正序数组的中位数
给定两个大小为
m
和n
的正序
(从小到大
)数组 nums1
和nums2
。请你找出并返回这两个正序数组的中位数。进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
示例 3:
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000
示例 4:
输入:nums1 = [], nums2 = [1]
输出:1.00000
示例 5:
输入:nums1 = [2], nums2 = []
输出:2.00000
提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-106 <= nums1[i], nums2[i] <= 106
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int tot=nums1.size()+nums2.size(); //记录两个数组的总长度;注意.size()时间复杂度是O(1)
if(tot%2==0){ //如果总长度为偶数,
int left=find(nums1,0,nums2,0,tot/2); //寻找中间偏左数,
int right=find(nums1,0,nums2,0,tot/2+1); //寻找中间偏右数,
return (left+right)/2.0; //注意要除以2.0,因为结果让我们求的是带小数的结果;
}else return find(nums1,0,nums2,0,tot/2+1); //如果总长度是奇数,返回中间的数即可。
}
int find(vector<int>& nums1,int i,vector<int>& nums2,int j,int k){
if(nums1.size()-i>nums2.size()-j) return find(nums2,j,nums1,i,k); //如果递归过程中,第一个数组的大小大于了第二个数组的大小,就交换两个数组的地位;
if(k==1){ //如果递归过程中,两个数组只剩下了一个元素,
if(nums1.size()==i) return nums2[j]; //如果短数组元素为空就j返回第二个数组元素
else return min(nums1[i],nums2[j]); //否则返回两个数组的最小值。
}
if(nums1.size()==i) return nums2[j+k-1]; //如果递归过程中,短数组元素已空就返回长数组的第k个元素,
int si=min((int)nums1.size(),i+k/2),sj=j+k-k/2; //定义两个指针。
if(nums1[si-1]>nums2[sj-1]) return find(nums1,i,nums2,sj,k-(sj-j)); //看图,如果数组1的第k/2的元素大于数组2的第k/2个元素,就把数组2的前k/2个元素删除。
else return find(nums1,si,nums2,j,k-(si-i)); //看图,如果数组1的第k/2的元素小于或者等于数组2的第k/2个元素,就把数组1的前k/2个元素删除。
}
};
二刷第四题:
class Solution {
public:
//这个题目可以先合并两个数组,再sort一遍,如果总个数是奇数个则答案就是最中间的那个数,如果是偶数个则答案就是最中间两个数的平均数。时:O(nlogn)
//注意题目两个数组都是从小到大排好序的,所以我们可能想一个更好的方法,下面就用到了递归来解决
//我们现在需要能求出来两个数组从小到大排,第k个数是多少(第一个数是最小值,第二个数是次小值,以此类推...)?我们只需要让k=(m+n)/2即可,
//这个题目我们使用递归解决,我们分别考虑两个数组的第k/2个数,此时一共有三种情况:
//注意下面个数是从1开始枚举的,而数组的下标是从0开始的,
//1.如果a[k/2]<b[k/2],则在a数组中一共有k/2个元素小于a[k/2],在b数组中小于a[k/2]的元素个数一定小于k/2个,所以总共来看,小于a[k/2]的元素个数一定是小于k个的,
//则a数组从开始到k/2元素一定不可能是答案,所以我们就可以把这一部分给删掉(即是忽略掉这一部分,并不是真正意义上的删除)
//2.如果b[k/2]<a[k<2],同样道理,可以忽略掉b数组的前面一直到k/2元素
//3.如果a[k/2]==b[k/2],则a数组中小于a[k/2]的元素个数共k/2-1个,同理b数组中小于a[k/2](即b[k/2])的元素个数共k/2-1个,则b[k/2]或者a[k/2]就是第k个数,
//最后如果k等于了1,则说明我们找到了要求的数,我们返回两个数组中最小的那个数就是要求的数
//我们每次都可以去掉k/2个元素,最多递归logk次,所以时间复杂度是O(log(n+m))
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int n=nums1.size(),m=nums2.size(); //求出两个数组的长度
int tot=n+m;
if(tot%2==0){ //如果是偶数个元素,则结果是最中间两个数的平均数
//注意y总的k是从1开始的,所以第五个参数都没有减一,比如合并之后是[61,71,81,91],共4个数,则中间靠左数是4/2=2.即第二个即是71,中间靠右数是4/2+1=3,即是81
//如果合并之后是[61,71,81],则最中间是3/2+1=2,即是71
int left=find(nums1,0,nums2,0,tot/2); //求出最中间两个数里靠左的那一个
int right=find(nums1,0,nums2,0,tot/2+1); //求出最中间两个数里靠右的那一个
return (left+right)/2.0; //最后返回的是两个数的平均数,注意除以的是2.0
}else{ //如果个数是奇数个,就要求的是最中间的那个数
return find(nums1,0,nums2,0,tot/2+1);
}
}
//个数从1开始,数组下标从0开始
//下面编写find函数,目的是求出第k个数是多少
//find函数第一个参数是数组a,第二个参数是此时数组a中除去被忽略的元素之后的那一个元素的下标(注意这个下标是从0开始枚举的)
//第三个参数是数组b,第四个参数是同上,只不过是数组b的,第五个参数是合并两个数组之后我们要找到的第多少个数(从1开始)
int find(vector<int>& nums1,int i,vector<int>& nums2,int j,int k){
//为了简便我们假定第一个数组的长度小于第二个数组的长度,如果不满足我们就交换两个数组的地位,之后再进行递归
if(nums1.size()-i>nums2.size()-j) return find(nums2,j,nums1,i,k);
//即是说如果第一个数组剩余的元素个数大于第二个元素的剩余个数的话,就交换两个数组的地位之后再继续进行递归,
//递归结束条件:
if(k==1){ //即是说此时如果我们要找的数是第一个数的话就说明我们找到了我们要找的那个数
if(nums1.size()==i) return nums2[j]; //注意,因为nums1数组较短,所以a数组有可能递归到空;如果a数组为空(即nums1.size()==i),
//我们直接返回第二个数组的此时的起始位置的数字即是要找的数
else return min(nums1[i],nums2[j]); //否则就返回两个数组的中的较小值即是第k个数
}
//否则就需要进行正常的递归过程:
if(nums1.size()==i) return nums2[j+k-1]; //如果在递归的过程中,较短数组已经为空,我们就返回长数组的第k个元素,因为数组下标是从0开始的,所以这里要减一
int si=min((int)nums1.size(),i+k/2),sj=j+(k-k/2); //si,sj分别代表此时两个数组的第k/2个位置,因为k有可能为奇数,所以sj是k-k/2,
//这里因为数组1较短,所以i+k/2有可能越界,所以这里si最大取到nums1.size(),而sj不可能超过数组j的长度
if(nums1[si-1]>nums2[sj-1]) return find(nums1,i,nums2,sj,k-(sj-j)); //即是如果a[k/2]>b[k/2],则要从数组b中去掉前(sj-j)个元素,因为数组下标是从0开始的,所以要是si-1,sj-1,继续往下递归
else return find(nums1,si,nums2,j,k-(si-i)); //同理如果a[k/2]<b[k/2],则也要继续往下递归,且去掉数组a的前(si-i)个元素。
//注意这里是else,而不是else if(nums[si-1]<nums2[sj-1]),因为这样(即只写else)就把nums1[si-1]==nums2[sj-1]这种情况也包含进去了
}
};
疑惑点:
1.y总,这里21行为什么要加(int)进行强转?
vector数组中size函数的返回值是size_type ,而 size_type 的类型是:unsigned
int,min接受int型参数,所以需要强转
2.y总,这里的k为什么要从1开始呢?是不是为了边界情况比较好处理。另外就是这种从0开始从1开始有没有什么经验之谈呢
这里从0开始还是从1开始都是可以的。比较看经验,有些题目从0开始会简单点,有些从1开始会简单点。
3.y总,为什么我把相等的情况单独拿出来就过不了了,就像这样if (nums1[si - 1] == nums2[sj - 1]) return nums1[si - 1];,求解答~
存在一种边界情况:nums1[]中的数不足k / 2个,那么此时si + sj < k,所以当前数就不是第k个数了
2021年8月15日11:20:25:
算法分析
给定两个有序的数组,找中位数(n + m) / 2
,等价于找第k
**小
**的元素,k = (n + m) / 2
- 1、当一共有偶数个数时,找到第
total / 2
小left
和第total / 2 + 1
小right
,结果是(left + right / 2.0)
- 2、当一共有
**奇数个数**
时,找到第total / 2 + 1
小,即为结果
如何找第k
小?
- 1、默认第一个数组比第二个数组的有效长度小,如果判断出来了不是这样的就交换两个数组的位置。
- 2、第一个数组的有效长度从
i
开始,第二个数组的有效长度从j
开始,其中[i,si - 1]
是第一个数组的前k / 2
个元素,[j, sj - 1]
是第二个数组的前k - k / 2
个元素 - 3、当
nums1[si - 1] > nums2[sj - 1]
时,则表示第k
小一定在[i,n]
与[sj,m]
中 - 4、当
nums1[si - 1] <= nums2[sj - 1]
时,则表示第k小
一定在[si,n]
与[j,m]
中
递归:
时间复杂度 Olog(n+m)
//给定两个有序的数组,找中位数(n + m) / 2,等价于找第k小的元素:k = (n + m) / 2
//我们这样考虑:对于两个有序数组a,b而言,令k=(m+n)/2,我们先看一下a[k/2]和b[k/2]的大小关系,1.如果a[k/2]<b[k/2],则将a数组的前k/2个元素和b的前k/2个元素按大小排完序之后,
//a数组的前k/2个元素肯定不是答案,这部分数据我们就可以不用考虑了。2.如果a[k/2]>b[k/2],则b数组的前k/2个元素肯定不是答案,b数组的前k/2个元素可以不用考虑了
//3.如果a[k/2]==b[k/2],两个值相同,就说明a[k/2]或者b[k/2]就是第k个元素,就找到答案了,
//所以我们每次都可以删除k/2个元素,时间复杂度是log(m+n),注意考虑m+n的就问题,如果是奇数,中位数正好是两个数组排好序之后中间数,如果是偶数,是中间两个数的平均数
//注意并不是真正的删除,而是用下标记录现在查找到第几个数,前面的数不考虑了,近似达到“删除”的效果,当k被删除到了1就找到了答案
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int tot=nums1.length+nums2.length; //求出两个数组的总长度
if(tot%2==0){ //如果总长度是偶数
int left=find(nums1,0,nums2,0,tot/2); //中间左边那个数是两个数组排完序之后第tot/2个数(注意第几个数是从1开始的),
int right=find(nums1,0,nums2,0,tot/2+1); //中间右边那个数是两个数组排完序之后第tot/2+1个数
return (left+right)/2.0; //最后返回两个数的平均数,注意除以2.0
}else{ //否则的话,两个数组的总长度就是奇数个
return find(nums1,0,nums2,0,tot/2+1); //要找的是第tot/2+1个数(第几个数下标是从1开始的)
}
}
//下面编写find函数,find函数的作用就是寻找两个数组从0开始的第几个数的数值
public int find(int[] nums1,int i,int[] nums2,int j,int k){ //find函数的第2,4个参数是现在查找到数组的哪一个下标,最后一个参数是现在查找到第几个数了
//为了方便,我们假定现在被“删除”之后的数组1的长度小于数组2的长度,如果不满足我们就交换一下两个数组的位置
if(nums1.length-i>nums2.length-j) return find(nums2,j,nums1,i,k);
//边界问题,
//当第一个数组已经用完,就返会第二个数组中的第k/2个数即可,注意数组中是下标,是从0开始的,而第几个数是从1开始的
if(nums1.length == i) return nums2[j + k - 1];
//当取第1小元素元素,注意上面的id已经把第一个数组为空的情况排除掉了,所以我们现在要返回的就是两个数组中的较小值
if(k == 1) return Math.min(nums1[i],nums2[j]);
int si=Math.min(nums1.length,i+k/2),sj=j+k-k/2; //分别求出两个数组的第k/2个元素,由于数组1比较短,所以i+k/2可能越界,所以要和其长度取较小值
if(nums1[si-1]>nums2[sj-1]){ //当数组1的第k/2个元素大于数组2,就去掉数组2的前面的元素
return find(nums1,i,nums2,sj,k-(sj-j)); //删除数组的前sj-j个元素
}else{
return find(nums1,si,nums2,j,k-(si-i));
}
}
}
31. 下一个排列(指针)
实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
示例 4:
输入:nums = [1]
输出:[1]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100
代码:
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int k=nums.size()-1; //从最后一个元素开始查找第一个出现降序的位置。
while(k>0&&nums[k-1]>=nums[k]) k--; //找到例子中的7这个点。,注意这里是nums[k-1]>=nums[k]即先判断,再移动过去,即探雷游戏,最后k到达的是7这个点,不是4,这一点一定要搞清,否则下面将会错一堆。
if(k<=0) {
reverse(nums.begin(),nums.end());//找到最后发现k<=0了,说明整个序列都是降序,下一个排列就是最小的序列,也可以写sort但复杂度会变高。
}else{ //否则的话k和k-1就是第一个非降序(升序)的关系,注意这里else语句一定要写上,否则出现k<=0的情况的也会执行下面这些语句,将会出现数组下标越界错误。
int t=k; //我们此时需要找到第一个大于k-1的元素
while(t<nums.size()&&nums[t]>nums[k-1])t++; //只要还没有找到大于k-1的元素,就往后遍历查找(将t++),t-1就是我们要找到的例子中的5
swap(nums[t-1],nums[k-1]); //找到了交换4和5的位置。
reverse(nums.begin()+k,nums.end()); //从k到最后一个元素进行逆序排列。k就是下标,不用再计算下标,也可以写sort,时间复杂度会变高。
//返回为空void,最后不用返回任何元素
}
}
};
2021年8月24日19:45:23:
//即是要我们返回排列的下一个,注意不需要返回,只需要修改即可,
class Solution {
public void reverse(int[] nums,int l,int r){
while(l<r){
int t=nums[l];
nums[l]=nums[r];
nums[r]=t;
l++;r--;
}
}
public void nextPermutation(int[] nums) {
int n=nums.length;
int k=n-1; //从后面往前找第一个升序的数对
while(k>0&&nums[k-1]>=nums[k]) k--; //只要前面的元素还大于等于当前元素,当前元素就要往前移动,注意k>0这个条件一定不要忘记写,否则会下标越界
if(k<=0){ //如果k变到了0,说明整个数组都是降序关系,我们反转整个数组即可
reverse(nums,0,n-1); //交换一下后面的元素即是所求的下一个排列
}else{ //否则就要按照图上的说明进行操作,此时k是到达了图上a的位置,即是极点的位置,k-1即是图上b的位置
int b=k-1;
int t=k; //t用于找到大于b并且最靠近于b的数
while(t<n&&nums[t]>nums[b]) t++; //只要t位置上的数还比b大我们就往后走,当t停下来的时候,
//t前面的t-1即是我们要找的数,因为我们要找的是大于b的数,t此时是小于b的,而t-1才是满足要求的,我们交换nums[t-1]和nums[b]
//注意while条件中的t<b也不要忘记写,否则也会下标越界
int x=nums[t-1]; //交换nums[t-1]和nums[b]
nums[t-1]=nums[b];
nums[b]=x;
//之后将a已经其后面的数交换即可
reverse(nums,k,n-1);
}
}
}
也可以使用二分找t-1那个点:
class Solution {
public:
void nextPermutation(vector<int>& nums) {
if (nums.empty()) return;
int n = nums.size();
int i = n - 2;
while (i >= 0 && nums[i + 1] <= nums[i]) --i;
if (i < 0) {
reverse(nums.begin(), nums.end());
return;
}
int l = i + 1, r = n - 1, target = nums[i];
while (l <= r){
int mid = l + (r - l) / 2;
if (nums[mid] <= target) r = mid - 1;
else l = mid + 1;
}
swap(nums[i], nums[r]);
reverse(nums.begin() + i + 1, nums.end());
}
};
2021年10月24日10:13:23:
class Solution {
//字典序最小就意味着数值最小,也就是说我们应该尽可能保证前面的位数不变,只变后面的几位,即尽量保证高位不变,只变低位,这样才是比当前排列大的最小的那一个
//因此我们由此得到启发:应该是从后往前考虑,我们应该是从数组的后面往前走到第一个非降序(升序)的位置(比如23541的3),即其后面的位置的数是降序的关系(不要求是严格降序即54441也是可以的)
//而这个数比其后一个相邻的数要小(即5比3要小,而541是降序的关系),我们就可以发现这一位是可以变大一些的(即前面尽量保持不变,从后面往前第一个可以变大的数)
//因为其后面有些数是比当前这一位上的数要大的,我们就可以将当前这一位上的数变大一些,而这一位应该变大成其后面比当前这一位大的最小的那个数(即541的4)
//因为我们要求的是字典序最小的,然后把稍大的那个数字放到这一位上,即两个数交换一下位置,而交换完之后后面的数字应该是越小越好,即把后面的数字变成升序排序,这样就可以得到下一个排列了
//我们使用23541来模拟一下上面的过程,我们先找到从后往前第一个升序(非降序)的位置,即3和5,因为3<5,而5后面的数都是降序,然后在3后面找到第一个比3大的最小的那个数即是4
//然后把3和4交换一下位置,换完之后然后把4后面的531升序排序,得到135,这样最后的结果就是24135这样就是最小的
//这样整个数组最多被扫描三次,时间复杂度就是O(n)的
public void nextPermutation(int[] nums) {
int n=nums.length;
int k=n-1; //k用来找到从后往前的第一个非降序(升序)的位置,
while(k>0&&nums[k-1]>=nums[k]) k--; //这样k就到达的23541的5的位置,注意这里为了防止下标越界,应该让k>0
if(k<=0) { //如果扫描完发现k<=0了,说明整个序列都是降序,即是最大的排列,则其下一个排列就是第一个排列,即整个全排列中最小的那个
//这里可以使用sort对nums数组排个序,但是我们为了使得我们的时间复杂度保证在O(n),我们可以对数组进行翻转,而不是使用sort
reverse(nums,0,n-1); //对数组进行翻转就可以实现数组逆序排列了
//return nums; //翻转完之后再返回nums数组即可,注意这个题目不需要返回值,只需要我们对原数组做出修改即可
}else{ //否则的话,我们就需要进行上面的操作来寻找后面最小的那个大于3的数了
int t=k; //再用指针t来找后面最小的大于3的4
//此时我们的数3的下标是k-1,
while(t<n&&nums[t]>nums[k-1]) t++; //因为后面的数是降序的关系,所以当t停下来的时候,t-1就是后面最小的大于3的数的下标,注意是t-1,不是t
//t此时是第一个<=k-1的数,而t-1就是最小的大于k-1的数,因为后面的数是降序的关系,
//之后我们就交换一下nums[k-1]和nums[t-1]
int a=nums[t-1];
nums[t-1]=nums[k-1];
nums[k-1]=a;
//交换完两个数之后下标为k-1后面的数还是降序的关系,我们为了使得后面的数从小到大排序,可以使用sort(时间就是O(nlogn)),同样也可以使用reverse(时间O(n))
reverse(nums,k,n-1); //将k-1后面的从k到n-1的数翻转一下就可以了
//return nums; //翻转完之后再返回nums数组,注意这个题目不需要返回值,只需要我们对原数组做出修改即可
}
}
public void reverse(int[] nums,int l,int r){
while(l<r){
int t=nums[l];
nums[l]=nums[r];
nums[r]=t;
l++;r--; //注意一定要写l++,r--,否则就会死循环了
}
}
}
39. 组合总和
给定一个无重复元素的数组
candidates
和一个目标数target
,找出candidates
中所有可以使数字和为target
的组合。
candidates
中的数字可以无限制重复被选取。
说明:
所有数字(包括 target
)都是正整数
。
解集不能包含重复的组合
。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独一无二的。
1 <= target <= 500
代码:
class Solution {
public:
vector<vector<int>> ans; //定义全局答案数组,
vector<int> path; //定义全局方案数组,
vector<vector<int>> combinationSum(vector<int>& c, int target) {
dfs(c,0,target); //下面需要编写递归函数,从第0个数开始,当前需要凑的值仍为target,
return ans; //递归完成之后,返回答案数组
}
void dfs(vector<int>& c,int u,int target){ //递归函数,第一个参数为数组,第二个参数u为当前枚举到了第几个数,第三个参数为当前剩余的需要凑的数
if(target==0) { //如果需要凑的数为0了,说明我们已经找到了一组答案,就把这个答案放到答案数组里面
ans.push_back(path); //把当前方案数组放到答案数组
return; //结束循环。
}
if(u==c.size()) return; //如果我们已经枚举完了最后一个数,说明无解,结束即可
for(int i=0;c[u]*i<=target;i++){ //i是代表当前数c[u]在此时这种方案中的个数
dfs(c,u+1,target-c[u]*i); //一开始i等于0,即当前这个数一个都没有,我们就继续递归到下一个数,第二次枚举的时候选1个,第三次枚举的时候选2个,......
path.push_back(c[u]); //选取几个加到数组中几个当前数。
}
for(int i=0;c[u]*i<=target;i++){
path.pop_back(); //每次递归添加了几个c[u],就把几个c[u]清空。
}
}
};
40. 组合总和 II
给定一个数组
candidates
和一个目标数target
,找出candidates
中所有可以使数字和为target
的组合。
candidates
中的每个数字在每个组合中只能使用一次。说明:
所有数字(包括目标数)
都是正整数
。解集不能包含重复的组合
。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
这个题目和39题不同的是:数组中的元素不是独一无二的,且数组的数最多只能使用一次,且集合中的解集不能重复。这个题目在枚举元素的时候,不仅被总和限制还被元素的个数限制。(如一个数只有5个,你最多只能枚举5个这个数,不能枚举6个甚至更多,这个题目在枚举的时候需要再加一个个数的限制条件。)
这个题目需要先排序,这样就可以很方便的计算出每个元素的个数(或者使用哈希表也可以),
算法分析
dfs + 排序
- 1、为了避免选择了重复的元素,先对数组有规则的
进行排序
,例如从小到大排序- 2、当枚举到某一位置时,找到相同的元素区间
[u,k -1]
,共cnt
个可以选,暴力枚举每个数字选多少个- 3、当枚举到的总值
sum > targe
t,表示该枚举方式不合法,直接return
- 4、当枚举完每个数字之后,若
sum == target
,表示该选择方案是合法的,记录该方案
注意:枚举的时候的小细节,枚举每个数字选多少个的时候,本来是枚举多少个,最后
dfs
下一层完后,就应该全部恢复现场,删除多少个,再枚举下一种情况。可以直接枚举该数字选多少个,假设最多能选t
个,则每选一个dfs
一次,操作了t+ 1
次之后,再把这t
个相同的数字一次性恢复现场 (y总的小技巧)
代码:
class Solution {
public:
vector<vector<int>> ans; //定义全局答案数组
vector<int> path; //定义方案数组
vector<vector<int>> combinationSum2(vector<int>& c, int target) {
sort(c.begin(),c.end()); //对数组排序,方便计算每个数出现的个数。
dfs(c,0,target); //从第0个数开始递归,且此时需要凑的数值就是target。
return ans; //将答案数组返回。
}
void dfs(vector<int>& c,int u,int target){ //编写递归函数,u是遍历到第几个数,target是当前需要凑的值
if(target==0){
ans.push_back(path); //如果当前需要凑的数target为0了,说明我们此时找到了一组答案。
return; //结束每次递归
}
if(u==c.size())return; //如果遍历完最后一个数仍没有凑出来target,说明无解,我们直接结束即可。
//下面我们就需要开始枚举当前数了,我们需要先在排好序的数组中数出来当前数的个数,
int k=u+1; //从第u+1个数往后数一下一共多少个数和当前数c[u]相同,一开始就1个
while(k<c.size()&&c[k]==c[u])k++; //往后遍历,最后时k到达了第一个大于c[u]的位置;
int cnt=k-u; //c[u]的个数
for(int i=0;c[u]*i<=target&&i<=cnt;i++){ //这里不仅需要总和<=target,还需要个数不能大于cnt个,
dfs(c,k,target-c[u]*i); //继续对下一个数进行递归,此时k的位置就是下一个不同于c[u]的位置。
path.push_back(c[u]); //每枚举一个当前数c[u],就压入一个c[u]到方案数组中。
}
for(int i=0;c[u]*i<=target&&i<=cnt;i++){ //记得恢复现场。
path.pop_back();
}
}
};
41. 缺失的第一个正数
给你一个未排序的整数数组
nums
,请你找出其中没有出现的最小的正整数
。进阶:你可以实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案吗?
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
提示:
0 <= nums.length <= 300
-2^31 <= nums[i] <= 2^31 - 1
算法一:哈希表
(时间:O(N),空间:O(N))
代码:
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
unordered_set<int> map;
for(auto x:nums) map.insert(x);
int res=1;
while(map.count(res)!=true) res++; //注意C++中set集合判断是否存在用set.count(res),返回值不是个数,而是true或者false。
return res;
}
};
java代码:
class Solution {
public int firstMissingPositive(int[] nums) {
HashSet<Integer> hash=new HashSet<>(); //定义一个哈希表,HashSet基于 HashMap 来实现的,是一个不允许有重复元素的集合
for(int x:nums) hash.add(x); //将数组中的每一个数都添加到哈希表里面。
int res=1; //从小到大枚举每一个正整数,注意res初始化为1。
while(hash.contains(res)==true) res++; //只要哈希表中存在这个正整数,res就++,当while迭代循环结束的时候,res记录的就是最小为出现的正整数
return res; //while循环结束,res记录的就是最终答案。
}
}
进阶:你可以实现时间复杂度为 O(n)
并且只使用常数级别额外空间
的解决方案吗?
算法二:
进阶,时间:O(N),空间:O(1)
桶排序思想(注意这里不能用额外的空间,只能原地操作)
-
1、数组长度是
n
,确保通过某种规律的交换使得,nums[0] = 0,nums[1] = 1 … nums[n - 1] = n-1
,必须下标与对应的数值保持一致 -
2、若存在不在
[1,n]
区间的数时,则表示该数一定会在原数组占空间,且占到不能被对应的位置上,因此从小到大枚举,若nums[i] != i
,则表示i + 1
这个数是第一个缺失的正数,若都没有缺失,那么n + 1
就是第一个缺失的正数 -
3、1操作所说的通过某种规律的交换是指:如果该位置的数值与该坐标不对应,即
nums[i] != i
时,交换i和nums[i]
位置的数(其中nums[i] 是nums[i]需要填进去的位置),例如3 2 1,交换后变成1 2 3
时间复杂度 O(n),空间:O(1)
。
代码:
class Solution {
public:
//先把每个数放到它对应的位置上,即数放到下标0,数1放到下标1,......
int firstMissingPositive(vector<int>& nums) {
int n=nums.size(); //求出数组长度
if(n==0) return 1; //如果数组为空特判返回1即可。
//先完成将数1~n映射到0~n-1,即将每一个数都先减一
for(auto& x:nums) { //注意在c++中遍历数组的时候一定要加&,这样才是取值
if(x!=INT_MIN) x--; //x等于最小值再减一,会越界,所以这里x不能大于INT_MIN, 如果等于了INT_MIN,下面的代码会忽略掉的。
}
//完成数0到n-1放到数组下标0到n-1,负数和大于等于n的数都忽略掉
for(int i=0;i<n;i++){ //从前往后看每一个数
//下面我们需要把nums[i]放到其应该放的位置(nums[nums[i]])上,注意这里用while迭代循环,不能用if判断:因为交换后,当前位置出现的新数也需要同样的判断
while(nums[i]>=0&&nums[i]<n&&nums[i]!=i&&nums[i]!=nums[nums[i]]){ //只要数不是负数(nums[i]>=0)且比n小(nums[i]<n)
//并且num[i]没有放到其应该放的位置nums[nums[i]](nums[i]!=i),且这两个数不能相同,否则交换就会没有意义,会一直进行下去
swap(nums[i],nums[nums[i]]); //否则迭代完成nums[i]和nums[nums[i]]的交换。
}
}
//下面从小到大看一下哪个位置上的数是不等于其下标,就返回这个数
for(int i=0;i<n;i++){
if(nums[i]!=i){
return i+1; //如果不对应,就返回下标+1
}
}
//下面的for循环没有返回return的话,说明1~n这几个数都存在,我们就返回n+1
return n+1;
}
};
java代码:
class Solution {
public void swap(int[] nums,int a,int b){
int t=nums[a];
nums[a]=nums[b];
nums[b]=t;
}
public int firstMissingPositive(int[] nums) {
int n=nums.length;
if(n==0) return 1;
for(int i=0;i<n;i++){
if(nums[i]!=Integer.MIN_VALUE) nums[i]=nums[i]-1;
}
for(int i=0;i<n;i++){
while(nums[i]>=0&&nums[i]<n&&nums[i]!=i&&nums[i]!=nums[nums[i]]) swap(nums,i,nums[i]);
}
for(int i=0;i<n;i++) {
if(nums[i]!=i) return i+1;
}
return n+1;
}
}
2021年8月25日17:00:22:
方法一:使用哈希表解决
//方法一:使用排序,然后从前往后扫描,或者使用哈希表将数组中的数放到哈希表中,之后从1开始枚举,最多枚举到n+1,期间如果从1~n都存在,那就返回n+1,
//即此时n+1就是缺失的第一个正数,否则如果枚举的时候那个数没有出现过,就返回那个数
class Solution {
public int firstMissingPositive(int[] nums) {
int n=nums.length;
Set<Integer> set=new HashSet<>(); //申请一个哈希表
for(int x:nums) set.add(x); //将x放到哈希表中
int res=1; //注意先定义一个答案,因为题目要求我们有返回值,所以我们把res定义出来,最后返回res
for(int i=1;i<=n+1;i++){
if(!set.contains(i)) {
res=i;
break; //最后找到了res之后,一定要及时结束循环,
}
}
return res; //最后将res返回即可
}
}
方法二:
//方法二:使用O(n)时间,O(1)空间,我们使用交换的方法,把每一个数放到和下标相同的位置,即是说,把0放到下标0,把1放到下标1,......
//做的时候
//如以[3,4,6,7,2]为例
class Solution {
public void swap(int[] nums,int l,int r){
int t=nums[l];
nums[l]=nums[r];
nums[r]=t;
}
public int firstMissingPositive(int[] nums) {
int n=nums.length;
if(n==0) return 1; //特判数组是空的情况
//先完成将数1~n映射到0~n-1,即将每一个数都先减一,因为我们要完成的是让0=nums[0],1=nums[1]
//for(int x:nums) x--; //先把数组中的所有数都先减一,因为数组的下标是从0开始的,注意这样写因为java是值传递,这样并没有将nums中的数据减一,nums根本就没有发生改变,应该使用普通for循环
for(int i=0;i<n;i++){
if(nums[i]!=Integer.MIN_VALUE) nums[i]--; //注意必须要保证nums[i]不是最小值,否则nums[i]--会越界,
//同时我们也没有必要再特判nums[i]等于最小值的情况,因为如果nums[i]是最小值,我们也会有值进行返回的
}
for(int i=0;i<n;i++){ //枚举数组中的每一个数,即下标从0到n-1
while((nums[i]>=0)&&(nums[i]<n)&&(nums[i]!=i)&&(nums[i]!=nums[nums[i]])) { //首先要保证nums[i]在0~n-1的范围内,
//即要写上nums[i]>=0&&nums[i]<n,否则下标越界,因为nums[i]也作为了下标传到了数组中,即是这个:nums[nums[i]]
//其次nums[i]!=i是说当前这个数还没有放到和下标相同的位置,如注释样例中的3还没有放到下标3的位置,
//nums[i]!=nums[nums[i]]这句话是说,当前这个数应该要放到的位置上的数还不是当前这个数,如样例中的nums[0]=3,此时下标3的位置上的数是7
//注意以上几个条件必须全部都要满足才可以,否则就说明要么有负数出现要么有大于n的数出现,要么已经放到了对应位置,
//要么是要放到的位置上已经存放了这个数
//如果以上条件都满足,我们就要while迭代交换nums[i]和nums[nums[i]]
// int t=nums[i];
// nums[i]=nums[nums[i]]; //注意上面的while循环中的条件中可以保证nums[i]不是负数或者大于n的数(即保证下标不越界)
// //但是无法保证nums[nums[i]]这个数是否是负数,所以我们这里不能这样写,我们可以写一个外部的swap函数
// nums[nums[i]]=t;
swap(nums,i,nums[i]); //注意这里传入的参数是nums数组的两个下标,不是nums[i]和nums[nums[i]],这样就完成了即使是负数也可以交换两个数的目的,同时保证了数组下标不会越界
}
}
//全部交换完之后,所有数都到了对应的位置,下面我们在从1开始遍历,那个位置上的数不是和位置相同,则说明这个数是缺失的第一个正数
for(int i=0;i<n;i++){ //注意下标是从1开始的,防止下标越界最大为n-1,
if(nums[i]!=i) return i+1; //从小到大枚举,那个位置上的数不是i,则i+1就是缺失的第一个正数,因为上面我们已经减去1了,所以这里我们在返回答案的时候要加上刚才减去的1
}
return n+1; //最后就说明从1~n均没有缺失,则n+1就是缺失的第一个正数,
}
}
每个数字最多被执行n次就会放到对应的位置上,时间复杂度即是O(n)。
2021年11月4日15:55:07:
class Solution {
//注意这个题目不是内卷法,而是坐标和数值对应,即将对应的数字放到和其下标相同的位置,即如果是数字0就将0放到下标0,如果是数字5就将5放到下标5的位置
//注意这个题目的下标的对应,值是在1~n,而下标是0~n-1
public int firstMissingPositive(int[] nums) {
int n=nums.length;
for(int i=0;i<n;i++) nums[i]=nums[i]-1; //使用for循环,将数组中的所有数都减1,这样的话,1~n就会变成0~n-1了,注意不能直接--,java是值传递,必须用下标
for(int i=0;i<n;i++){ //遍历原数组将每一个数放到和其值相等的下标处,即每一个数都会执行下面的while迭代循环
while(nums[i]>=0&&nums[i]<n&&nums[i]!=i&&nums[i]!=nums[nums[i]]) swap(nums,i,nums[i]); //这样while迭代完nums[i]一定是i
//这里前两个判断是为了保证下标不越界,即当前数在0~n-1里面,nums[i]!=i是说当前数(nums[i])还没有放到其对应的位置上
//而nums[i]!=nums[nums[i]]是为了保证nums[i]位置上的数(nums[nums[i]])不被重复交换,即比如因为可能有重复元素,
//比如数组[1,1,0],当遍历到第一个1的时候,1在下标0,没有在下标1,我们就要交换,而这时下标1位置上已经是1了,所以我们就不要进行这步交换了
}
//如果数在[0,n-1]之间的话,就会被放到对应的位置上,如果有第i个数的话,nums[i]一定就是i,所以我们从前往后遍历看哪一个正数是第一次没有出现过的
//因为要找的是第一次没有出现过的正数,所以我们要从1开始遍历,但是下面我们已经将所有数都减一了,所以我们还是要从0开始遍历
for(int i=0;i<n;i++){
if(nums[i]!=i) return i+1; //注意这里要返回i+1,而不是nums[i],因为我们已经减一映射了
}
//最后上面的数都没有返回的话,缺失的第一个正数就是n+1
return n+1;
}
public void swap(int[] nums,int l,int r){
int t=nums[l];
nums[l]=nums[r];
nums[r]=t;
}
}
42. 接雨水(单调栈)
给定
n 个非负整数
表示每个宽度为1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
算法分析:
维持一个单调递减
的单调栈
。
栈中存的是下标。
注意:最后栈中只剩下画绿线的三个长方形。
代码:
class Solution {
public:
int trap(vector<int>& height) {
stack<int> stk; //定义栈,栈用来存柱子的高度数组的下标
int res=0; //定义答案,初始化为0;
for(int i=0;i<height.size();i++){ //遍历柱子高度数组
int last=0; //记录上一根柱子的高度
while(stk.size()&&height[stk.top()]<=height[i]){ //栈非空且新柱子的高度大于栈顶元素
res+=(height[stk.top()]-last)*(i-stk.top()-1); //计算面积1
last=height[stk.top()]; //将上一根柱子的高度更新记录为栈顶元素
stk.pop(); //将栈顶元素弹出。
}
if(stk.size()) res+=(i-stk.top()-1)*(height[i]-last); //计算面积2
stk.push(i); //面积计算结束,将柱子高度数组下标加到栈中。
}
return res; //将答案返回
}
};
45. 跳跃游戏 II (dp+贪心)
给定一个非负整数数组,
你最初位于数组的第一个位置
。数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
说明:
假设你总是可以到达数组的最后一个位置。
算法分析:
这个题目只使用贪心是不对的,即每次不一定跳最远,如:
[2,3,1,1,4]
,每次跳最远则需要从0跳两步到2,2跳1步到3,3跳一步到4,达到最终位置,即需要3步;而实际上我们从0跳一步到1,从1跳3步到4,则只需要2步。
算法分析
dp + 贪心
状态表示:
f[i]
表示所有从0位置到达i位置的最少步数
状态计算:
j
表示能到达i
位置最前的位置,假设j < i' < i
,如果j
能跳到i
,那么j一定能跳到i'
,可是如果j能跳到i',那么j不一定能跳到
i,因此偏前面的位置需要跳的步数一定是比后面的少。
若枚举到i
时,根据上一步可知i - 1
的位置对应最前的位置是j
,那么如果j
不能到达i
,需要j ++
,直到找到能跳到j
的最前位置为止 方程:f[i] = f[j] + 1
时间复杂度 O(n)
DP时间复杂度:O(n)
算法分析
f[i]表示所有从0位置到达i位置的最少步数
j是找到 从j跳到i, 跳到i有很多条路,而从j到i是最前的那条路,即f[i] = f[j]+1
代码:
class Solution {
public:
int jump(vector<int>& nums) {
int n=nums.size();
vector<int> f(n); //定义状态数组,f数组是跳到点i的最小值,数组f下标从0开始
f[0]=0; //初始化f[0]=0,即跳0步就可以到达位置0。
// 遍历[1,nums.length-1]中的每一个位置
for(int i=1,j=0;i<n;i++){ //从前往后找到每个点可以有最前面的那个点跳过来,且i从1开始,上面已经初始化f[0]了
// 找到第一个能跳到i的点j
while(j+nums[j]<i)j++; //j停下来的时候,即是第一个可以跳到i的最前面的位置即(j+nums[j]>=i)
// 使用点j更新f[i]/使用第一个能到i的点更新f[i]
// 由于f[i]具有单调递增且是连续递增的, 所以第一次能到达i的点j, 只需要再跳1步就能到达i了.
f[i]=f[j]+1; //更新f[i]为f[j]+1。即从j跳到i还需要f[j]+1
}
return f[n-1]; //返回跳到最后一个点所需要的最小跳数。
}
};
48. 旋转图像
给定一个
n × n
的二维矩阵matrix
表示一个图像。请你将图像顺时针
旋转90
度。你必须在
原地
旋转图像,这意味着你需要直接修改
输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
示例 3:
输入:matrix = [[1]]
输出:[[1]]
示例 4:
输入:matrix = [[1,2],[3,4]]
输出:[[3,1],[4,2]]
提示:
matrix.length == n
matrix[i].length == n
1 <= n <= 20
-1000 <= matrix[i][j] <= 1000
提示:
n == height.length
0 <= n <= 3 * 10^4
0 <= height[i] <= 10^5
先沿着对角线翻转,再左右翻转
代码:
//先沿着对角线翻转,再左右翻转
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n=matrix.size(); //即求出n行n列的这个n
//下面先将矩阵沿着对角线翻转
//斜对称
for(int i=0;i<n;i++){ //枚举每一行
for(int j=0;j<i;j++){ //即矩阵左下角的元素
swap(matrix[i][j],matrix[j][i]); //将矩阵左下角的元素和右上角的元素进行交换。
}
}
//上面左右翻转,我们需要枚举每一行
//竖对称
for(int i=0;i<n;i++){ //枚举每一行
for(int j=0,k=n-1;j<k;j++,k--){ //将每一行左右对称的元素进行交换。
swap(matrix[i][j],matrix[i][k]); //沿着中间对称轴,将左右元素进行翻转。
}
}
//返回值为void,不用写返回语句。
}
};
2021年8月26日17:18:20:
//先转置后镜像对称,
//顺时针旋转90度,并且使用原地算法,顺时针旋转90度的操作可以分解为两步完成:1.先沿着正对角线交换元素,2.再沿着竖对角线进行交换左右元素
//当我们这样交换完这样两步之后就完成了顺时针旋转90度的操作,注意代码的书写
class Solution {
public void rotate(int[][] matrix) {
int n=matrix.length; //这个矩阵的行列是相同的,所以我们求一个即可
//先进行第一步:沿着斜对角线进行翻转,我们只需要枚举左下角的元素即可,注意j的枚举不是从0到n,否则就会交换两次,即又变回去了
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){ //这里我们只要枚举矩阵的左下角元素即可,否则就会被交换两次,达不到交换的效果
//沿着斜对角线进行交换,
int t=matrix[i][j];
matrix[i][j]=matrix[j][i];
matrix[j][i]=t;
}
}
//第二步: 沿着中心对称轴进行交换,我们枚举每一行,共n行,对于每一行,我们使用双指针一个在开头,一个在结尾,进行元素的交换
for(int i=0;i<n;i++){
for(int j=0,k=n-1;j<k;j++,k--){ //使用双指针,一个在开头,一个在结尾,完成每一行矩阵的左右交换
int t=matrix[i][j];
matrix[i][j]=matrix[i][k];
matrix[i][k]=t;
}
}
//这个题目的返回为空,所以这里不需要写return语句
}
}
54. 螺旋矩阵
给你一个
m
行n
列的矩阵matrix
,请按照顺时针螺旋顺序
,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 10
-100 <= matrix[i][j] <= 100
代码:
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res; //定义答案数组,看样例数组是一个一位数组,所以我们在这里定义一个一位答案数组
int m=matrix.size(); //求出矩阵的行,题目已经保证行列均不空,所以这里不用判断行列是否为空。
int n=matrix[0].size(); //求出矩阵的列
int dx[]={0,1,0,-1}, dy[]={1,0,-1,0}; //定义方向矩阵,即四个方向的偏移量
vector<vector<bool>> st(m,vector<bool>(n)); //定义m行n列的判重数组,用于代表这个格子是否被访问过,
//注意这里定义的时候,用(),st后面的()代表定义m个大小为n的一维数组,初始化为false,即代表没有被遍历过,为true代表已经被访问过,就不能再访问这个格子了。
for(int i=0,x=0,y=0,d=0;i<m*n;i++){ //从左上角元素开始移动,一共移动m*n次,i代表移动的次数,如m*n等于1,则代表就移动一次(初始位置也算移动一次),
//x,y代表下标,一开始在(0,0)点,用d代表方向,一开始要往右走,所以初始化为0(0-右,1-下,2-左,3-上)
res.push_back(matrix[x][y]); //首先先遍历当前这个格子,即先把当前这个格子加入到答案数组里。
st[x][y]=true; //同时将st[x][y]置为true,代表这个格子已经被遍历过了,后面不能再遍历这个格子。
int a=x+dx[d],b=y+dy[d]; //a,b代表下一个要移动的方向,将a赋值为当前点坐标加上偏移量,将b赋值为当前点坐标加上偏移量。
if(a<0||a>=m||b<0||b>=n||st[a][b]==true){ //如果下一个将要前往的位置出界了或者下一个将要前往的位置已经被访问过,
//我们现在就需要按照012301230123......的顺序换到下一个方向。
d=(d+1)%4; //将移动方向换到下一个方向,就是将d加一再模4。
a=x+dx[d],b=y+dy[d]; //将a,b重新更新为换完方向之后的将要移动的下一个格子的位置
}
x=a,y=b; //将要移动的下一个格子的位置记录下来。
}
return res; //将答案数组返回输出
}
};
2021年8月26日20:06:28:
//蛇形矩阵,使用偏移量来做,即方向数组,
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res=new ArrayList<>(); //定义答案数组
int m=matrix.length; //矩阵的行
if(m==0) return res; //如果矩阵是空的,我们就返回空答案
int n=matrix[0].length; //矩阵的列
int[] dx={0,1,0,-1},dy={1,0,-1,0}; //定义两个方向数组,即偏移量数组,之所以要分成两个数组定义,是因为每次变化的时候,行和列是分开的,用{ }定义数组
//即是说在变化移动的时候,可能只变行,不变列,也可能只变列,不变行,所以分开来比较方便
boolean[][] st=new boolean[m][n]; //定义状态数组,记录当前格子是否已经被遍历过
int d=0; //d表示要移动的方向,d=0表示往右移动,d=1向下,d=2向左,d=3向上,我们使用d=(d+1)%4即可完成往下一个方向移动的目标
int x=0,y=0; //x,y表示当前遍历到了哪一个格子,最开始是在左上角,所以下标是(0,0)
for(int i=0;i<m*n;i++){ //我们需要往答案数组中添加m*n个数,或者说需要遍历m*n个格子
res.add(matrix[x][y]); //先把当前格子遍历一下,即把当前格子中的数加到答案数组中去,因为我们是按照方向一步一步来的,
//最开始最左上角的格子一定是没有遍历过的,所以我们直接遍历即可,之后的格子我们在下面的语句中保证了一定不会重复遍历,所以也不用写if(st[x][y]==false)
//所以我们遍历到的格子一定是先前没有遍历过的,所以这里我们没有必要写if(st[x][y]==false)
st[x][y]=true; //将当前格子标记为已经遍历过,即更新st数组
//之后往下一个格子走,下一个格子的坐标是
int a=x+dx[d],b=y+dy[d]; //下一个要移动过去的格子的下标是(a,b)
//判断一下(a,b)是否合法,以及(a,b)是否已经被遍历过,如果已经出界或者已经遍历过就说明我们需要更换方向了
if(a<0||a>=m||b<0||b>=n||st[a][b]==true){ //如果a或者b出界,或者下一个格子(a,b)已经被遍历过,就说明我们需要更换新的方向了
d=(d+1)%4; //用d=(d+1)%4来更新方向
a=x+dx[d];b=y+dy[d]; //因为(a,b)不合法,所以我们就需要继续更新a,b,这一次的(a,b)由于我们改变了方向所以一定是合法的
}
x=a;y=b; //我们复用x,y,所以我们这里更新x,y继续遍历格子,注意两条更新语句之间用;隔开,java中不允许它们之间用,隔开
}
//遍历完m*n次,就说明前面正好遍历完了所有的格子,将答案返回
return res;
}
}
55. 跳跃游戏
给定一个
非负整数
数组nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 3 * 10^4
0 <= nums[i] <= 10^5
算法分析
贪心
核心思想:尽可能跳到更远的位置
- 1、若往后的位置能跳到,则前面的位置一定可以跳到,
last
表示的是从前i - 1
个位置中跳,能跳到最远的位置是last
- 2、若前
i - 1
个位置中跳,跳到最远的位置是last
比i
小,表示从前i - 1
个位置中跳,跳不到i
的位置,因此一定不能跳到最后一个的位置- 3、若前
i - 1
个位置中跳,能跳到i
,则继续尝试从i
位置跳,可能会跳得更远,更新last
的值
时间复杂度 O(n)
代码:
class Solution {
public:
bool canJump(vector<int>& nums) {
for(int i=0,last=0;i<nums.size();i++){ //i表示当前所在的位置,从前往后扫描,
//last表示最靠右的位置是什么,一开始在起始位置,所以一开始能跳到的位置就是0
if(last<i) return false; //如果能跳到的最大位置<i,说明当前位置不能跳到,返回false即可。
last=max(last,i+nums[i]); //否则的话说明第i个位置能跳到,我们就看一下从第i个位置往后最大能跳多远,即更新记录最大位置
}
//上面循环会枚举到最后一个位置,如果没有返回false,说明最后一个位置能跳到,返回true即可。
return true;
}
};
2021年10月19日14:17:45:class Solution {
//注意这个题目不能暴力的往后跳,即不能有多远跳多远,否则原本是可以到达最后位置的,但是如果暴力跳的话,可能就到不了终点了,
//这个题目是问我们从起点是否能够跳到终点,而跳跃游戏II即45题问我们的是能否从起点跳到终点
//我们能跳到的位置一定是连续的一段,所以我们从前往后扫描,扫描的时候记录一下当前可以跳到的最靠右的位置是什么,即看一下所有的i+nums[i]这些项的最大值
//比如我们在扫描的时候有某一个位置i,其前面的i-1个位置可以跳到的最靠右的位置是j是在i的前面,即j<i,就说明i是永远到不了的位置,返回false即可
//否则返回true,
public boolean canJump(int[] nums) {
int n=nums.length;
int j=0; //j记录我们可以跳到的最靠右的位置是什么,一开始是0,第一个点之前因为前面是没有点的,所以前面的点可以跳到的最右的位置是0
for(int i=0;i<n;i++){ //从前往后扫描每一个格子
if(j<i) return false; //如果我们在扫描的时候发现j<i了,说明i这个点是跳不到的,所以直接返回false
else j=Math.max(j,i+nums[i]); //否则我们就更新一下前面的格子可以跳到的最靠右的位置是什么,即从第i个位置往后跳可以跳多远
}
return true; //如果上面枚举完了所有的格子都没有返回false,说明每一个格子都可以跳到,我们就返回true
//因为上面会枚举到最后一个格子,如果枚举完了最后一个格子也没有返回false的话,就说明每一个格子都可以跳到,我们就返回true
}
}
56. 合并区间(贪心)
以数组
intervals
表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
1 <= intervals.length <= 10^4
intervals[i].length == 2
0 <= starti <= endi <= 10^4
算法分析
1、将每个区间
按左端点从小到大进行排序
2、如图所示,可分
3种
情况
- 情况一:当前区间完全被上一区间覆盖,直接跳过
- 情况二:将当前区间的右端点更新为上一区间的右端点,达到区间延长的效果
- 情况三:当前区间的左端点严格大于上一区间的右端点,则表示该区间不能合并,更新区间且该区间加入到ans的链表中
时间复杂度 O(nlogn)
C++代码:
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& a) {
vector<vector<int>> res; //定义合并之后的答案数组
sort(a.begin(),a.end()); //对区间进行排序,vector排序是先排first,再排second,正好可以起到将区间左端点进行排序的作用。
int l=a[0][0],r=a[0][1]; //存一下第一个区间的左右端点,
for(int i=1;i<a.size();i++){ //从第二个区间开始枚举合并,
if(a[i][0]>r){ //如果下一个区间的左端点大于当前区间的右端点,
res.push_back({l,r}); //如果下一个区间的左端点大于当前区间的右端点,则说明当前区间可以保存到答案数组里。
l=a[i][0],r=a[i][1]; //将当前区间更新为下一个区间,
}else{ //否则按照两个区间的最大右端点更新右端点
r=max(r,a[i][1]);
}
}
//记得保存最后一个区间
res.push_back({l,r});
return res; //将答案返回
}
};
JAVA代码:
class Solution {
public int[][] merge(int[][] a) {
int n=a.length; //求出区间的个数
List<int[]> ans = new ArrayList<>(); //定义答案数组,但是这里要注意,最后返回的是int[][],所以代码最后要返回的时候,需要将数组列表转变为二维数组。
//且这里还要注意,List的类型是int[],即是数组类型
Arrays.sort(a,(x,y) -> x[0] - y[0]); //对二维数组中每个一维数组的起始位置进行大小排序,这里是Lambda表达式写法
int l=a[0][0],r=a[0][1];
for(int i=0;i<n;i++){
if(a[i][0]>r){
ans.add(new int[]{l,r}); //将答案添加到数组列表中。
l=a[i][0];
r=a[i][1];
}else{
r=Math.max(r,a[i][1]);
}
}
ans.add(new int[]{l,r});
int m = ans.size(); //下面是将数组列表转化为二维数组。
int[][] res = new int[m][2];
for(int i = 0;i < m;i ++)
{
res[i][0] = ans.get(i)[0]; //取第二维元素
res[i][1] = ans.get(i)[1]; //取第二维元素
}
return res;
}
}
2021年8月27日18:15:29:
先根据区间的起始位置排序,再进行 n -1 次 **两两合并**。
//把所有由交集的区间进行合并,将合并的区间输出即可,看图上的分析
class Solution {
public int[][] merge(int[][] nums) {
List<int[]> res=new ArrayList<>(); //定义答案数组,ArrayList可以实现add的操作,而直接定义int[][]有两点,1.长度不知道,2.不能实现add操作。
//所以这里我们不能定义int[][],而是定义List<int[]>,即列表的类型是数组,这样再添加数组会很方便,注意最后要转为二维数组
int n=nums.length; //nums数组的一维数组的个数,即nums的长度,而nums[0]即一维数组的元素个数都是2,是确定的
Arrays.sort(nums,(x,y)->(x[0]-y[0])); //使用lambda表达式就实现了按照区间左端点进行排序的目的,x[0]-y[0]即实现了左端点的从小到大排序
//遍历数组,用l,r维护需要合并的区间的左右端点
int l=nums[0][0],r=nums[0][1]; //nums[0]即排完序之后的第一个区间,nums[0][0]即第一个区间的左端点,nums[0][1]即第一个区间的右端点
//从排序之后的第二个区间开始遍历合并
for(int i=1;i<n;i++){
if(nums[i][0]>r){ //如果当前区间的左端点大于r,表示当前区间不需要合并,我们将准备合并的别人的区间[l,r]加到res中,
//这里的思路和方法和蛇形矩阵一样,即先加到答案中,再更新区间的左右边界
res.add(new int[]{l,r});
//记得要更新准备合并的别人的区间的左右边界l,r
l=nums[i][0]; //nums[i][0]即是当前区间的左端点
r=nums[i][1]; //nums[i][1]即是当前区间的右端点
}else{
//否则的话当前区间就需要合并,但是左边界是不需要合并的,只需要更新右边界
r=Math.max(r,nums[i][1]); //因为有可能当前区间是上一个区间的子集或者当前区间的右端点比上个区间要大,所以我们取两者的最大值
}
}
res.add(new int[]{l,r}); //看循环体内,我们可以发现,最后一个区间在更新完之后不会再次执行for循环,即不会再被加到res中,所以我们这里手动将最后一个区间{l,r}加到res中
//最后就需要返回答案数组了,注意上面res是ArrayList,我们需要转为二维数组,并且二维的长度等于res.size(),每个一维数组中元素个数是2个
int m=res.size(); //合并之后的答案区间的个数,注意n已经被用过了,所以我们不能再用n了,
int[][] ans=new int[m][2]; //申请答案二维数组
for(int i=0;i<m;i++){ //将列表转为二维数组
ans[i]=res.get(i);
}
return ans; //最后将ans返回
}
}
57. 插入区间
给你一个 无重叠的 ,按照区间起始端点排序的区间列表。
在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,
可以合并区间
)。
示例 1:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]
示例 2:
输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
示例 3:
输入:intervals = [], newInterval = [5,7]
输出:[[5,7]]
示例 4:
输入:intervals = [[1,5]], newInterval = [2,3]
输出:[[1,5]]
示例 5:
输入:intervals = [[1,5]], newInterval = [2,7]
输出:[[1,7]]
提示:
0 <= intervals.length <= 10^4
intervals[i].length == 2
0 <= intervals[i][0] <= intervals[i][1] <= 10^5
intervals 根据 intervals[i][0] 按 **升序** 排列
newInterval.length == 2
0 <= newInterval[0] <= newInterval[1] <= 10^5
算法分析
题目的理解意义如图所示
- 1、把前面没有和指定区间有相交关系的区间全部加入到
ans
队列中。- 2、从第
k
个区间开始连续一段存在与指定区间有相交的关系,需要将有相交关系的区间与指定区间进行合并,将合并后的区间加入到ans
队列中,当枚举的区间的左端点<=
指定区间的右端点时,表示当前区间可以与指定的区间进行合并。- 3、再把剩余的区间加入到
ans
队列中。
时间复杂度 O(n)
代码:
class Solution {
public:
vector<vector<int>> insert(vector<vector<int>>& a, vector<int>& b){
vector<vector<int>> res; //定义答案数组
int k=0; //先枚举第一部分
while(k<a.size()&&a[k][1]<b[0]) {
res.push_back(a[k]); //将左边完全没有交集的部分加入到答案数组中
k++; //看下一个区间
}
//再来看中间有交集的部分,k此时到达了第一个有交集的区间
if(k<a.size()){ //基本条件不能缺少
b[0]=min(b[0],a[k][0]); //将交集区间的左端点更新为最左的数
while(k<a.size()&&a[k][0]<=b[1]) b[1]=max(b[1],a[k++][1]); //while循环条件是要始终保证有交集的区间的左端点小于给定区间的右端点,
}
res.push_back(b); //将交集合并之后加入答案数组中。
while(k<a.size()) res.push_back(a[k++]); //将后面没有交集的部分直接抄下来即可。
return res; //最后将答案返回。
}
};
2021年8月27日20:45:12:
//这个题目的所有区间都是没有重叠的,现在是又给了我们一个区间,新区间可能就会和原有的区间有交集,这样就需要我们进行区间的合并,使得合并之后的区间仍是没有交集的
//这个题目和是一个题目之间没有任何关系,这个题目的区间是没有重叠的,并且按区间起点已经排好序了
//注意往后遍历的三部分的先后顺序不能混了,即不能先放后面第三部分,再放中间第二部分,因为这样得到区间将是无序的
//代码中有两部分很容易忘记:1.一直都要判断k<n,2.让k++,即将k往后移动
class Solution {
public int[][] insert(int[][] a, int[] b) {
List<int[]> list=new ArrayList<>(); //和上一个题目一样,我们为了添加数组方便,所以我们使用的是ArrayList
int n=a.length; //区间的个数是n
int k=0; //k用于往后遍历区间,当前是遍历的第一个区间,所以k=0
while(k<n&&a[k][1]<b[0]) list.add(a[k++]); //当区间的右端点小于新区间的左端点,我们就往后遍历下一个区间,边遍历边将区间加到list中,并将k加一,即往后移动
//当k停下来的时候,第k个区间(从0开始的)就是第一个和新区间有交集的区间
int l=b[0],r=b[1]; //l,r是合并之后的区间的左右端点,初始化为新区间的左右端点
if(k<n){ //注意因为可能所有的区间都小于新区间,所以我们要先判断一下
l=Math.min(b[0],a[k][0]); //合并之后区间的左端点是新区间和第一个和新区间相交区间的最小值,所以这里我们取最小值
while(k<n&&a[k][0]<=b[1]) r=Math.max(r,a[k++][1]); //在往后遍历的同时(k++),同时更新合并之后的区间的右端点
}
list.add(new int[]{l,r}); //将合并之后的区间的左右端点数组加到list中
//当k停下来的时候,k就是右边第一个和新区间没有交集的区间,我们直接加到数组中即可,注意可能k已经出界了,所以我们要写判断语句k<n
while(k<n) list.add(a[k++]); //注意往后移动的时候k要++,否则就一直在加a[k],内存要爆炸了
//这样三部分我们就全部处理完了
int m=list.size(); //要将列表转为数组,所以要求出来数组的长度
int[][] res=new int[m][2];
for(int i=0;i<m;i++){
res[i]=list.get(i);
}
return res;
//最后的将数组列表转为数组的操作:return res.toArray(new int[res.size()][]);可以替代。
}
}
也可以申请一个新的数组,将新区间加到新数组中,这样题目就转化为了上一题。
算法思想
- 构建一个新的数组,前k个元素和被插入数组相同,k+1个元素就是需要插入的元素
- 然后这个问题就转化成了LC56
class Solution {
public int[][] insert(int[][] o, int[] n) {
int a[][]=new int[o.length+1][2];
for(int i=0;i<o.length;i++)
for(int j=0;j<o[0].length;j++) a[i][j]=o[i][j];
a[a.length-1][0]=n[0]; a[a.length-1][1]=n[1];
Arrays.sort(a,(o1,o2)->{return o1[0]-o2[0];});
LinkedList<int []> stack=new LinkedList<>();
for(int arr[]:a){
if(!stack.isEmpty() && arr[0]<=stack.peekLast()[1])
stack.peekLast()[1]=Math.max(stack.peekLast()[1],arr[1]);
else stack.addLast(arr);
}
return stack.toArray(new int[stack.size()][2]);
}
}
2021年10月18日15:47:56:
class Solution {
//做了好多次的题目了,前不久刚做,又他妈的忘了,
//我们这样来考虑用来的区间数组和新待插入的区间,可以将用来的区间这样分成三部分:1.左边的完全和新区间没有任何交集的部分,这部分可以直接加到答案数组中
//2.右边的完全和新区间没有任何交集的部分这部分也是可以直接加到答案数组中; 最后再来考虑的就是中间的和新区间有交集的部分,这部分最后会变成一个区间
//并且新区间的左端点是新区间和第一个有交集的较小值,右端点和新区间和最后一个有交集的较大值,再把更新之后的区间加到答案数组中
//这里y总很巧妙的使用了一个指针k来遍历原数组区间,并且代码中的k<n的写法很重要,if中有k<n,while中也有k<n,
public int[][] insert(int[][] a, int[] b) {
List<int[]> res=new ArrayList<>();
int n=a.length;
int k=0; //使用索引找到左边和新区间完全没有交集的部分,k是遍历原数组区间的指针
//先看左边完全没有交集的部分直接加到答案数组中去
while(k<n&&a[k][1]<b[0]) res.add(a[k++]); //即只要右端点小于新区间的左端点就一定没有交集,k就++,
//最后k到达的是第一个和新区间有交集的区间的下标,注意这部分是直接插到答案数组中的,所以这里的操作是res.add(a[k]),同时记得要将k++,即将k往后移动一位
//注意这里可能k最后遍历完了全部的区间,即新区间在原数组区间的所有区间的右边,这时候我们只需要再将新区间加到答案数组中,再返回即可
if(k==n) { //如果上面的k到达了n,说明所有的区间都在新区间的左边,我们直接将新区间加到答案中去,其实下面的else语句中的第一句就是k<n,所以这里我们可以不写这个判断了
res.add(b);
//返回在最后
}else{ //否则我们再来找中间和最后的部分
if(k<n){ //找中间的部分即找到所有有交集的部分和新区间的共同的最左点和最右点,k此时到达的是第一个和新区间有交集的区间,
b[0]=Math.min(b[0],a[k][0]); //先记录下来交集区间的左端点,这里复用了b[0]
while(k<n&&a[k][0]<=b[1]){ //只要数组的左端点<=新区间的右端点,就说明两者之间有交集
b[1]=Math.max(b[1],a[k++][1]); //有交集我们就更新新区间的b[1],注意这里也要让k++
}
res.add(new int[]{b[0],b[1]}); //最后把中间有交集的区间合并之后的区间加到答案数组中
}
}
//最后再把用来的数组区间的右边的和新区间没有交集的部分直接加到答案数组中去就行了,并且这里不需要再判断交集啥的了,因为上面已经保证没有交集了,所以只需要保证k<n就行了
while(k<n) res.add(a[k++]); //只要k没有越界,就将对应的区间加到res中
//把ArrayList转为int[][]
int m=res.size();
int[][] f=new int[m][2];
int idx=0;
for(int[] x:res){ //这里遍历的是res,不是f啊,伞兵
f[idx++]=x;
}
return f;
}
}
刚简洁的写法:
class Solution {
public int[][] insert(int[][] a, int[] b) {
int n=a.length;
List<int[]> res=new ArrayList<>();
int k=0;
while(k<n&&a[k][1]<b[0]) res.add(a[k++]);
if(k<n){
b[0]=Math.min(b[0],a[k][0]);
while(k<n&&a[k][0]<=b[1]){
b[1]=Math.max(b[1],a[k++][1]);
}
}
res.add(b); //这里要把res.add(b)放到if的下面,因为可能在执行完第一个while之后,k是等于n的,这时候也要将b加到res中去,所以要放到if语句外,
while(k<n) res.add(a[k++]);
return res.toArray(new int[res.size()][2]); //这里可能这里使用res.toArray(new int[res.size()][2])将数组列表转为数组
}
}
59. 螺旋矩阵 II
给你一个
正整数
n
,生成一个包含1
到n^2
所有元素,且元素按顺时针顺序螺旋排列的n x n
正方形矩阵matrix
。
示例 1:
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1
输出:[[1]]
提示:
1 <= n <= 20
代码:
//这个题目和54题基本上完全一样
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n,vector<int>(n)); //定义答案数组,res
int dx[]={0,1,0,-1},dy[]={1,0,-1,0}; //定义方向矩阵,即偏移量
for(int i=1,x=0,y=0,d=0;i<=n*n;i++){ //i是我们要填充的数字,(x,y)是当前的位置,初始是左上角,d是代表将要移动的方向,初始时是向右即0(0-右,1-下,2-左,3-上)
res[x][y]=i; //先把i填到(x,y)这个位置
int a=x+dx[d],b=y+dy[d]; //定义将要移动的下一个位置。
if(a<0||a>=n||b<0||b>=n||res[a][b]!=0){ //如果a越界,或者b越界或者这个位置已经被填过数
d=(d+1)%4; //就移动到新的方向
a=x+dx[d],b=y+dy[d]; //同时求出在新的方向上的将要移动到的新的位置
}
x=a,y=b; //最后说明(a,b)这个格子是可以走的,我们就将a,b更新记录下来。
//这一步非常容易忘记,如果没有写说明res[x][y]=i将起不到任何作用了。
}
return res; //最后将答案返回
}
};
2021年8月26日21:42:02:
//这个题目是让我们自己生成蛇形矩阵,即按照蛇形矩阵遍历的顺序填入数字,之后按照一行一行的顺序遍历出来就可以得到答案,这个题目和I基本上完全相同,
//我们还是使用方向数组的方法,这个题目不需要开判重数组,因为数组初始化时都是0,当我们填一个数之后,就会改为对应的数,所以我们判断一下这个格子是不是0
//我们就可以知道这个格子是否已经被填过数了,如果不是0就说明已经填过数了
class Solution {
public int[][] generateMatrix(int n) {
int[][] res=new int[n][n]; //定义答案数组
int[] dx={0,1,0,-1},dy={1,0,-1,0}; //定义方向矩阵
int d=0; //一开始的方向是向右即是0
int x=0,y=0; //一开始的坐标是(0,0)
for(int i=1;i<=n*n;i++){ //i是我们要填入的数,是从1开始的,最大是填到n*n,注意不是从0开始的,最大可以取到n*n
res[x][y]=i; //先把i填到对应的位置上
int a=x+dx[d],b=y+dy[d]; //下一个要填的格子的下标是(a,b)
//如果下一个我们要去的格子的下标越界或者已经填过数字了,我们就要使用d=(d+1)%4来改变方向,并且要更新(a,b),
if(a<0||a>=n||b<0||b>=n||res[a][b]!=0){ //注意我们要把判断a,b是否越界放到res[a][b]!=0之前,否则可能会下标越界
d=(d+1)%4;
a=x+dx[d];b=y+dy[d];
}
x=a;y=b; //复用a,b,表示a,b要去的下一个格子的坐标
}
return res; //最后将答案数组进行返回即可
}
}
66. 加一
给定一个由
整数
组成的非空
数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,
这个整数不会以零开头
。
示例 1:
输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。
示例 2:
输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。
示例 3:
输入:digits = [0]
输出:[1]
提示:
1 <= digits.length <= 100
0 <= digits[i] <= 9
算法分析
类似高精度加法的原理
- 1、将当前数组的每个数
逆序
存入到链表中,且初始化t = 1
- 2、枚举所有位,将
t = t + a[i]
对t % 10
(10取模
)的值放在当前位,将t / 10
(剩下的)放在下一位- 3、若枚举完所有位后,
t > 0
,将t
再次存入到下一位- 4、将链表的元素
逆序
返回到一个数组中,然后返回数组
时间复杂度 O(n)
代码:
class Solution {
public:
vector<int> plusOne(vector<int>& digits) {
reverse(digits.begin(),digits.end()); //先将数组反转
int t=1; //t是进位,初始化为1
for(auto& x:digits){ //从个位开始枚举每一位
t+=x; //总和为t加上x
x=t%10; //这一位真实值
t/=10; //下一位的进位
}
if(t!=0) digits.push_back(t); //如果最后有进位,就在答案数组中加上这个进位
reverse(digits.begin(),digits.end()); //运算完记得还要将数组再翻转回来
return digits; //最后还要返回答案
}
};
2021年9月17日16:32:08:
class Solution {
public int[] plusOne(int[] d) {
List<Integer> res=new ArrayList<>(); //因为最后的答案数组的位数可能和d相等,也可以会多一位比如是999+1=1000,之前是3位现在是4位,所以我们用列表,最后转为数组
int n=d.length;
int[] a=new int[n]; //因为我们要翻转数组d,而数组是无法被翻转的,数组a用于存储数组d逆序的结果
int k=0;
for(int i=n-1;i>=0;i--) a[k++]=d[i]; //倒着遍历数组就可以将数组d中的数字逆序放到新数组a中了
int t=1; //t记录进位或者说每一位上的总和,因为我们要加一,所以t初始化为1
for(int x:a){ //此时再遍历数组就是从个位数开始了,其实我们也可以倒着遍历d数组,这样就可以不用逆转d数组了,牛哇,
t+=x;
res.add(t%10);
t/=10;
}
if(t!=0) res.add(t); //注意有可能t最后往前进了一位,而上面的代码我们是无法处理这种情况的,我们需要自己判断,如果最后有进位,我们也要加到res中
Collections.reverse(res); //最后记得翻转res
int m=res.size();
int[] ans=new int[m];
for(int i=0;i<m;i++){
ans[i]=res.get(i);
}
return ans;
}
}
73. 矩阵置零
给定一个
m x n
的矩阵,如果一个元素为0
,则将其所在行和列的所有元素都设为0
。请使用原地
算法。
示例 1:
输入:
[
[1,1,1],
[1,0,1],
[1,1,1]
]
输出:
[
[1,0,1],
[0,0,0],
[1,0,1]
]
示例 2:
输入:
[
[0,1,2,0],
[3,4,5,2],
[1,3,1,5]
]
输出:
[
[0,0,0,0],
[0,4,5,0],
[0,3,1,0]
]
进阶:
一个直接的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个常数空间的解决方案吗?
代码:
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
if(matrix.empty()||matrix[0].empty()) return; //如果矩阵为空,就结束
int n=matrix.size(),m=matrix[0].size(); //求出矩阵的行n和列m
//用0表示这一行或者这一列有0,用1表示这一行或者这一列没有0
int r0=1,c0=1; //r0表示第一行有没有零,c0表示第一列有没有零,均初始化为1,表示还没有0
//下面遍历第一行,如果有0就把r0置为1
for(int i=0;i<m;i++){
if(matrix[0][i]==0){
r0=0; //等于零,说明有零
break;
}
}
//下面遍历第一列,如果有0就把c0置为1
for(int i=0;i<n;i++){
if(matrix[i][0]==0){
c0=0; //等于零,说明有零
break;
}
}
//下面用第一行记录每一列(除了第0列)有没有零
for(int i=1;i<m;i++){ //从第0列到m-1列
for(int j=0;j<n;j++){ //枚举每一列的第0行到第n-1行
if(matrix[j][i]==0){ //只要这一列中有0存在,,就将第一行对于的存储本列的数matrix[0][i]赋值为0。
matrix[0][i]=0;
break;
}
}
}
//下面用第一列记录每一行(除了第0行)有没有零
for(int i=1;i<n;i++){ //从第0行到n-1行
for(int j=0;j<m;j++){ //枚举每一行的第0列到第n-1列
if(matrix[i][j]==0){ //只要这一行中有0存在,,就将第一列对于的存储本行的数matrix[0][j]]赋值为0。
matrix[i][0]=0;
break;
}
}
}
//下面需要对矩阵置零,
//只要这一行有零就将这一行赋值为0
for(int i=1;i<m;i++){ //枚举除了第一列的每一列
if(matrix[0][i]==0){ //如果这一列的第一行那个数存储的是零,就将这一列每一个元素均赋值为零
for(int j=0;j<n;j++){
matrix[j][i]=0;
}
}
}
//只要这一行有零就将这一行赋值为0
for(int i=1;i<n;i++){ //枚举除了第一行的每一行
if(matrix[i][0]==0){ //如果这一行的第一列那个数存储的是零,就将这一行每一个元素均赋值为零
for(int j=0;j<m;j++){
matrix[i][j]=0;
}
}
}
if(r0==0){ //如果r0存储的是0,就将第一行所有元素赋值为零
for(int i=0;i<m;i++){
matrix[0][i]=0;
}
}
if(c0==0){ //如果c0存储的是0,就将第一列所有元素赋值为零
for(int i=0;i<n;i++){
matrix[i][0]=0;
}
}
//返回为空,这里不需要返回值。
}
};
2021年9月17日17:29:25:
//如果有一个元素是0,我们就需要将这个0所在的行和列中的所有元素均设置为0,注意矩阵中的元素不止0和1,还有其他元素
class Solution {
public void setZeroes(int[][] matrix) {
int n=matrix.length,m=matrix[0].length; //求出矩阵的行和列
int r0=1,c0=1; //r0,c0分别用来记录第一行和第一列中是否有0,等于0表示有0,等于1表示没有0,我们先初始化为1表示没有0
//我们要先更新第一行和第一列中是否有0,否则我们再更新中间元素的时候,会改变其值
for(int i=0;i<m;i++) { //遍历第一行中的每一列
if(matrix[0][i]==0) r0=0; //第一行中只要有0,r0就更新为0,表示第一行中有0
}
for(int i=0;i<n;i++){
if(matrix[i][0]==0) c0=0; //第一列中只要有0,c0就更新为0,表示第一列中有0,如果全是1,c0就不会被更新,即是1
}
//我们更新中间那些元素中是否有0
//先更新第一行,其记录的是对应的列上是否有0
for(int i=1;i<m;i++){ //看一下每一列
for(int j=0;j<n;j++){ //看一下每一行,所以matrix是matrix[j][i],不是[i][j],只要有0,我们就把第一行对应列的f[0][i]更新为0,表示有0
if(matrix[j][i]==0) matrix[0][i]=0;
}
}
//再更新第一列,其记录的是对应的行上是否有0
for(int i=1;i<n;i++){
for(int j=0;j<m;j++){
if(matrix[i][j]==0) matrix[i][0]=0;
}
}
for(int i=1;i<m;i++){ //看一下除了第一列之外的每一列是否应该被赋值为0
if(matrix[0][i]==0) {
for(int j=0;j<n;j++){ //在赋值的时候要把第一行也要赋值为0,所以我们的行是从0到n-1
matrix[j][i]=0;
}
}
}
for(int i=1;i<n;i++){ //看一下除了第一行之外的每一行是否应该被赋值为0
if(matrix[i][0]==0){
for(int j=0;j<m;j++){
matrix[i][j]=0;
}
}
}
//最后在看第一行和一列是否应该被赋值为0,注意在看第一行和第一列是否应该被赋值为0的时候要放到最后看,不应该早于上面,赋值可能就把所有的元素均赋值为0了
if(r0==0){ //如果第一行全是零,就将第一行中的所有元素均赋值为0
for(int i=0;i<m;i++){
matrix[0][i]=0;
}
}
if(c0==0){ //如果第一列全是零,就将第一列中的所有元素均赋值为0
for(int i=0;i<n;i++){
matrix[i][0]=0;
}
}
//返回是0,所以最后我们不需要返回任何东西,我们只需要把原矩阵改一下即可,不需要返回值
}
}
方法二:使用标记数组:
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean[] row = new boolean[m];
boolean[] col = new boolean[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
row[i] = col[j] = true;
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
}
}
2021年11月18日10:53:13:
//注意这个题目不是用的(二进制位存状态)的方法 题目要求我们我们不能开辟新的数组并且如果一个位置是0的话,就要把其所在行和列上的元素都初始化为0
//我们这里可以用原数组中第一行上的数来表示每一列(不包括第一行的数)中有没有0,用原数组中第一列上的数来表示每一行(不包括第一列)中有没有0,
//也就是除了左上角那个元素没有被我们用,其他的第一行和第一列的元素都被我们用来记录其他行和列中有没有0了
//这样最后除了第一行和第一列的状态,其他元素的状态都被我们记到了第一行和第一列中,而我们还需要开两个变量row0,col0来分别记录第一行和第一列中有没有0即可
//这样我们就只需要开两个变量和利用原矩阵的第一行和第一列就可以表示出来原矩阵的每一行和每一列中有没有0来表示清楚了
//这样我们就只使用了O(1)的空间,就达到了题目中的要求,
//需要注意就是遍历和赋值改变的顺序很重要,我们这里应该是应该先统计第一行和第一列中有没有0(否则如果我们先遍历看其他的行和列的话,因为我们要用第一行和列来存储,所以这里第一行和列就会被我们改变了)
//再遍历矩阵的其他行和列,用第一行和第一列的元素来记录有没有0,注意当用每一行的第一列的元素来记录其所在行有没有0的时候,我们要看的是这一整行(即从0到m-1),不是从1到m-1
//同理当用第一列的元素来记录有没有0的时候,注意当用第一行的每一列的元素来记录其所在列有没有0的时候,我们要看的是这一整行(即从0到n-1),不是从1到n-1
//脑袋瓜子灵光一点
class Solution {
public void setZeroes(int[][] mat) {
int n=mat.length,m=mat[0].length;
//先分别看第一行和第一列有没有0
int row=1,col=1; //row和col分别用来记录第一行和第一列的元素有没有0,当row和col为1的时候表示没有0,为0的时候表示有0
for(int i=0;i<n;i++){ //遍历每一行的第一列,如果有0的话,则col就是0
if(mat[i][0]==0) col=0;
}
for(int i=0;i<m;i++){ //遍历第一行的每一列,如果有0的话,则row就是0
if(mat[0][i]==0) row=0;
}
//再遍历除了第一行和第一列的矩阵中其他的行和列,如果有0的话,我们就把对应的的第一行和列上的元素改为0
for(int i=1;i<n;i++){ //从第二行开始遍历,如果有0的话,就把其第一列对应的位置上的数标记为0(不管其之前是不是0),表示这一行中有0
//这样只要之后这个位置上的数是0的话(不管是被我们改的,还是原本的就是0)其这一行都应该是0
for(int j=0;j<m;j++){ //注意这里看的是这一行的所有元素(所以是从0到m,注意不是从1到m)
if(mat[i][j]==0) mat[i][0]=0; //这一行中有0的话,就把这一行的第一列元素的值改为0,这样之后只要这个位置上是0的话,就把这一整行的数改为0
}
}
for(int i=1;i<m;i++){ //遍历除了第一列的每一列
for(int j=0;j<n;j++){ //这里是要遍历每一列的全部行
if(mat[j][i]==0) mat[0][i]=0; //注意这里i是列,j是行
}
}
//下面就要改变值了,在改变值的时候也要注意要先改变的是除了第一行和第一列的其他行和列,最后再改变第一行和第一列的状态
for(int i=1;i<n;i++){ //从第二行开始遍历
if(mat[i][0]==0){ //如果这一行的第一列上的数是0的话,我们就要把这一行的全部数改为0
for(int j=0;j<m;j++){ //遍历每一列,注意这里也是从0到m
mat[i][j]=0;
}
}
}
for(int i=1;i<m;i++){ //遍历除了第一行列的每一列
if(mat[0][i]==0){
for(int j=0;j<n;j++){
mat[j][i]=0; //注意这里j是行,i是列
}
}
}
//再使用row和col来改变第一行和第一列的值
for(int i=0;i<n;i++){ //看第一列是不是应该为0
if(col==0){
mat[i][0]=0;
}
}
for(int i=0;i<m;i++){ //看第一列是不是应该为0
if(row==0){
mat[0][i]=0;
}
}
//不需要返回值
}
}
75. 颜色分类(三指针)
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
示例 3:
输入:nums = [0]
输出:[0]
示例 4:
输入:nums = [1]
输出:[1]
提示:
n == nums.length
1 <= n <= 300
nums[i] 为 0、1 或 2
算法分析:
三指针;
如图所示,[0,j - 1]
维护的是全是0
的区间,[j,i - 1]
维护的是全是1
的区间,[k + 1,n - 1]
维护的是全是2
的区间,而[i,k]
区间的元素是还没放的数(或者说还没有确定其值的数)
枚举整个数组,若当前枚举到的位置是i
,
- 1、若
nums[i] == 0
,则交换i
和j
的位置的元素,由于nums[i] == 0
且nums[j] == 1
,因此交换后i和j
同时往后移动1
位 - 2、若
nums[i] == 1
,则直接i ++
- 3、若
nums[i] == 2
,则交换i
和k
位置的元素,由于nums[i] == 2
,因此填入到k
位置时,k
的元素就是2
,因此k
需要往前移动1
位,而i
的元素未知,不做移动
时间复杂度 O(n)
双指针算法 这个算法比较特殊,直接背过就好
i
, j
指向数组头部,k
指向尾部
如果 a[i] = 0
交换 a[i]
,a[j]
, i ++ j ++
如果 a[i] = 1 i ++
如果 a[i] = 2
交换 a[i] a[k], k ++
时间复杂度:O(n),空间复杂度:O(1)
代码:
class Solution {
public:
void sortColors(vector<int>& nums) {
for(int i=0,j=0,k=nums.size()-1;i<=k;) //i,j指向开头,k指向尾元素,i值最大为k,注意这里i最大可以为k,即是结束条件,且不要写i++,j++,k--这样的语句,因为在下面的语句中均会改变其值,这里一定不要写。
if(nums[i]==0){ //如果i值为0,就交换nums[i]和nums[j],同时让i和j往后移动一位,因为j值一定为1(看图),所以i值可以放心往后移动。
swap(nums[i],nums[j]);
i++,j++;
}
else if(nums[i]==2) { //如果i值为2,就交换nums[i]和nums[k],同时让k往前移动一位,但是由于ak移动过来的值不确定是不是1,所以i不能往后移动。而是继续判断。
swap(nums[i],nums[k]);
k--;
}else{ //如果i值为1,则不用交换元素,直接往后移动i即可。
i++;
}
return; //返回值类型是void,所以这里返回空
}
};
注意如果不考虑题目要求,直接去做的话,我们可以使用三个变量:a,b,c,我们扫描一遍数组,统计一下0,1,2的数量,用a,b,c进行统计,之后我们在写a个0,b个1,c个2即可。
79. 单词搜索
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false
提示:
board 和 word 中只包含大写和小写英文字母。
1 <= board.length <= 200
1 <= board[i].length <= 200
1 <= word.length <= 10^3
算法分析
dfs
从单词矩阵中枚举每个单词的起点,从该起点出发往四周
dfs
搜索目标单词,并记录当前枚举到第几个单词,若当前搜索到的位置(i,j)
的元素恰好是word单词
第depth
个字符,则继续dfs
搜索,直到depth
到最后一个字符则表示有了符合的方案,返回true
注意:搜索过的位置继续搜索下一层时,需要对当前位置进行标识,表示已经搜索
时间复杂度 O(n2)*(3k)
对于每个起点,
k
表示搜索的单词的长度,搜索的时候除了来了方向不用搜索,其余3
个方向都需要进行搜索,因此3k种情况,一共有n2个起点,因此时间复杂度是O(n2)*(3k)
代码:
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
int n=board.size(); //一共多少行单词
int m=board[0].size(); //每一行有多少个字符
//下面就是对每一个字符作为递归起点
for(int i=0;i<n;i++){ //
for(int j=0;j<board[i].size();j++){
if(dfs(board,word,0,i,j)) return true; //每次递归结束,只要找到了要求的单词,就返回true
}
}
return false; //当所有字符作为起点均不存在要求的单词就返回false。
}
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1}; //定义方向矩阵(偏移量),分别代表上-0,右-1,下-2,左-3。
bool dfs(vector<vector<char>>& board,string& word,int u,int x,int y){ //编写递归函数,第一个参数是要被查找的数组(要改变数组中的字符,所以用&),第二个参数是要查找的单词,
//第三个参数u是查找到了单词的第几个位置,x,y是本次递归查找的起点
if(board[x][y]!=word[u]) return false; //如果每次递归的时候不等于word[u]说明这个位置字符不相等,结束本次递归
if(u==word.size()-1) return true; //说明此时我们已经搜到了最后一个字符,并且没有执行上面的语句,即最后一个字符仍然相等就返回true即可。
//上面两个if语句没有执行,上面还在查找过程中或者说递归过程中(即board[x][y]==word[u])
char t=board[x][y]; //存储下来当前位置是什么
board[x][y]='.'; //用掉了board[x][y]就将其置为一个不存在的字符'.',表示这个位置已经被用过了,不要重复用。
for(int i=0;i<4;i++){ //枚举四个方向
int a=x+dx[i],b=y+dy[i]; //新方向的下标
if(a<0||a>=board.size()||b<0||b>=board[0].size()||board[a][b]=='.') continue; //如果出界还在这个下一个位置被用过了,就跳过这个字符,继续看下一个字符是否被用过
else { //否则说明下一个位置没有出界且没有被用过,我们就对其继续递归
if(dfs(board,word,u+1,a,b)) return true; //如果递归结果为true,则说明下一个位置的字符是匹配的,
}
}
board[x][y]=t; //恢复现场,执行到这里,说明26行的:if(dfs(board,word,u+1,a,b)) return true; 没有被执行,
//即下一个位置的字符上下左右递归均是不匹配的,我们就需要更换新的起始位置,一定要将21行:board[x][y]='.';恢复回来
return false; //还要返回false。
}
};
84. 柱状图中最大的矩形 (单调栈)
给定
n
个非负整数
,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为1
。求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为
1
,给定的高度为[2,1,5,6,2,3]
。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为
10
个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
算法分析
严格单调递增栈
对于每个柱子
i
,找到左边第一个比它小的柱子的位置left[i]
,和找到右边第一个比它小的柱子的位置right[i]
,(right[i] - left[i] - 1) * heights[i]
是当前柱子所能找到的最大的矩形面积
如何找到左边第一个比它小的柱子的位置?
维护一个严格单调递增的栈
,当枚举到当前柱子时,若栈顶元素的值比它大,则将栈顶元素pop出
,直到栈顶元素的值小于等于它为止,则栈顶元素记录的就是左边第一个比它小的元素的位置,最后把当前元素加入到栈中,继续维护栈的单调性
栈中存放的是下标,可以方便的计算宽度,且方便的对应到柱子的高度。
严格单调递增栈
时间复杂度 O(n)
先来看下示意图:
梳理下思路:
首先,遍历整个数组,对于每个元素都需要求两个东西:
第一,在它之前,比它小的第一个元素的索引。
第二,在它之后,比它小的第一个元素的索引。
并依次将其存储左右两个数组中。
然后,明确两点:
第一点,栈中存的是索引
,便于计算宽度,以及防止重复。
第二点,单调栈
具有严格单调递增
的特点。
时间复杂度 O(n)
代码:
class Solution {
public:
int largestRectangleArea(vector<int>& h) {
int n=h.size(); //柱子的个数
vector<int> left(n); //left数组记录每个柱子左边最靠近它且比它小的柱子的位置left[i]
vector<int> right(n); //right数组记录每个柱子右边最靠近它且比它小的柱子的位置right[i]
stack<int> stk; //严格单调栈,记录的是下标(方便计算宽度,且很容易对应高度),即stk.top()存的是下标,而h[stk.top()]存放的是柱子的高度
for(int i=0;i<n;i++){ //从前往后扫描整个数组
//下面要注意,因为我们要得到严格递增栈,所以是>=。不用忘了写=
while(stk.size()>0&&h[stk.top()]>=h[i]) stk.pop(); //栈不空并且栈顶元素的高度>=当前柱子高度,我们为了是栈是单调的,所以我们需要迭代将栈顶弹出
//如果迭代弹出栈顶,发现栈空了,说明左边柱子都比当前柱子高,我们就让left[i]=-1(为了方便计算宽度,我们让其为-1);
if(stk.empty()) left[i]=-1;
else left[i]=stk.top(); //如果迭代弹出栈顶,栈不空,则栈顶元素就是左边第一个严格小于当前柱子高度的,我们就让left[i]=stk.top()
stk.push(i); //之后再将当前元素压入栈中,继续构建单调栈
}
//下面同理,我们需要从右到左构建一个单调栈,注意将stk清空,或者重新申请一个栈。
//同理再从右往左扫描整个数组,找到每个柱子右边最靠近它且比他小的柱子高度构建数组right[i]
while(stk.size()) stk.pop(); //先清空栈或者重新申请一个栈
//stk=stack<int>();
for(int i=n-1;i>=0;i--){ //注意是从右往左,所以i从n-1开始递减到0
//下面要注意,因为我们要得到严格递增栈,所以是>=。不用忘了写=
while(stk.size()&&h[stk.top()]>=h[i]) stk.pop(); //栈不空,并且栈顶元素大于等于(构建的是严格单调栈)当前元素,我们就需要一直弹栈
if(stk.empty()) right[i]=n; //如果迭代弹栈之后,栈为空,则右边都比其低,让right[i]=n(方便计算宽度)
else right[i]=stk.top();
stk.push(i);
}
int res=0; //res记录答案
for(int i=0;i<n;i++){
res=max(res,(right[i]-left[i]-1)*h[i]); //right[i]在比它小的右边,left[i]在比比他小的左边,所以宽度要减一。
}
return res; //返回答案。
}
};
85. 最大矩形 (单调栈+动态规划)
给定一个仅包含
0
和1
、大小为rows x cols
的二维二进制矩阵,找出只包含1
的最大矩形
,并返回其面积
。
示例 1:
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6
解释:最大矩形如上图所示。
示例 2:
输入:matrix = []
输出:0
示例 3:
输入:matrix = [["0"]]
输出:0
示例 4:
输入:matrix = [["1"]]
输出:1
示例 5:
输入:matrix = [["0","0"]]
输出:0
提示:
rows == matrix.length
cols == matrix[0].length
0 <= row, cols <= 200
matrix[i][j] 为 '0' 或 '1'
算法分析
单调栈加动态规划
一共有
n
行,枚举每一行
作为基准线,以该基准线为底线往上的图像形成一个柱状图(黄色区域),用单调栈算出柱状图中最大的矩形( LeetCode 84题的解题方法 ),再算出所有基准线情况的最大值res
状态表示
h[i,j]表示所有以(i,j)为终点,能往上延伸的最大高度。
状态计算
若当前位置是0,则h[i,j] = 0
若当前位置是1,则h[i,j] = 1 + h[i,j]
时间复杂度 O(n2)
代码:
class Solution {
public:
int largestRectangleArea(vector<int>& h) {
int n=h.size();
stack<int> stk;
vector<int> l(n+10),r(n+10);
for(int i=0;i<n;i++){
//下面要注意,因为我们要得到严格递增栈,所以是>=。不用忘了写=
while(stk.size()&&h[stk.top()]>=h[i]) stk.pop();
if(stk.empty()) l[i]=-1;
else l[i]=stk.top();
stk.push(i);
}
while(stk.size()) stk.pop();
for(int i=n-1;i>=0;i--){
//下面要注意,因为我们要得到严格递增栈,所以是>=。不用忘了写=
while(stk.size()&&h[stk.top()]>=h[i]) stk.pop();
if(stk.empty()) r[i]=n;
else r[i]=stk.top();
stk.push(i);
}
int res=0;
for(int i=0;i<n;i++){
res=max(res,(r[i]-l[i]-1)*h[i]);
}
return res;
}
//上面即是上一题的代码,这一题我们用来计算面积
int maximalRectangle(vector<vector<char>>& matrix) {
//先判空,再求行数和列数
if(matrix.empty()||matrix[0].empty()) return 0; //如果矩阵为空,直接返回0。
int n=matrix.size(),m=matrix[0].size(); //n是行,m是列
vector<vector<int>> h(n+1,vector<int>(m+1,0)); //二维h数组表示每个数上面最多有多少个连续的1;且均初始化为0
//下面求各个h[i][j]
for(int i=0;i<n;i++){ //每一行
for(int j=0;j<m;j++){ //每一列
if(matrix[i][j]=='1'){ //如果这个数是1,
if(i>0) h[i][j]=1+h[i-1][j]; //如果不是第0行,h[i][j]=h[i-1][j]即这个点上面的情况
else h[i][j]=1; //否则就是1
}//如果这个数是0,就不用管它了,因为上面初始化的时候已经全部初始化为0了。
}
}
//最后h[i]记录的就是每一行上1的个数,即二维的上一题(柱子高度),
int res=0; //res记录最终答案
//枚举每一行
for(int i=0;i<n;i++){
res=max(res,largestRectangleArea(h[i])); //更新res。
}
return res;
}
};
90. 子集 II
给定一个可能包含重复元素的整数数组
nums
,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
//这个题目和78题不同,这个题目含有相同元素,78题每个数只能枚举0次或者1次,所以78题可以使用
二进制
的方法,但是这个题目中含有相同元素,每个数的枚举次数就不止0次或者1次了,可能有多次,所以就不能再使用二进制的方法了。
代码:
class Solution {
public:
vector<vector<int>> ans; //定义答案数组
vector<int> path; //用path记录当前方案
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end()); //先对数组进行排序
dfs(nums,0); //从第0个数开始递归枚举
return ans; //枚举完之后返回答案
}
void dfs(vector<int>& nums,int u){ //编写递归函数,u是数组的下标位置
//看21行代码就知道为什么这里要写u==nums.size()了。
if(u==nums.size()){ //如果u已经枚举完了最后一个数,说明我们已经找到了一种方案,就将这种方案加入到答案数组中
ans.push_back(path); //将这种方案加入到答案数组中
return; //结束本次递归
}
//下面我们来数一下当前这个数的总个数
int k=u+1; //从当前数的下一个数开始看
while(k<nums.size()&&nums[k]==nums[u]) k++; //数一下当前数一共多少个,最后k落在了最后一个等于当前数的下一个位置
int cnt=k-u; //当前数的个数为cnt
for(int i=0;i<=cnt;i++){ //枚举当前数的出现次数,最少0次,最多cnt次
dfs(nums,k); //开始递归枚举下一个数
path.push_back(nums[u]); //每次递归就在答案数组中加一个当前数
}
//恢复现场,每次递归就将当前加入的当前数清空
for(int i=0;i<=cnt;i++){
path.pop_back();
}
//返回值为空,这里不需要返回语句。
}
};
118. 杨辉三角
给定一个非负整数
numRows
,生成杨辉三角的前numRows
行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
算法分析
模拟递推
对于每一行的元素,除了第一个和最后一个元素都是
1
之外,其余的元素(i,j)
都是(i - 1,j - 1)
和(i - 1,j)
坐标的元素累加出来的结果 。
时间复杂度O(n^2)
代码:
//第一行一个数,第二行两个数,第n行n个数
class Solution {
public:
vector<vector<int>> generate(int n) {
vector<vector<int>> res; //定义答案数组
for(int i=0;i<n;i++){ //枚举每一行
vector<int> line(i+1); //定义这一行的数组,大小为元素个数即i+1,line数组记录这一行数的值
line[0]=line[i]=1; //每一行的第一个数和最后一个数赋值为1
for(int j=1;j<i;j++){ //此时j起到的作用就是i的作用,即对每一行的第一个和最后一行元素中间的元素进行赋值操作。
line[j]=res[i-1][j-1]+res[i-1][j]; //中间的数的值等于上一行左边的数+上一行上面的数
}
res.push_back(line); //将这一行答案放到答案数组中
}
return res; //最后将答案数组返回。
}
};
2021年9月9日10:07:29:
//这个题目不要直接按着题目给定的形状考虑,而应该将其视为一个直角三角形,递推,第一行有一个数,第二行有两个数...第n行有n个数,每一行的第一个数和最后一个数都是1
//行中的每一个数等于其正上方数加上左上方数(考虑成直角三角形,因为数组在记录的时候不是等腰三角形,而是一个直角三角形),
class Solution {
public List<List<Integer>> generate(int n) {
List<List<Integer>> f=new ArrayList<>(); //定义答案,数组f[i][j]表示(i,j)这个格子上的值,我们递推完成数组f的赋值
for(int i=0;i<n;i++){ //为每一行元素进行更新赋值,每一行元素的下标是从0~i,
List<Integer> line=new ArrayList<>(); //定义这一行的答案
line.add(1); //将每一行的第一个数赋值为1,即往数组中添加一个1即可
for(int j=1;j<i;j++){ //为每一行中的下标从1~i-1进行赋值
int t=f.get(i-1).get(j-1)+f.get(i-1).get(j); //得到(i,j)这个格子的左上方加上正上方元素之和
line.add(t); //将t放到这一行中
}
if(i>0) line.add(1); //除了第一行,每一行的最后一个元素值也为1,我们也要将这个1加到答案中,注意一定要特判让i>1
f.add(new ArrayList<>(line)); //将当前这一行赋值为之后加到答案中
}
return f; //最后将答案数组记得返回
}
}
119. 杨辉三角 II
给定一个非负索引
k
,其中k ≤ 33
,返回杨辉三角的第k
行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 3
输出: [1,3,3,1]
进阶:
你可以优化你的算法到 O(k) 空间复杂度吗?
算法一:(空间复杂度未改进)
class Solution {
public:
vector<int> getRow(int n) {
vector<vector<int>> f(n+1,vector<int>(n+1)); //res数组用于记录每行每列的数
for(int i=0;i<=n;i++){
f[i][0]=f[i][i]=1; //每一行第一个数和最后一个数赋值为1
for(int j=1;j<i;j++){
f[i][j]=f[i-1][j-1]+f[i-1][j]; //对每行的中间的数进行赋值
}
}
return f[n]; //将第n行的数返回输出。
}
};
java代码:
class Solution {
public List<Integer> getRow(int n) {
List<List<Integer>> f=new ArrayList<List<Integer>>(n+1);
for(int i=0;i<=n;i++){
List<Integer> p=new ArrayList<>(n+1); //这一句一定要写在这里,不能作为全局数组。
for(int j=0;j<=i;j++){
p.add(0);
}
f.add(new ArrayList<>(p));
}
System.out.println(f);
for(int i=0;i<=n;i++){
f.get(i).set(0,1);
f.get(i).set(i,1);
for(int j=1;j<i;j++){
f.get(i).set(j,f.get(i-1).get(j-1)+f.get(i-1).get(j));
}
}
return f.get(n);
}
}
算法二:(滚动数组进行改进)
改法套路:将数组大小改为2,将以后用到的状态第一维改为&1。
class Solution {
public:
vector<int> getRow(int n) {
vector<vector<int>> f(2,vector<int>(n+1)); //res数组用于记录每行每列的数
for(int i=0;i<=n;i++){
f[i&1][0]=f[i&1][i]=1; //每一行第一个数和最后一个数赋值为1
for(int j=1;j<i;j++){
f[i&1][j]=f[(i-1)&1][j-1]+f[(i-1)&1][j]; //对每行的中间的数进行赋值
}
}
return f[n&1]; //将第n行的数返回输出,这里也要&1。
}
};
2021年9月9日10:45:47:
//这个题目只让我们返回杨辉三角的某一行(这个题目是从0开始计数的),即n=0,我们返回第一行,n=1,我们返回第2行,...
//这个题目要求我们只能使用O(k)的空间,并且每个位置上的数只跟其正上方和左上方的数字有关,所以我们这个题目可以使用滚动数组来优化求解
//
// class Solution {
// public List<Integer> getRow(int n) {
// List<Integer> res=new ArrayList<>(); //定义答案
// int[][] f=new int[n+1][n+1]; //f[i][j]表示(i,j)这个格子上的值,因为n=0,我们要返回第1行,所以n=n,我们要返回第n+1行,即下标为n, 是f[n]
// for(int i=0;i<=n;i++){
// f[i][0]=f[i][i]=1; //每一行的第一个数和最后一个数赋值为1
// for(int j=1;j<i;j++){ //为这一行中的其他的位置上的数赋值
// f[i][j]=f[i-1][j-1]+f[i-1][j]; //更新f[i][j]
// }
// }
// for(int i=0;i<n+1;i++){ //第n行有n+1个元素
// res.add(f[n][i]); //将这一行的每一个元素都加到答案中去
// }
// return res; //最后记得将答案返回
// }
// }
//我们使用滚动数组来优化,因为f[i][j]=f[i-1][j-1]+f[i-1][j],即第i行的值只依赖于上一行的值,所以我们使用滚动数组来优化
//有一个模板:先用原方法求出f[i][j], 然后将数组的第一维改为2,其他用到第一维的地方改为原下标&1即可
class Solution {
public List<Integer> getRow(int n) {
List<Integer> res=new ArrayList<>();
int[][] f=new int[2][n+1]; //改为滚动数组之后答案f的第一维只需要2即可
for(int i=0;i<=n;i++){ //为每一行中的所有数字赋值,第0行有1个元素,第1行有2个元素,第n行有n+1个元素,所以我们要从0循环到n
f[i&1][0]=f[i&1][i]=1; //将下面所有用到一维的地方全部改为&1
for(int j=1;j<i;j++){ //为每一行中间的数赋值
f[i&1][j]=f[i-1&1][j-1]+f[i-1&1][j]; //将下面所有用到一维的地方全部改为&1
}
}
for(int i=0;i<=n;i++){ //第n行共n+1个元素,所以赋值是从0到n
res.add(f[n&1][i]); //将第n行数组的每一个数放到答案数组列表中,将下面所有用到一维的地方全部改为&1
}
return res;
}
}
120. 三角形最小路径和
给定一个三角形
triangle
,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点
在这里指的是下标
与上一层结点下标
相同或者等于上一层结点下标 + 1
的两个结点。也就是说,如果正位于当前行的下标i
,那么下一步可以移动到下一行的下标i
或i + 1
。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]]
输出:-10
进阶:
你可以只使用
O(n)
的额外空间(n
为三角形的总行数)来解决这个问题吗?
代码:
class Solution {
public:
int minimumTotal(vector<vector<int>>& f) {
int n=f.size(); //求出一共多少行
for(int i=n-2;i>=0;i--){ //从倒数第二行开始往上枚举每一行。
for(int j=0;j<=i;j++){ //每一行的每一个数
f[i][j]+=min(f[i+1][j],f[i+1][j+1]); //从下往上更新记录最小路径和,这里没有使用新的变量,直接用f[i][j]更新记录到达这个点的最小路径和。
}
}
return f[0][0]; //最后将最顶点记录的最小路径和返回输出即可。
}
};
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择
某一天
买入这只股票,并选择在未来的某一个不同的日子
卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。
如果你不能获取任何利润,返回 0
。
算法分析
- 1、当枚举到
i
时,minv
维护的是[0,i-1]
最小的价格,price[i]-minv
是在当前点i
买入的最大收益,- 2、计算所有点的最大收益取最大值
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res=0; //res记录获得的最大价值,原本应该初始化为最小值,但是我们这里为了保证不赔钱,我们设置res等于0。
for(int i=0,minp=INT_MAX;i<prices.size();i++){ //我们从前往后扫描整个数组,一开始的最小值minp初始化为最大值。
res=max(res,prices[i]-minp); //我们假设在第i天卖出,我们获得的最大利润就是当天的价值减去前面的最小值。
minp=min(minp,prices[i]); //记得更新最小值,最小值是前面天数中值最小的。
}
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。
不能赔钱交易。
提示:
- 1 <= prices.length <= 3 * 10 4
- 0 <= prices[i] <= 10 4
代码:
class Solution {
public:
int maxProfit(vector<int>& nums) {
int n=nums.size(); //数组长度,即一共多少天
int res=0; //初始化最大收益为0,不能为负值,不能赔钱
for(int i=0;i<n-1;i++){ //枚举每一天,看第7行代码就知道我们这里要小于n-1,而不是平时的n
res+=max(0,nums[i+1]-nums[i]); //如果有正收益就加到答案数组里面,否则就加0
}
return res; //最后将答案返回。
}
};
152. 乘积最大子数组
给你一个整数数组
nums
,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
算法分析
dp + 滚动数组优化
f[i]表示所有从0到i并且选用nums[i]获得的最大乘积
g[i]表示所有从0到i并且选用nums[i]获得的最小乘积
1、
当nums[i] >= 0时,f[i] = max(nums[i], f[i - 1] * nums[i])
当nums[i] < 0时,f[i] = max(nums[i], g[i - 1] * nums[i])
2、
当nums[i] >= 0时,g[i] = min(nums[i], g[i - 1] * nums[i])
当nums[i] < 0时,g[i] = min(nums[i], f[i - 1] * nums[i])
可以通过用滚动数组的方式,记录 f = f[i - 1], g = g[i - 1]
时间复杂度 O(n)
我们可以合并统一为f[i]=max(ai,max(f[i],g[i]));
代码:
class Solution {
public:
int maxProduct(vector<int>& nums) {
int res=nums[0]; //记录最大乘积,最小为nums[0]
int f=nums[0],g=nums[0]; //f记录之前的最大值,g记录之前的最小值(但是可能nums[i]是负值,乘以g之后就会变成最大值),注意都要初始化为nums[0]
for(int i=1;i<nums.size();i++){ //从第二个数开始枚举
//下面先求出三种情况的值
int a=f*nums[i],b=g*nums[i],c=nums[i]; //分别求出三种情况下的值,经过这一步b可能由于负负得正就变成了最大值(也可能变得更小,所以就有了第10行代码)
f=max(a,max(b,c)); //更新记录最大值
g=min(a,min(b,c)); //重新将g更新为最小值
res=max(res,f); //更新记录最大值
}
return res; //将结果返回
}
};
164. 最大间距
给定一个无序的数组,找出数组在排序之后,相邻元素之间最大的差值。
如果数组元素个数小于 2,则返回 0。
示
例 1:
输入: [3,6,9,1]
输出: 3
解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
示例 2:
输入: [10]
输出: 0
解释: 数组元素个数小于 2,因此返回 0。
说明:
你可以假设数组中所有元素都是非负整数,且数值在 32 位有符号整数范围内。
请尝试在线性时间复杂度和空间复杂度的条件下解决此问题。
算法分析:
如果不考虑题目中的时间和空间,暴力用sort来做的话,答案放到最下面。
考虑时间复杂度的要求:
不考虑题目中的要求:
class Solution {
public:
int maximumGap(vector<int>& nums) {
int res=0;
int n=nums.size();
if(n<2) return 0;
sort(nums.begin(),nums.end());
for(int i=0;i<n-1;i++){
res=max(res,nums[i+1]-nums[i]);
}
return res;
}
};
189. 旋转数组
给定一个数组,将数组中的元素
向右移动
k
个位置,其中k
是非负数
。
进阶:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。 你可以使用空间复杂度为
O(1)
的 原地 算法解决这个问题吗?
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 2 * 10^4
-2^31 <= nums[i] <= 2^31 - 1
0 <= k <= 10^5
算法分析
3次翻转
- 1、整个链表进行翻转
- 2、将
0
到k - 1
的位置进行翻转- 3、将
k
到n - 1
的位置进行翻转
k
的数字可能比n
大,因此需要对k
取模,k = k % n
时间复杂度 O(n)
代码:
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n=nums.size(); //求出数组长度
k%=n; //注意先取模
reverse(nums.begin(),nums.end()); //先对整个数组翻转
reverse(nums.begin(),nums.begin()+k); //再对0~k-1元素进行翻转
reverse(nums.begin()+k,nums.end()); //最后对k~n-1元素进行翻转
//返回为空,不用返回语句。
}
};
2021年9月17日18:21:55:
//使用三次翻转即可,第一次翻转整个数组,第二次翻转前k个元素,第三次翻转后面n-k个元素,比如:1,2,3,4,5,6,7,k=3,第一次翻转整个数组:7,6,5,4,3,2,1
//第二次翻转前3个元素:5,6,7,4,3,2,1,第三次翻转后7-3=4个元素:5,6,7,1,2,3,4,我们发现这就是答案,而且题目不要求我们返回任何数据,所以这里我们无需返回
//注意k有可能大于n,而大于n是没有意义的,所以我们需要先用k对n取模,并且java中对于数组没有reverse函数,我们需要自己实现
//这样我们就满足了空间复杂度是O(1)的要求
class Solution {
public void reverse(int[] nums,int l,int r){
while(l<r){
int t=nums[l];
nums[l]=nums[r];
nums[r]=t;
l++;r--; //这两步一定不要忘了写,否则就是死循环了
}
}
public void rotate(int[] nums, int k) {
int n=nums.length;
k%=n; //k对n取模,这一步一定要写,否则就是错误的
reverse(nums,0,n-1); //第一次对整个数组进行翻转
reverse(nums,0,k-1); //第二次对前k个元素翻转,
reverse(nums,k,n-1); //第三次对后n-k个元素翻转
//注意这个题目不需要我们返回任何东西,所以这里我们不要写return语句
}
}