●day 2 任务以及具体安排:第一章 数组part02
第一题:209. 长度最小的子数组 - 力扣(LeetCode)
解题思路
本题要求找出一个连续子数组 ,其总和≥target ,且长度最小 。由于数组中元素均为正整数,可以使用滑动窗口法 高效求解,时间复杂度为 O(n)。以下是核心思路:
核心思想
- 滑动窗口法 :利用两个指针(
left
和right
)表示窗口的左右边界。窗口内的元素和sum
逐步调整,确保每次操作都能有效缩小搜索范围。 - 窗口扩展 :右指针
right
向右移动时,将当前元素值累加到sum
中。 - 窗口收缩 :当
sum ≥ target
时,计算当前窗口长度,并尝试通过移动左指针left
来缩小窗口,寻找更小的满足条件的子数组。 - 记录最小长度 :每次窗口满足条件时,更新最小长度
result
,初始设为Integer.MAX_VALUE
,若最终未更新则返回 0。
代码
class Solution {public int minSubArrayLen(int target, int[] nums) {// 初始化左指针、当前窗口和、最小长度int left = 0;int sum = 0;int result = Integer.MAX_VALUE;// 右指针从 0 开始遍历数组for (int right = 0; right < nums.length; right++) {// 将当前元素值累加到窗口和中sum += nums[right];// 当窗口和满足 ≥ target 时,尝试缩小窗口以寻找更小的长度while (sum >= target) {// 更新当前窗口的最小长度result = Math.min(result, right - left + 1);// 缩小窗口:减去左指针的元素值,并右移左指针sum -= nums[left];left++;}}// 如果 result 未被更新,说明无满足条件的子数组,返回 0return result == Integer.MAX_VALUE ? 0 : result;}
}
第二题:59. 螺旋矩阵 II - 力扣(LeetCode)
解题思路
本题要求生成一个 n x n
的螺旋矩阵,其中的元素从 1
到 n²
按顺时针螺旋顺序填充。该问题的核心在于模拟螺旋填充的过程 ,通过分层遍历的方式逐圈填充矩阵的四边,并正确处理边界条件。
核心思想
- 分层填充 :将矩阵分为若干圈(层),每圈处理四条边(上、右、下、左),直到填充完整个矩阵。
- 起始点控制 :每圈的起始点为
(startX, startY)
,初始为(0, 0)
,每完成一圈后更新起始点。 - 边界控制 :使用
offset
来控制每圈的边界范围,避免覆盖已填充的区域。 - 奇数中心点处理 :当
n
为奇数时,最后一圈会剩下一个中心点需要单独填充。 - 填充顺序的记忆口诀
上行左到右,列增行不动;
右边上到下,行增列不动;
下行右到左,列减行不动;
左边下到上,行减列不动。
填完一圈,起点进,偏移增;
奇数中心点,单独补。
代码
class Solution {public int[][] generateMatrix(int n) {// 初始化 n x n 的矩阵int[][] nums = new int[n][n];// 定义每一圈的起始位置 (startX, startY)int startX = 0, startY = 0;// 定义循环次数、偏移量、计数器int loop = 1, offset = 1, count = 1;// 每圈循环填充四条边,直到填满所有圈while (loop <= n / 2) {// 1. 填充上边:从左到右(左闭右开)for (int j = startY; j < n - offset; j++) {nums[startX][j] = count++;}// 2. 填充右边:从上到下(上闭下开)for (int i = startX; i < n - offset; i++) {nums[i][n - offset] = count++;}// 3. 填充下边:从右到左(右闭左开)for (int j = n - offset; j > startY; j--) {nums[n - offset][j] = count++;}// 4. 填充左边:从下到上(下闭上开)for (int i = n - offset; i > startX; i--) {nums[i][startY] = count++;}// 进入下一圈loop++;offset++;startX++;startY++;}// 如果 n 为奇数,填充中心点if (n % 2 == 1) {nums[startX][startY] = count;}return nums;}
}
细节解释
循环条件 loop <= n / 2
的核心目的是控制螺旋矩阵的填充层数 。具体原因如下:
1. 分层填充的逻辑
螺旋矩阵的填充是按圈进行的,每一圈对应矩阵的一层。例如:
- 当
n = 3
(奇数)时,需要填充 1 层外圈 (覆盖 8 个元素),最后中心点单独填充。 - 当
n = 4
(偶数)时,需要填充 2 层外圈 (覆盖所有 16 个元素),无需单独处理中心点。
因此,总的循环次数(即圈数)为 n / 2
的向下取整 (即 n // 2
)。在代码中,loop
变量表示当前填充的圈数,循环条件 loop <= n / 2
确保所有圈都被正确填充
2. 数学推导:为什么是 n / 2
- 偶数情况 :若
n = 4
,则总共有4 / 2 = 2
圈。每圈填充后,矩阵的边界会向内收缩 1 层(通过offset
控制),最终刚好填满整个矩阵。
通过 loop <= n / 2
,可以统一处理奇偶情况:
- 当
n
为偶数时,循环结束后矩阵完全填充。
3. 代码中的实现细节
offset
的作用 :每完成一圈,offset
增加 1,用于缩小下一圈的填充范围。例如:- 第一圈的上边填充范围是
[startY, n - offset)
,offset
初始为 1,因此范围是[0, n - 1)
。 - 第二圈的上边范围变为
[1, n - 2)
,以此类推。
- 第一圈的上边填充范围是
4. 示例验证
示例 1:n = 3
(奇数)
- 循环条件
loop <= 1
(n / 2 = 1.5
,向下取整为1
)。 - 循环执行 1 次,填充外圈(8 个元素)。
- 最后通过
if (n % 2 == 1)
填充中心点5
。
示例 2:n = 4
(偶数)
- 循环条件
loop <= 2
(n / 2 = 2
)。 - 循环执行 2 次,填充两圈,完全覆盖所有元素,无需额外处理。
总结
循环条件 loop <= n / 2
的本质是通过数学规律控制螺旋矩阵的填充层数 :
- 偶数情况 :恰好填满所有层。
- 奇数情况 :填满外层后,剩余中心点单独处理。 这种设计既保证了代码的简洁性,又避免了重复填充或遗漏元素的问题
第三题:58. 区间和(第九期模拟笔试)
解题思路
本题要求对一个整数数组进行多次区间求和查询,每次查询给出左右边界 a
和 b
(满足 b >= a
),计算数组在区间 [a, b]
内所有元素的和。直接暴力计算每次查询的和会导致时间复杂度为 O(n×q)(q
为查询次数),在数据量较大时容易超时。因此,采用 前缀和 (Prefix Sum)算法优化查询效率。
前缀和原理
前缀和数组 prefix
的第 i
项 prefix[i]
表示原数组从第一个元素到第 i
个元素的累加和(索引从0开始)。通过前缀和数组,任意区间 [a, b]
的和可以由 prefix[b] - prefix[a-1]
快速计算得到(若 a == 0
则直接取 prefix[b]
)
算法步骤
-
预处理前缀和数组 :
- 输入数组后,构建前缀和数组
prefix
,其中prefix[i] = prefix[i-1] + array[i]
。 - 时间复杂度为 O(n)。
- 输入数组后,构建前缀和数组
-
处理查询 :
- 对每个查询
(a, b)
,根据公式sum = prefix[b] - (a > 0 ? prefix[a-1] : 0)
计算区间和。 - 每次查询时间复杂度为 O(1),总体复杂度为 O(q)。
- 对每个查询
复杂度分析
- 时间复杂度 :预处理 O(n),查询总时间 O(q),适用于大规模数据。
- 空间复杂度 :需要额外的前缀和数组,占用 O(n) 空间。
代码
import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);// 读取数组长度 nint n = scanner.nextInt();int[] array = new int[n]; // 存储原始数组int[] prefix = new int[n]; // 存储前缀和数组// 读取数组元素for (int i = 0; i < n; i++) {array[i] = scanner.nextInt();}// 构建前缀和数组// 前缀和数组的第一个元素等于原始数组的第一个元素prefix[0] = array[0];// 从第二个元素开始,每个前缀和等于前一个前缀和加上当前元素for (int i = 1; i < n; i++) {prefix[i] = prefix[i - 1] + array[i];}// 处理查询while (scanner.hasNextInt()) {int a = scanner.nextInt(); // 区间左边界int b = scanner.nextInt(); // 区间右边界// 计算区间和int sum;if (a == 0) {sum = prefix[b]; // 当 a=0 时,直接取 prefix[b]} else {sum = prefix[b] - prefix[a - 1]; // 否则用 prefix[b] - prefix[a-1]}System.out.println(sum); // 输出结果}scanner.close(); // 关闭输入流}
}
第四题:44. 开发商购买土地(第五期模拟笔试)
解题思路(基于前缀和思想)
核心思想:利用前缀和预处理,快速枚举所有合法划分方式并计算差值
- 前缀和预处理 :将每行和每列的总和视为一维数组,构建其前缀和序列,从而在划分时快速获取任意前缀区间的总和。
- 横向划分 :将每行的总和构成一维数组
horizontal
,其前缀和prefix_row[i]
表示前i+1
行的总和。在任意划分位置i
(0 ≤ i < n-1
),前缀和prefix_row[i]
对应前i+1
行的总和,剩余部分为sum - prefix_row[i]
,差值为|sum - 2 * prefix_row[i]|
。 - 纵向划分 :同理,将每列的总和构成一维数组
vertical
,其前缀和prefix_col[j]
表示前j+1
列的总和。在任意划分位置j
(0 ≤ j < m-1
),前缀和prefix_col[j]
对应前j+1
列的总和,差值为|sum - 2 * prefix_col[j]|
。
算法步骤
-
读取输入并计算总和 :
- 遍历网格
grid[n][m]
,累加所有元素得到总和sum
。
- 遍历网格
-
构建前缀和序列 :
- 横向前缀和 :通过累加
horizontal[i]
构建前缀和序列。例如,prefix_row[i] = prefix_row[i-1] + horizontal[i]
。
- 横向前缀和 :通过累加
-
枚举划分并计算差值 :
- 横向划分 :遍历
horizontal
数组前n-1
项,累加得到前缀和horizontalCut
,计算差值|sum - 2 * horizontalCut|
。 - 纵向划分 :遍历
vertical
数组前m-1
项,累加得到前缀和verticalCut
,计算差值|sum - 2 * verticalCut|
。
- 横向划分 :遍历
前缀和的作用与优势
数学推导(差值计算)
设某次划分的前缀和为 cut
,则两个子区域的总价值分别为 cut
和 sum - cut
,差值为:
∣cut−(sum−cut)∣=∣sum−2×cut∣
此公式将差值计算简化为单次减法和取绝对值操作,无需显式计算两部分和的差值
代码
import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt(); // 行数int m = scanner.nextInt(); // 列数int sum = 0; // 整个网格的总和int[][] grid = new int[n][m]; // 存储网格数据int[] horizontal = new int[n]; // 每行的总和int[] vertical = new int[m]; // 每列的总和// 读取网格数据并计算总和for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {grid[i][j] = scanner.nextInt();sum += grid[i][j];}}// 计算每行的总和(前缀和预处理)for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {horizontal[i] += grid[i][j]; // horizontal[i] 表示第 i 行的总和}}// 计算每列的总和(前缀和预处理)for (int j = 0; j < m; j++) {for (int i = 0; i < n; i++) {vertical[j] += grid[i][j]; // vertical[j] 表示第 j 列的总和}}int result = Integer.MAX_VALUE; // 初始化最小差值为最大值// 枚举所有合法的横向划分方式(前缀和应用)int horizontalCut = 0; // 隐式前缀和变量,逐步累加每行的和for (int i = 0; i < n - 1; i++) { // 只遍历 n-1 个合法划分位置horizontalCut += horizontal[i]; // 累加前 i+1 行的总和(前缀和)int diff = Math.abs(sum - 2 * horizontalCut); // 计算差值result = Math.min(result, diff); // 更新最小差值}// 枚举所有合法的纵向划分方式(前缀和应用)int verticalCut = 0; // 隐式前缀和变量,逐步累加每列的和for (int j = 0; j < m - 1; j++) { // 只遍历 m-1 个合法划分位置verticalCut += vertical[j]; // 累加前 j+1 列的总和(前缀和)int diff = Math.abs(sum - 2 * verticalCut); // 计算差值result = Math.min(result, diff); // 更新最小差值}System.out.println(result); // 输出最小差值scanner.close();}
}