今日内容
- leetcode.209 长度最小子数组
- leetcode. 59 螺旋矩阵Ⅱ
- 前缀和
之前刷的时候还没有区间和,结果不知道什么时候更新了。果然是温故而知新。
Leetcode. 209 长度最小子数组
本题引入了滑动窗口的概念。其实说是说滑动窗口,本质上还是双指针。
此处需要明确的一点就是:当我们循环时,调整的是滑动窗口的起始指针还是终点指针呢?
假设我们在循环里调整的是滑动窗口的起始指针,那么就会发现我们不知道终点指针该在何处停下,这就又需要我们循环遍历符合条件的终点指针。没错,又回到暴力法了。
所以滑动窗口在循环中是调整终点指针。
这样,就有如下步骤:
- 循环调整滑动窗口的终点指针。
- 当窗口内元素符合条件,记录当前窗口。将起始指针往后移动(相当于把窗口首个元素剔除)。
- 若起始指针移动后窗口内元素依然符合条件,则继续重复第2步;否则,再从第1步开始。
本题代码如下:
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int start = 0; // start:起始指针
int sum = 0;
int min = Integer.MAX_VALUE;
for (int end = 0; end < nums.length; end++){ // end: 终点指针
sum += nums[end];
while (sum >= target){ // 满足条件,调整起始指针。注意用的是while
min = min < (end - start) + 1 ? min : (end - start) + 1;
sum = sum - nums[start];
start++; // 移动起始指针
}
}
if (min == Integer.MAX_VALUE){min = 0;} // 如果遍历后min依然为int最大值,说明没找到。赋值为0
return min;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(1)
Leetcode. 59 螺旋矩阵Ⅱ
这道题重要的还是循环不变量,确定好整个代码的区间定义就比较清晰了。
当然上面的只是思路,我也知道该这样做,但是思路虽然清晰,我代码写不出来口牙!!!!!代码能力属实弱鸡了。
在本题中,我们确定区间为左闭右开:
这样就不会糊里糊涂了。
代码如下:
class Solution {
public int[][] generateMatrix(int n) {
int[][] result = new int[n][n];
// 起始X和Y
int startX = 0;
int startY = 0;
int offset = 1; // 偏移量
int count = 1; // 当前填入数字
int circle = n / 2; // 要旋转的圈数
int i, j;// 真正要移动的X和Y, i表示行,j表示列
while (circle > 0){
// 从左到右的循环
for (j = startY; j < n - offset; j++){
result[startX][j] = count;
count++;
}
// 从上到下的循环
for (i = startX; i < n - offset; i++){
result[i][j] = count; // 此处因为之前的循环使 j 已经到了最右边的列,所以指代列的索引可以使用 j
count++;
}
// 从右到左的循环
for (; j > startY; j--){ // 此处不做初始化定义是因为填充列的操作只需要左右移动就好,真正决定要填充哪一行的列依靠的是 i 指向哪一行
result[i][j] = count; // i 已经指向了最下边的行,可以直接用它当行索引
count++;
}
// 从下到上的循环
for (; i > startX; i--){ // 与从右到左同理
result[i][j] = count; // j 已经指向最左边的列,直接用它当列索引
count++;
}
// 跑完一轮,更新参数
startX++;
startY++;
offset++;
circle--;
}
if (n % 2 != 0){
result[startX][startY] = count;
}
return result;
}
}
感觉主要难点还是在于突然增多的参数量。
下面我按照个人理解,整理整理这段代码中的各个参数:
- startX,startY:这两个参数就是划定起点的,它在整个代码中只会在一圈跑完后进行变更。
- offset:偏移量。因为区间定义为左闭右开,所以offset的作用就是保证区间定义无误。
- count:这个没什么说的,就是要填的数字。
- circle:要转的圈数。至于为什么 n / 2 就能得出要转的圈数,个人是这么想的:转完一圈之后,最上面一行和最下面一行就不管了,也就是说一圈之后会少两行。而现在总共有 n 行,也就是说会最多会减少 n / 2 次。所以 n / 2 就是圈数。
- i,j:前面的 startX,startY 指明了起点,i 和 j 才是真正跑来跑去用来定位的牛马。
前缀和
前缀和非常适合解决区间和问题。
区间和,顾名思义,就是求数组的某一区间的元素和。
面对这种问题,第一反应就是使用暴力法。但假如我们极端一点,我们要查询 m 次,每次查询范围为 0 到 n - 1,那么它的时间复杂度为O(n * m)。当查询次数多起来的话,那耗时已经不敢想象了。
基于此就引入前缀和的思想。
前缀和:重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。
以下图为例,简单解释一下何为前缀和:
vec[i] 表示数组,p[i] 表示从下标 0 到下标 i 的累加和。
所以当我们想要求区间 2 到 5 的区间和时,就可以使用 p[5] - p[1]得到,因为:
p[1] = vec[0] + vec[1];
p[5] = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5];
p[5] - p[1] = vec[2] + vec[3] + vec[4] + vec[5];
这样仅需要时间复杂度O(1)就可以获得区间和了。
卡码网. 58 区间和
本题直接用前缀和就好,代码如下:
import java.util.Scanner;
class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 数组长度
int[] array = new int[n]; // 整数数组
int[] prefix = new int[n]; // 前缀和数组
int sum = 0; // 前缀和
for (int i = 0; i < n; i++){
array[i] = sc.nextInt();
sum += array[i];
prefix[i] = sum;
}
while (sc.hasNextInt()){
int a = sc.nextInt();
int b = sc.nextInt();
if (a > b){
continue;
} else if (a == 0){
int output = prefix[b];
System.out.println(output);
} else {
int output = prefix[b] - prefix[a - 1];
System.out.println(output);
}
}
sc.close();
}
}
- 时间复杂度:O(1)
- 空间复杂度:O(n)
卡码网. 44 开发商购买土地
这道题一开始看代码随想录上的文字描述没怎么读懂,到题目链接上看到题之后才看懂....主要看不懂的地方在于“提示信息”那一块。
看懂之后,本题总结起来就是说:有一块 n * m 的区域,可以按照横向或纵向划分为两个子区域,我们需要找到两个子区域间差距最小的划分。
此题本质上就是要比较若干行(或者列)之间的数值和。一看到数值和,直接联系上前缀和。我们就做一个行的前缀和数组,一个列的前缀和数组,然后进行比较。
代码如下:
import java.util.Scanner;
class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
// 区域地块的行列数
int n = sc.nextInt();
int m = sc.nextInt();
// 区域地块二维数组
int[][] allArea = new int[n][m];
int sum = 0;
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
allArea[i][j] = sc.nextInt();
sum += allArea[i][j]; // 区域地块的总体权值
}
}
int[] horizon = new int[n]; // 行的前缀和数组
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
if (i == 0){
horizon[i] += allArea[i][j];
}else{
horizon[i] = horizon[i - 1] + allArea[i][j];
}
}
}
int[] vertical = new int[m]; // 列的前缀和数组
for (int j = 0; j < m; j++){
for (int i = 0; i < n; i++){
if (j == 0){
vertical[j] += allArea[i][j];
}else{
vertical[j] = vertical[j - 1] + allArea[i][j];
}
}
}
int result = Integer.MAX_VALUE;
for (int i = 0; i < horizon.length; i++){
int elem = sum - horizon[i]; // horizon[i] 表示前 i 行的土地价值。所以 elem 表示剩下几行的土地价值
result = result < Math.abs(horizon[i] - elem) ? result : Math.abs(horizon[i] - elem);
}
for (int i = 0; i < vertical.length; i++){
int elem = sum - vertical[i]; // vertical[i] 表示前 i 列的土地价值。所以 elem 表示剩下几列的土地价值
result = result < Math.abs(vertical[i] - elem) ? result : Math.abs(vertical[i] - elem);
}
System.out.println(result);
sc.close();
}
}
- 时间复杂度:O(n ^ 2)
- 空间复杂度:O(max(n, m))【算法新手,不确定是不是这样的】
总结
至此,数组相关的问题就解决了。
其中知道了数组是存放在连续内存空间中的相同类型的数据集合,数组元素的删除实际上是覆盖。
之后从数组的经典问题中总结出了几种解决方法:
- 二分法:适用于 有序、元素不重复 的数组,注意循环不变量。
- 双指针:常见于数组和链表
- 滑动窗口:本质上也是双指针,注意滑动窗口调整的是终点指针。
- 前缀和:适用于解决数组区间元素和的问题。
再次刷题收获满满,真的温故而知新吧。