总结
前缀和技巧
【参考:前缀和技巧:解决子数组问题__微信公众号】
前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
题目:560. 和为 K 的子数组
【参考:小而美的算法技巧:前缀和数组 :: labuladong的算法小抄】
简单
27. 移除元素
双指针法(快慢指针法)
- 拷贝覆盖
元素的顺序可以改变
class Solution {
public int removeElement(int[] nums, int val) {
int n = nums.length;
int left = 0;
for (int right = 0; right < n; right++) {
if (nums[right] != val) {
nums[left] = nums[right]; // 拷贝覆盖
left++;
}
}
return left; // 返回移除后数组的新长度
}
}
// 双指针优化(尽可能少拷贝覆盖)
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0;
int right = nums.length;
while (left < right) {
if (nums[left] == val) {
// 元素的顺序可以改变 所以可以随意点
nums[left] = nums[right - 1]; // 交换移除
right--;
} else {
left++;
}
}
return left;
}
}
26. 删除有序数组中的重复项
【参考:26. 删除有序数组中的重复项 - 力扣(LeetCode)】
【参考:【双指针】删除重复项-带优化思路 - 删除有序数组中的重复项 - 力扣(LeetCode)】
元素的顺序不可以改变
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums)<=1:
return len(nums)
slow=0
fast=0
while fast<len(nums):
if nums[slow]!=nums[fast]:
slow+=1 # 下一个数字的下标,因为这个数字本身就需要留一个
nums[slow]=nums[fast]
fast+=1
return slow+1
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums)<=1:
return len(nums)
slow=0
fast=0
while fast<len(nums):
if nums[slow]!=nums[fast]:
if fast-slow>=2: # 有相同的元素nums[slow] nums[slow+1]
nums[slow+1]=nums[fast] # 拷贝覆盖
slow+=1
fast+=1
return slow+1
80. 删除有序数组中的重复项 II ??
【参考:80. 删除有序数组中的重复项 II - 力扣(LeetCode)】
【参考:[一招秒杀所有同类型题] 非常容易理解的双指针 - 删除有序数组中的重复项 II - 力扣(LeetCode)】
有点没有理解透彻的感觉 待定
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums)<=1:
return len(nums)
left=1 # 从第二个元素开始
for right in range(2,len(nums)):
# nums[right] 与前两个数相比
# 如果全部相等, 说明right指向的元素是重复元素,不保留
if nums[right]==nums[left] and nums[right]==nums[left-1]:
continue
# 此时right指向的是非重复元素
left+=1
nums[left]=nums[right]
return left+1
中等
209. 长度最小的子数组
参考:209. 长度最小的子数组 - 力扣(LeetCode)
滑动窗口
class Solution {
public int minSubArrayLen(int target, int[] nums) {
if(nums.length==0||nums==null)
return 0;
int i=0; // 滑动窗口的起始位置i,结束位置j
int result=nums.length+1; // 返回的结果 初始为一个比nums.length大的值即可
int total=0; // 滑动窗口的数值之和
for(int j=0;j<nums.length;j++){
total+=nums[j];
while(total>=target){
int subLength=j-i+1; // 滑动窗口的长度
result=Math.min(result,subLength); // 取最小值
// total去除nums[i],滑动窗口从nums[i+1]开始
total-=nums[i];
i++;
}
}
if(result==nums.length+1)
return 0;
else
return result; // 返回其长度
}
}
59. 螺旋矩阵 II
参考:Spiral Matrix II (模拟法,设定边界,代码简短清晰) - 螺旋矩阵 II - 力扣(LeetCode)
class Solution {
public int[][] generateMatrix(int n) {
int l = 0, r = n - 1, t = 0, b = n - 1;
int[][] result = new int[n][n];
int num = 1, tar = n * n;
while(num <= tar){
for(int i = l; i <= r; i++) result[t][i] = num++; // left to right.
t++;
for(int i = t; i <= b; i++) result[i][r] = num++; // top to bottom.
r--;
for(int i = r; i >= l; i--) result[b][i] = num++; // right to left.
b--;
for(int i = b; i >= t; i--) result[i][l] = num++; // bottom to top.
l++;
}
return result;
}
}
560. 和为 K 的子数组 ***
【参考:560. 和为 K 的子数组 - 力扣(LeetCode)】
int n = nums.length;
// 前缀和数组
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
preSum[i + 1] = preSum[i] + nums[i];
preSum[i+1] 就是 nums[0..i] 的和,
如果想求 nums[i..j] 的和,只需要 preSum[j+1] - preSum[i] 即可
比如 nums[3,4,5] =-2+4+1=3
preSum[5+1]-preSum[3]=13-10=3
- 解法一
无法通过
class Solution {
public int subarraySum(int[] nums, int k) {
int n=nums.length;
int[] preSum=new int[n+1];
preSum[0]=0;
for(int i=0;i<n;i++){
preSum[i+1]=preSum[i]+nums[i];
}
int result=0;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(preSum[j+1]-preSum[i]==k){
result++;
}
}
}
return result;
}
}
- 方法二:前缀和 + 哈希表优化
动画:【参考:和为K的子数组 - 和为 K 的子数组 - 力扣(LeetCode)】
代码:【参考:小而美的算法技巧:前缀和数组 :: labuladong的算法小抄】
分清 sum0_i , sum0_j的范围
class Solution {
public int subarraySum(int[] nums, int k) {
int n = nums.length;
// map:前缀和,该前缀和出现的次数
HashMap<Integer, Integer> preSum = new HashMap<>();
// base case
preSum.put(0, 1);
int res = 0, sum0_i = 0;
for (int i = 0; i < n; i++) {
sum0_i += nums[i];
// 这是我们想找的前缀和 nums[0..j]
int sum0_j = sum0_i - k;
// 如果前面有这个前缀和,则直接更新答案
if (preSum.containsKey(sum0_j))
res += preSum.get(sum0_j);
// 把前缀和 nums[0..i] 加入并记录出现次数
preSum.put(sum0_i,
preSum.getOrDefault(sum0_i, 0) + 1);
}
return res;
}
}
- 枚举法
【参考:和为K的子数组 - 和为 K 的子数组 - 力扣(LeetCode)】
逐步扩大范围避免重复求和
class Solution {
public int subarraySum(int[] nums, int k) {
int n=nums.length;
int result=0;
for(int end=0;end<n;end++){
int sum=0;
for(int start=end;start>=0;start--){ // [0..start..end]
// sum=num[start..end]; start:0 <- end 子数组范围逐步扩大
// 如果知道 [start,end]子数组的和,就能直接推出[start-1,end]的和
sum += nums[start];
if(sum==k){
result++;
}
}
}
return result;
}
}
380. O 时间插入、删除和获取随机元素
【参考:380. O 时间插入、删除和获取随机元素 - 力扣(LeetCode)】
【参考:O 时间插入、删除和获取随机元素 - O(1) 时间插入、删除和获取随机元素 - 力扣(LeetCode)】
【参考:常数时间删除/查找数组中的任意元素 :: labuladong的算法小抄】
class RandomizedSet {
List<Integer> nums;
// 存放nums中的val:index
Map<Integer,Integer> valToIndex;
Random random;
public RandomizedSet() {
nums=new ArrayList<>();
valToIndex=new HashMap<>();
random=new Random();
}
public boolean insert(int val) {
// 已经存在val
if(valToIndex.containsKey(val)) return false;
nums.add(val);
valToIndex.put(val,nums.size()-1);
return true;
}
// 每次删除转化为删除数组最后一个元素(题目没说要保持数组内元素有序)
public boolean remove(int val) {
// 不存在val
if(!valToIndex.containsKey(val)) return false;
// 将last覆盖index处的val值(相当于变相删除了val),
int index=valToIndex.get(val);
int last=nums.get(nums.size()-1);
nums.set(index,last);
// 再删除nums最后一个元素,并更新valToIndex
nums.remove(nums.size()-1);
valToIndex.put(last,index);
valToIndex.remove(val);
return true;
}
public int getRandom() {
int i=random.nextInt(nums.size());
return nums.get(i);
}
}
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet obj = new RandomizedSet();
* boolean param_1 = obj.insert(val);
* boolean param_2 = obj.remove(val);
* int param_3 = obj.getRandom();
*/
困难
710. 黑名单中的随机数
【参考:710. 黑名单中的随机数 - 力扣(LeetCode)】
【参考:详解官方思路,慢慢学会map - 黑名单中的随机数 - 力扣(LeetCode)】
//按官方给的思路,逻辑上构思一个0~n的整型数组,而我们把随机边界bound就设为(n - blacklist.length)
//也就是我们逻辑上认为0~n的所有整数中,刚好最后blacklist.length个数全是黑名单数
//这样我们只需要在0~bound上随机一次,就能得到一个可取随机白名单数
//那实际上不可能刚好黑名单数就全出现在最后
//所以我们要是可以把在前边出现的黑名单数,都一一映射到后边的非黑名单数的话,那就相当于实现了这样的效果了
class Solution {
//随机对象
Random random;
//随机边界,即构思的0~n大整型数组,前边全白名单,后边全黑名单数的分界线
public int bound;
//存放前面出现黑名单,通过键值就能一一映射到后边非黑名单的map集合
public Map<Integer,Integer> blaceToWhite; // num:index
public Solution(int n, int[] blacklist) {
//初始化随机对象
this.random=new Random();
//初始化随机边界
this.bound=n-blacklist.length;
//初始化map
//前面的键作为前面的数本身,若随机到的此数不是黑名单数,直接返回这个数
//后面的值作为他映射到的后边的非黑名单数,若随机到的此数是黑名单数,则返回这个映射值
blaceToWhite = new HashMap<Integer,Integer>();
//那么如何把边界前面出现的黑名单数,一一映射到边界后边的非黑名单数去呢?
//考虑到边界前面有多少个黑名单数,边界后边就会有多少个非黑名单数
//将边界后的黑名单数记录下
Set<Integer> backBlack = new HashSet<Integer>();
for(int x : blacklist){
//比如x拿到50,既表示它数本身是50,但同时也表示对应在0~n的大数组中,它的下标也是50
//故x > bound可以表示x的下标位置在边界后,即将它添加到边界后黑名单集合set中
if(x >= bound){
backBlack.add(x);
}
}
//开始将边界前的黑名单数数映射到边界后的非黑名单数
int w = bound;
for(int x : blacklist){
//同理,它表示边界前的黑名单数
if(x < bound){
//去找边界后的非黑名单数
//从边界数往后依次判断,若此数是边界后黑名单数,那向后继续找
while(backBlack.contains(w)){
w++;
}
//直到找到边界后的一个非黑名单数
//建立映射
blaceToWhite.put(x,w);
//让下一个映射,往后到下一个数
w++;
}
}
}
public int pick() {
//如果边界前随机的就是白名单数,直接返回就完了,否则返回这个边界前黑名单数对应的边界后映射
//map方法:default V getOrDefault(Object key, V defaultValue)
//返回指定键映射到的值,如果此映射不包含该键的映射,则返回 defaultValue
int x = random.nextInt(bound);
return blaceToWhite.getOrDefault(x, x); // 没看懂
}
}