Bootstrap

LeetCode热题100JS(11/100)第二天|42. 接雨水|3. 无重复字符的最长子串|438. 找到字符串中所有字母异位词|560. 和为 K 的子数组|239. 滑动窗口最大值

42. 接雨水

题目链接:42. 接雨水

难度:困难

刷题状态:2刷

新知识:

- `nums.unshift(a)`  在数组前面加元素

解题过程

思考

示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

从左往右,从右往左,我自己敲出来了,但是还有可以改进的地方

题解分析

参考题解链接:接雨水

1刷的过程

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function (height) {
  //这种方法的核心是从两边往中间逼近
  const n = height.length;
  let res = 0;
  let leftHeight = 0;
  let rightHeight = 0;
  let left = 0;
  let right = n - 1;
  while (left < right) {
    leftHeight = Math.max(leftHeight, height[left]);
    rightHeight = Math.max(rightHeight, height[right]);
    if (leftHeight < rightHeight) {
      //left右移
      res += leftHeight - height[left];
      left++;
    } else {
      //right左移
      res += rightHeight - height[right];
      right--;
    }
  }
  return res;
};

这是2刷我自己敲的

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function(height) {
    let n=height.length,res=0
    let left=0,right=n-1
    let lh=height[0],rh=height[n-1],tag=lh<rh?'l':'r'
    while(left<right){
        lh=Math.max(lh,height[left])
        rh=Math.max(rh,height[right])
        if(tag=='l'){
            x=Math.min(lh,rh)-height[left]
        }else{
            x=Math.min(lh,rh)-height[right]
        }
        x=x<0?0:x
        res+=x
        if(height[left]<height[right]){
            left++
            tag='l'
        }else{
            right--
            tag='r'
        }
    }
    return res
};

可以看出还有改进的地方,主要是res的计算可以放在判断height[left]<height[right] 的大小之后

而且判断条件改成了lh<rh,应该是不断往中间逼近左右两边的最大值

再手搓一遍

手搓答案(无非废话版)

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function(height) {
    let res=0,n=height.length
    let lh=0,rh=0,left=0,right=n-1
    while(left<right){
        lh=Math.max(lh,height[left])
        rh=Math.max(rh,height[right])
        if(lh<rh){
            res+=lh-height[left]
            left++
        }else{
            res+=rh-height[right]
            right--
        }
    }
    return res
};

总结

 典型多刷!

3. 无重复字符的最长子串

题目链接:3. 无重复字符的最长子串

难度:中等

刷题状态:2刷

新知识:

- `nums.shift(a)`  在数组前面加元素

解题过程

思考

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

滑动窗口题目,注意 s 由英文字母、数字、符号和空格组成

手搓出来了

题解分析

参考题解链接:无重复字符的最长子串

这里用的是map方法,还可以试试用set,还要把计算长度的逻辑放在处理完重复情况之后

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    let res=0,str=[],n=s.length
    let left=0
    let count=new Map()
    for(let right=0;right<n;right++){
        let x=s[right]
        res=Math.max(res,str.length)
        str.push(x)
        if(!count.get(x)){
            count.set(x,0)
        }
        count.set(x,count.get(x)+1)
        while(count.get(x)==2){
            let y=s[left]
            count.set(y,count.get(y)-1)
            str.shift(y)
            left++
        }
    }
    res=Math.max(res,str.length)
    return res
};

手搓答案(无非废话版)

这里用的是set方法

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    let res=0,n=s.length
    let left=0,right=0
    let set=new Set()
    while(right<n){
        while(set.has(s[right])){
            set.delete(s[left])
            left++
        }
        set.add(s[right])
        res=+Math.max(res,right-left+1)
        right++
    }
    return res
};

总结

 pass

438. 找到字符串中所有字母异位词

题目链接:438. 找到字符串中所有字母异位词

难度:中等

刷题状态:1刷

新知识:

- `set.size` set的长度

解题过程

思考

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

这道题要处理两个问题,遇到重复的,和遇到不在p里面的字符

但是p可能包含重复元素,如果是有重复元素,set就不行了,只能考虑map

等等等等,要找的是异位词,只用确保字母和对应的个数是一样的就行了

手搓出来了!!,看看还能怎么改进

题解分析

参考题解链接:找到字符串中所有字母异位词

手搓答案(无非废话版)

两种方法

var findAnagrams = function(s, p) {
    let res=[],n=s.length
    let left=0,right=0
    let set=new Set(p)
    let map=new Map()
    for(let m of p){
        map.set(m,(map.get(m)||0)+1)
    }
    while(right<n){
        //如果p中没有
        if(!set.has(s[right])){
            right++
            left=right
            let mapp=new Map()
            for(let m of p){
                mapp.set(m,(mapp.get(m)||0)+1)
            }
            map=mapp
            continue
        }
        //如果加入这个字符会超出p中个数的限制
        while(map.get(s[right])==0){
            map.set(s[left],map.get(s[left])+1)
            left++
        }
        map.set(s[right],map.get(s[right])-1)
        if(isOk(map)){
            res.push(left)
        } 
        right++
    }
    return res
};
function isOk(map){
    for(let [key,val] of map){
        if(val) return false
    }
    return true
}
var findAnagrams = function(s, p) {
    let res=[],n=s.length
    let left=0,right=0
    let map=new Map()
    let mapp=new Map()
    for(let m of p){
        mapp.set(m,(mapp.get(m)||0)+1)
    }
    while(right<n){
        if(!mapp.get(s[right])){
            right++
            left=right
            map=new Map()
            continue
        }
        while((map.get(s[right])||0)==mapp.get(s[right])){
            map.set(s[left],map.get(s[left])-1)
            left++
        }
        map.set(s[right],(map.get(s[right])||0)+1)
        if(isOk(map,mapp)){
            res.push(left)
        } 
        right++
    }
    return res
}
function isOk(map,mapp){
    if(map.size!=mapp.size) return false
    for(let [key,val] of map){
        if(val!=mapp.get(key)) return false
    }
    return true
}

总结

 真是酣畅淋漓的一场战斗啊!不过好像还有更快的做法,是用到了

s 和 p 仅包含小写字母  的条件,

先把长度为26的数组建立好

560. 和为 K 的子数组

题目链接:​​​​​​​560. 和为 K 的子数组

难度:中等

刷题状态:1刷

新知识:

解题过程

思考

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

类型是子串,有很多种方法可以做出来,包括dfs递归,不过这题应该没那么麻烦

用滑动窗口做试试

对于每个新增的right,都要left从头筛选一遍

做出来了,但速度慢,复杂度O(n2)

var subarraySum = function(nums, k) {
    let res=0,n=nums.length
    let sum=0
    let left=0,right=0
    while(right<n){
        left=right
        sum=0
        while(left>=0){
            sum+=nums[left]
            if(sum==k) res++
            left--
        }
        right++
    }
    return res
};
题解分析

参考题解链接:​​​​​​​和为K的子数组

详细分析如下,复杂度O(n)

var subarraySum = function(nums, k) {
    //用来存target出现的次数
    //初始化为了防止出现
    //当遍历到某个位置时,如果当前的前缀和 sum 恰好等于 k(即找到了一个从数组开始到当前位置的子数组,其和为 k),那么我们需要一个初始的0前缀和来匹配这个情况。因为 sum - k = sum - sum = 0,所以我们需要 sum 中有一个键为0的条目来记录这种情况。
    let map=new Map([[0,1]])//Map(1) { 0 => 1 }
    let res=0
    //用于计算当前遍历到某个位置时的前缀和。
    let sum=0
    for(let i=0;i<nums.length;i++){
        sum+=nums[i]
        // 目标前缀和
        let target=0-(k-sum)//这里target是负数
        //如果map中有target,说明存在一个或多个之前的子数组 它们的和(target)
        //其和加上当前遍历到 i 的子数组的和(sum)等于 k
        res+=map.has(target)?map.get(target):0
        //更新map中当前前缀和 sum 的次数
        map.set(sum,(map.get(sum)||0)+1)
        // console.log(res,map)
    }
    return res
}

手搓答案(无非废话版)

var subarraySum = function(nums, k) {
    let res=0,n=nums.length
    let sum=0
    let map=new Map([[0,1]])
    for(let i=0;i<n;i++){
        sum+=nums[i]
        let target=0-(k-sum)
        res+=map.has(target)?map.get(target):0
        map.set(sum,(map.get(sum)||0)+1)
    }
    return res
}

总结

 注意取值取的target,存值存的sum

239. 滑动窗口最大值

题目链接:​​​​​​​239. 滑动窗口最大值

难度:困难

刷题状态:1刷

新知识:

解题过程

思考

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

这题的难点在于怎么简化

下面这样可行,但会超时

var maxSlidingWindow = function(nums, k) {
    let res=[]
    let right=k
    while(right<=nums.length){
        let a=nums.slice(right-k,right)
        res.push(Math.max(...a))
        right++
    }
    return res
};

能不能想办法记录一下每段的最大值及其索引

看一下下一个left是否越过了这个最大值,以及right是否更新了最大值呢?

想不出来了,明天再看吧

题解分析

参考题解链接:预处理+递归(Python/Java/C++/Go/JS/Rust)

下面是详细分析

有点难理解,不过确实是最简单的方法,最主要要理解q是存的索引,而且是当前窗口的索引,而且只从当前窗口最大值开始存

注意几个细节 q.length&&nums[i]>=nums[q[q.length-1]]这里是>=

while(q[0]<=i-k){ 这里是while,if也可以

var maxSlidingWindow = function(nums, k) {
    const n = nums.length;
    //这个数组将用作双端队列(deque),用来存储当前窗口内元素的索引,这些元素按值从大到小排序。
    //q[0]始终是窗口内的最大值
    const q = [];
    for (let i = 0; i < k; i++) {
        while (q.length && nums[i] >= nums[q[q.length - 1]]) {
            q.pop();
        }
        q.push(i);
    }
    
    //用于存储每个窗口的最大值
    const ans = [nums[q[0]]];
    for (let i = k; i < n; i++) {
        while (q.length && nums[i] >= nums[q[q.length - 1]]) {
            q.pop();
        }
        q.push(i);
        // console.log(q)
        //左边移出去了
        while (q[0] <= i - k) {
            q.shift();
        }
        ans.push(nums[q[0]]);
    }
    return ans;
};

手搓答案(无非废话版)

var maxSlidingWindow = function(nums, k) {
    let res=[],n=nums.length
    let q=[]
    for(let i=0;i<k;i++){
        while(q.length&&nums[i]>=nums[q[q.length-1]]){
            q.pop()
        }
        q.push(i)
    }
    res.push(nums[q[0]])
    for(let i=k;i<n;i++){
        while(q.length&&nums[i]>=nums[q[q.length-1]]){
            q.pop()
        }
        q.push(i)
        while(q[0]<=i-k){
            q.shift()
        }
        res.push(nums[q[0]])
    }
    return res
}

总结

 这题的类型是子串,要注意第一个循环是为了给q赋初值,不会出现left离开窗口的情况

;