🔥个人主页: 中草药
🔥专栏:【算法工作坊】算法实战揭秘
一.连续数组
题目链接:525.连续数组
代码
public int findMaxLength(int[] nums) {HashMap<Integer,Integer> map=new HashMap<>();map.put(0,-1);int sum=0,ret=0;for(int i=0;i<nums.length;i++){sum+=(nums[i]==0?-1:1);if(map.containsKey(sum)){ret=Math.max(ret,i-map.get(sum));}else{map.put(sum,i);}}return ret;}
算法原理
这段代码实现了一个寻找数组中具有相等数量的0和1的最长子数组长度的算法。具体来说,它使用了前缀和(prefix sum)的概念以及哈希映射(HashMap)来优化查找过程。
-
初始化哈希映射:
- 创建一个
HashMap
,用来存储前缀和与对应的数组索引。 - 初始时,将前缀和为0的情况标记在索引-1处,这是为了处理从数组开始位置就有相等数量的0和1的情况。
- 创建一个
-
计算前缀和:
- 遍历数组中的每一个元素
nums[i]
。 - 如果
nums[i]
是0,则将其视为-1,如果是1,则保持为1。这样,我们可以用加法来累积0和1的差值。 sum
变量用于跟踪当前遍历到的位置为止的累积差值。
- 遍历数组中的每一个元素
-
检查相等数量的0和1:
- 每次更新
sum
后,我们检查sum
是否已经存在于HashMap
中。 - 如果存在,这意味着从前一次出现相同
sum
的点到当前点之间,0和1的数量是相等的(因为累积的差值没有改变)。 - 我们可以通过当前索引
i
减去先前sum
出现的索引来计算这个子数组的长度,并更新最长子数组的记录。
- 每次更新
-
更新哈希映射:
- 如果
sum
不在HashMap
中,说明我们首次遇到这个累积差值。我们将sum
及其对应的索引i
添加到HashMap
中。
- 如果
-
返回结果:
- 在遍历完整个数组后,返回最长子数组的长度。
这种方法的时间复杂度为O(n),因为我们只遍历数组一次,并且对每个元素执行常数时间的操作。空间复杂度也是O(n),最坏情况下需要存储n个前缀和值。
举例
测试用例 nums = [0,1,0]
-
初始化阶段:
- 哈希映射
map
被创建并初始化,其中map.put(0, -1)
,表示前缀和为0的初始位置在-1,这是为了处理从数组开始就有相等数量的0和1的情况。
- 哈希映射
-
开始遍历数组:
- 对于
nums[0]
即0
,由于nums[i] == 0
,因此sum
增加-1
,此时sum = -1
。map
中不存在键-1
,所以将sum = -1
和其对应的索引i = 0
放入map
中。
map
现在包含{0: -1, -1: 0}
。
- 对于
-
继续遍历:
- 对于
nums[1]
即1
,sum
增加1
(因为nums[i] == 1
),此时sum = 0
。map
中存在键0
,这表明从map.get(0) = -1
(即数组的起始点之前)到当前索引i = 1
之间的子数组[0, 1]
有相等数量的0和1。- 计算子数组长度
i - map.get(sum) = 1 - (-1) = 2
,更新ret
为最大值,即ret = 2
。
- 此时
map
包含{0: -1, -1: 0, 0: 1}
,但是我们不需要保留旧的0
键值对,通常这个实现中不会更新键值对,而是会在下一次遇到相同的sum
时计算新的子数组长度。
- 对于
-
最后一个元素:
- 对于
nums[2]
即0
,再次sum
增加-1
,此时sum = -1
。map
中存在键-1
,这表明从map.get(-1) = 0
到当前索引i = 2
之间的子数组[1, 0]
有相等数量的0和1。- 计算子数组长度
i - map.get(sum) = 2 - 0 = 2
,但ret
已经是2,所以ret
保持不变。
- 对于
-
返回结果:
- 遍历结束后,
ret
的值为2,表示最长子数组的长度是2。
- 遍历结束后,
因此,对于nums = [0, 1, 0]
,这段代码会正确地返回最长子数组长度为2,即子数组[0, 1]
或[1, 0]
。值得注意的是,尽管最后的结果是正确的,但根据题目要求和具体实现细节,可能有多个满足条件的子数组,而这段代码仅返回长度信息,不提供具体的子数组元素。
二.和为k的子数组
题目链接:560.和为k的子数组
代码
public int subarraySum(int[] nums, int k) {HashMap<Integer,Integer> map=new HashMap<>();map.put(0,1);int ret=0,sum=0;for(int i=0;i<nums.length;i++){sum+=nums[i];ret+=map.getOrDefault(sum-k,0);map.put(sum,map.getOrDefault(sum,0)+1);}return ret;}
算法原理
这段代码实现了一个算法,用于找到数组nums
中所有和等于给定值k
的连续子数组的个数。该算法的核心思想同样基于前缀和和哈希映射,但这里的目标是在遍历过程中查找是否有任何前缀和能够与当前前缀和形成差值为k
的配对。
以下是算法的详细步骤:
-
初始化哈希映射:
- 创建一个
HashMap
,用来存储前缀和与对应的出现次数。 - 初始时,将前缀和为0的情况标记出现次数为1。这是为了处理从数组开始位置就有和等于
k
的子数组的情况。
- 创建一个
-
初始化变量:
ret
用于累计满足条件的子数组的个数。sum
用于跟踪当前遍历到的位置为止的累积和。
-
遍历数组:
- 遍历数组中的每一个元素
nums[i]
。 - 更新
sum
,使其反映当前位置为止的累积和。
- 遍历数组中的每一个元素
-
查找匹配的前缀和:
- 使用
getOrDefault
方法查询map
中是否存在sum - k
的前缀和。 - 如果存在,意味着从某个位置到当前位置的累积和正好为
k
。getOrDefault
返回的值是这个前缀和出现的次数,将其累加到ret
中,代表找到了这么多满足条件的子数组。
- 使用
-
更新哈希映射:
- 将当前的
sum
及其对应的出现次数更新到map
中。如果sum
已经在map
中,那么它的计数器加1;否则,设置为1。
- 将当前的
-
返回结果:
- 在遍历完整个数组后,
ret
包含了所有满足条件的子数组的个数,直接返回即可。
- 在遍历完整个数组后,
这种方法的时间复杂度同样是O(n),因为我们只遍历数组一次,并且每次操作都是常数时间的。空间复杂度为O(n),因为在最坏的情况下,哈希映射可能需要存储n个不同的前缀和。这种算法利用了哈希映射快速查找和插入的特点,从而实现了高效的解决方案。
举例
测试用例 nums = [1,2,3], k = 3
-
初始化阶段:
- 创建哈希映射
map
并初始化,其中map.put(0, 1)
,表示前缀和为0的出现次数为1。
- 创建哈希映射
-
遍历数组:
-
对于
nums[0]
即1
,- 当前累积和
sum
更新为1
。 - 查询
map
中是否存在sum - k = 1 - 3 = -2
,显然不存在,因此ret
不改变。 - 更新
map
,map.put(sum, map.getOrDefault(sum, 0) + 1)
,即map.put(1, 1)
,因为1
的前缀和第一次出现。
- 当前累积和
-
对于
nums[1]
即2
,- 当前累积和
sum
更新为3
。 - 查询
map
中是否存在sum - k = 3 - 3 = 0
,存在并且出现次数为1,因此ret
增加1,现在ret = 1
。 - 更新
map
,map.put(3, map.getOrDefault(3, 0) + 1)
,即map.put(3, 1)
,因为3
的前缀和第一次出现。
- 当前累积和
-
对于
nums[2]
即3
,- 当前累积和
sum
更新为6
。 - 查询
map
中是否存在sum - k = 6 - 3 = 3
,存在并且出现次数为1,因此ret
再增加1,现在ret = 2
。 - 更新
map
,map.put(6, map.getOrDefault(6, 0) + 1)
,即map.put(6, 1)
,因为6
的前缀和第一次出现。
- 当前累积和
-
-
返回结果:
- 遍历结束后,
ret
的值为2,表示有2个连续子数组的和等于k
。
- 遍历结束后,
通过上面的分析,我们发现确实有两个子数组的和等于k = 3
,分别是[1, 2]
和[3]
。因此,这段代码对于测试用例nums = [1, 2, 3]
和k = 3
,会正确返回2作为结果。
三.和可被k整除的子数组
题目链接:974.和可被k整除的子数组
代码
public int subarraysDivByK(int[] nums, int k) {HashMap<Integer,Integer> map=new HashMap<>();map.put(0,1);int ret=0,sum=0;for(int i=0;i<nums.length;i++){sum+=nums[i];int r=(sum%k+k)%k;//对负数的处理//根据,同余定理,(sum-x)%k==0,sum%k==0,x%k==0ret+=map.getOrDefault(r,0);map.put(r,map.getOrDefault(r,0)+1);}return ret;}
算法原理
实现了一个算法,用于寻找数组nums
中所有和能被整数k
整除的连续子数组的个数。这里的算法原理与之前的略有不同,主要是针对模运算的特性进行优化,特别是处理负数模运算的情况。
算法的关键点在于使用模运算的性质来简化问题,并利用哈希映射来存储和查找特定的模结果。以下是算法的详细步骤:
-
初始化哈希映射:
- 创建一个
HashMap
,用来存储模结果与对应的出现次数。 - 初始时,将模结果为0的出现次数标记为1。这是因为如果累积和本身就能被
k
整除,那么从数组的起始位置到当前位置的子数组就满足条件。
- 创建一个
-
遍历数组:
- 遍历数组中的每一个元素
nums[i]
。 - 更新
sum
,使其反映当前位置为止的累积和。
- 遍历数组中的每一个元素
-
处理模运算:
- 为了确保模运算结果始终为正,使用
(sum % k + k) % k
的技巧。这是因为sum % k
可能得到一个负数(当sum % k < 0
时),而我们需要确保r
始终在0
到k-1
之间。这个技巧保证了即使sum % k
是负数,r
也能正确地映射到0
到k-1
的范围内。
- 为了确保模运算结果始终为正,使用
-
查找匹配的模结果:
- 使用
getOrDefault
方法查询map
中是否存在r
的模结果。 - 如果存在,意味着从某个位置到当前位置的累积和模
k
的结果为r
,且已有其他子数组也得到同样的模结果。getOrDefault
返回的值是这个模结果出现的次数,将其累加到ret
中,代表找到了这么多满足条件的子数组。
- 使用
-
更新哈希映射:
- 将当前的模结果
r
及其对应的出现次数更新到map
中。如果r
已经在map
中,那么它的计数器加1;否则,设置为1。
- 将当前的模结果
-
返回结果:
- 在遍历完整个数组后,
ret
包含了所有满足条件的子数组的个数,直接返回即可。
- 在遍历完整个数组后,
这种方法的时间复杂度仍然是O(n),因为我们只遍历数组一次,并且每次操作都是常数时间的。空间复杂度为O(k),因为在最坏的情况下,哈希映射可能需要存储k
个不同的模结果。通过这种方式,算法有效地利用了模运算的性质和哈希映射的快速查找能力,实现了高效解决方案。
举例
测试用例 nums = [4,5,0,-2,-3,1], k = 5
-
初始化阶段:
- 创建哈希映射
map
并初始化,其中map.put(0, 1)
,表示模结果为0的出现次数为1。
- 创建哈希映射
-
遍历数组:
-
对于
nums[0]
即4
,- 当前累积和
sum
更新为4
。 - 计算模结果
r = (sum % k + k) % k = (4 % 5 + 5) % 5 = 4
。 - 查询
map
中是否存在r = 4
,显然不存在,因此ret
不改变。 - 更新
map
,map.put(4, map.getOrDefault(4, 0) + 1)
,即map.put(4, 1)
,因为4
的模结果第一次出现。
- 当前累积和
-
对于
nums[1]
即5
,- 当前累积和
sum
更新为9
。 - 计算模结果
r = (9 % 5 + 5) % 5 = 4
。 - 查询
map
中是否存在r = 4
,存在并且出现次数为1,因此ret
增加1,现在ret = 1
。 - 更新
map
,map.put(4, map.getOrDefault(4, 0) + 1)
,即map.put(4, 2)
,因为4
的模结果第二次出现。
- 当前累积和
-
对于
nums[2]
即0
,- 当前累积和
sum
更新为9
。 - 计算模结果
r = (9 % 5 + 5) % 5 = 4
。 - 查询
map
中是否存在r = 4
,存在并且出现次数为2,因此ret
再增加2,现在ret = 3
。 - 更新
map
,map.put(4, map.getOrDefault(4, 0) + 1)
,即map.put(4, 3)
,因为4
的模结果第三次出现。
- 当前累积和
-
对于
nums[3]
即-2
,- 当前累积和
sum
更新为7
。 - 计算模结果
r = (7 % 5 + 5) % 5 = 2
。 - 查询
map
中是否存在r = 2
,显然不存在,因此ret
不改变。 - 更新
map
,map.put(2, map.getOrDefault(2, 0) + 1)
,即map.put(2, 1)
,因为2
的模结果第一次出现。
- 当前累积和
-
对于
nums[4]
即-3
,- 当前累积和
sum
更新为4
。 - 计算模结果
r = (4 % 5 + 5) % 5 = 4
。 - 查询
map
中是否存在r = 4
,存在并且出现次数为3,因此ret
再增加3,现在ret = 6
。 - 更新
map
,map.put(4, map.getOrDefault(4, 0) + 1)
,即map.put(4, 4)
,因为4
的模结果第四次出现。
- 当前累积和
-
对于
nums[5]
即1
,- 当前累积和
sum
更新为5
。 - 计算模结果
r = (5 % 5 + 5) % 5 = 0
。 - 查询
map
中是否存在r = 0
,存在并且出现次数为1,因此ret
再增加1,现在ret = 7
。 - 更新
map
,map.put(0, map.getOrDefault(0, 0) + 1)
,即map.put(0, 2)
,因为0
的模结果第二次出现。
- 当前累积和
-
-
返回结果:
- 遍历结束后,
ret
的值为7,表示有7个连续子数组的和能被k = 5
整除。
- 遍历结束后,
通过上面的分析,我们可以看到,确实有7个子数组的和能被5
整除,例如[4]
、[4, 5]
、[4, 5, 0]
、[5]
、[5, 0]
、[5, 0, -2, -3]
、和[1]
。因此,这段代码对于测试用例nums = [4, 5, 0, -2, -3, 1]
和k = 5
,会正确返回7作为结果。
四.矩阵区域和
题目链接:1314.矩阵区域和
代码
public int[][] matrixBlockSum(int[][] mat, int k) {int m=mat.length,n=mat[0].length;int[][] dp=new int[m+1][n+1];//预处理前缀和矩阵for(int i=1;i<dp.length;i++){for(int j=1;j<dp[0].length;j++){dp[i][j]=dp[i-1][j]+dp[i][j-1]+mat[i-1][j-1]-dp[i-1][j-1];//由于边界情况的处理,故加上的mat[i][j]变为mat[i-1][j-1]}}//求解int[][] ret=new int[m][n];for(int i=0;i<ret.length;i++){for(int j=0;j<ret[0].length;j++){int x1=Math.max(0,i-k)+1;int y1=Math.max(0,j-k)+1;int x2=Math.min(m-1,i+k)+1;int y2=Math.min(n-1,j+k)+1;ret[i][j]=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1];}}return ret;}
算法原理
这段代码实现了一个算法,用于计算二维矩阵mat
中每个元素为中心、边长为2*k+1
的正方形区域内的元素之和,并将这些和组成一个新的矩阵ret
返回。这里采用的是前缀和矩阵的方法,它是一种高效的空间换时间的策略,特别适用于解决二维范围查询问题。
算法的原理可以分为以下几步:
-
构建前缀和矩阵:
- 首先创建一个比原矩阵
mat
大一圈的二维数组dp
,用于存储前缀和。额外的一圈是为了简化边界条件的处理。 - 通过两重循环遍历
dp
矩阵的内部(从dp[1][1]
开始),计算前缀和。 - 前缀和的计算公式为:
dp[i][j] = dp[i-1][j] + dp[i][j-1] + mat[i-1][j-1] - dp[i-1][j-1]
。 - 这个公式实际上计算了以
(i-1, j-1)
为右下角的矩形区域内的元素之和。注意,由于dp
矩阵的大小比mat
大一圈,所以在公式中使用mat[i-1][j-1]
。
- 首先创建一个比原矩阵
-
计算查询结果矩阵:
- 创建一个与
mat
大小相同的矩阵ret
,用于存储最终结果。 - 再次通过两重循环遍历
mat
矩阵,对于每个位置(i, j)
:- 确定查询的四个边界:左上角
(x1, y1)
和右下角(x2, y2)
,考虑到边界条件,确保它们都在矩阵的有效范围内。 - 使用前缀和矩阵
dp
计算以(i, j)
为中心的2*k+1
大小的正方形区域内元素之和。公式为:ret[i][j] = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]
。 - 这个公式实际上是利用前缀和矩阵的性质,从大矩形中减去三个不需要的小矩形区域,得到目标区域的元素之和。
- 确定查询的四个边界:左上角
- 创建一个与
-
返回结果矩阵:
- 完成所有位置的计算后,返回结果矩阵
ret
。
- 完成所有位置的计算后,返回结果矩阵
这种方法的时间复杂度主要由两次遍历构成,分别是构建前缀和矩阵和计算查询结果,均为O(mn),其中m和n分别是矩阵的行数和列数。空间复杂度为O(mn),用于存储前缀和矩阵和结果矩阵。通过前缀和矩阵的使用,避免了对每个查询重复计算区域和,从而大大提高了效率。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸