977.有序数组的平方
思路:
- 暴力法:先遍历更新数组,再排序;
- 双指针法:数组有序,只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,分别指向起始位置和终止位置,共同向中间移动。
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置,从大到小赋值。
class Solution {
public int[] sortedSquares(int[] nums) {
int right = nums.length - 1;
int left = 0;
int[] result = new int[nums.length];
int index = result.length - 1;
while (left <= right) {
if (nums[left] * nums[left] > nums[right] * nums[right]) {
// 正数的相对位置是不变的, 需要调整的是负数平方后的相对位置
result[index--] = nums[left] * nums[left];
++left;
} else {
result[index--] = nums[right] * nums[right];
--right;
}
}
return result;
}
}
209.长度最小的子数组
思路:
- 暴力法:两个for循环,第一个for遍历子数组起始下标,第二个for寻找满足元素和>=target的终止下标。
class Solution { //遗憾,暴力法超出时间限制
public int minSubArrayLen(int target, int[] nums) {
int ans = Integer.MAX_VALUE;
int subLen = 0; //子数组长度
for(int i=0; i<nums.length; i++) {
int sum = 0; //注意每次更新i下标时更新sum值
for(int j=i; j<nums.length; j++) {
sum += nums[j]; //更新当前子数组元素和
subLen = j - i + 1; //更新子数组长度
if(sum>=target) {
ans = subLen<ans ? subLen : ans;
break; //下标j不需再向后遍历
}
}
}
if(ans == Integer.MAX_VALUE) return 0; //所有元素和都小于target的情况
return ans;
}
}
- 滑动窗口法:滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。暴力解法用两个for循环 完成了一个不断搜索区间的过程。那么滑动窗口如何用一个for循环来完成这个操作呢。
在本题中实现滑动窗口,主要确定如下三点:- 窗口内是什么?
窗口就是满足其和 ≥ target 的长度最小的连续子数组。 - 如何移动窗口的起始位置?
如果当前窗口的值大于target了,窗口就要向前移动了(也就是该缩小了)。 - 如何移动窗口的结束位置?
窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
- 窗口内是什么?
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
滑动窗口本质是满足了单调性,即左右指针只会往一个方向走且不会回头。收缩的本质即去掉不再需要的元素。也就是做题我们可以先固定移动右指针,判断条件是否可以收缩左指针算范围。
class Solution {//时间复杂度:O(n),其中n是数组的长度。指针left和right最多各移动n次
// 滑动窗口
public int minSubArrayLen(int s, int[] nums) {
int left = 0;
int sum = 0;
int result = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= s) {
result = Math.min(result, right - left + 1);
sum -= nums[left++];
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
//双端队列实现滑动窗口
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int result = Integer.MAX_VALUE;
ArrayDeque<Integer> arrayDeque = new ArrayDeque();
int sum = 0;
for(int i : nums){
arrayDeque.addLast(i);
sum +=i;
while(sum >= target){
result = Math.min(result,arrayDeque.size());
int tmp = arrayDeque.removeFirst();
sum -=tmp;
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
- 前缀和+二分法找边界:前缀和数组主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。(相对的有差分数组)
这道题保证数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分法的正确性。如果题目没有说明数组中每个元素都为正,这里就不能使用二分来查找这个位置了。
class Solution {//时间复杂度:O(nlogn)
public int minSubArrayLen(int target, int[] nums) {
int ans = Integer.MAX_VALUE;
int n = nums.length;
int[] preSum = new int[n+1];
//前缀和数组,preSums[i]表示从 nums[0]到 nums[i−1]的元素和
//nums下标i到j之间的元素和即可表示为preSums[j+1]-preSums[i]
for(int i=1; i<=n; i++) {
preSum[i] = preSum[i-1] + nums[i-1];
}
for (int i = 0; i < n; i++) { //遍历起始下标
int l=i, r=n-1;
//在i到n-1的下标中二分寻找终止下标j,使得i到j的元素和≥target
//j为所有满足条件的位置区间左边界(保证子数组长度最小)
while(l <= r){
int mid = l + ((r-l)>>1);
if(preSum[mid+1]-preSum[i] >= target){ //preSum[mid+1]-preSum[i]计算i到mid子数组元素和
r = mid - 1;
}else if(preSum[mid+1]-preSum[i] < target){
l = mid + 1;
}
}
if(l<n) ans = Math.min(ans, l - i + 1);//若l=n,说明原数组所有元素和小于target
}
return ans==Integer.MAX_VALUE ? 0 : ans;
}
}
相关题目:
904.水果成篮
76. 最小覆盖子串
补充:差分数组知识
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
比如说,我给你输入一个数组 nums
,然后又要求给区间 nums[2..6]
全部加 1,再给 nums[3..9]
全部减 3,再给 nums[0..4]
全部加 2,再给…一通操作猛如虎,然后问你,最后 nums
数组的值是什么?
这里就需要差分数组的技巧,类似前缀和技巧构造的 prefix
数组,我们先对 nums
数组构造一个 diff
差分数组,diff[i]
就是 nums[i]
和 nums[i-1]
之差:
//构造差分数组
int n = nums.size();
vector<int> diff(n,0);
diff[0]=nums[0];
for (int i = 1; i < n; i++) {
diff[i] = nums[i] - nums[i - 1];
}
通过这个 diff
差分数组是可以反推出原始数组 nums
的,代码逻辑如下:
// 根据差分数组构造结果数组
int n = diff.size();
vector<int> res(n,0);
res[0]=diff[0];
for (int i = 1; i < n; i++) {
res[i] = res[i - 1] + diff[i];
}
这样构造差分数组 diff
,就可以快速进行区间增减的操作,如果你想对区间 nums[i..j]
的元素全部加 3,那么只需要让 diff[i] += 3
,然后再让 diff[j+1] -= 3
即可。
diff[i] += 3
意味着给 nums[i..]
所有的元素都加了 3,然后 diff[j+1] -= 3
又意味着对于 nums[j+1..]
所有元素再减 3,那综合起来,就是对区间 nums[i..j]
中的所有元素都加 3 了。
只要花费 O(1) 的时间修改 diff
数组,就相当于给 nums
的整个区间做了修改。多次修改 diff
,然后通过 diff
数组反推,即可得到 nums
修改后的结果。
59.螺旋矩阵II
思路:
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
由外向内一圈一圈这么画下去。
class Solution {
public int[][] generateMatrix(int n) {
int[][] res = new int[n][n];
int top = 0, left = 0, right = n-1, bottom = n-1;
int num = 1;
int end = n * n;
while (num <= end)
{
for (int i = left; i <= right; i++) res[top][i] = num++; //i代表列---从左到右
top++;
for (int i = top; i <= bottom; i++) res[i][right] = num++; //i代表行---从上到下
right--;
for (int i = right; i >= left; i--) res[bottom][i] = num++; //i代表列---从右到左
bottom--;
for (int i = bottom; i >= top; i--) res[i][left] = num++; //i代表行---从下到上
left++;
}
return res;
}
}