力扣刷题记录-数组相关问题

汇总力扣中数组相关问题,主要集中于可以使用双指针技巧的的题目,以及对二维数组的话还是遍历。

题目目录

    • 在数组中找符合要求的数
      • LeetCode 剑指 Offer 03. 数组中重复的数字
      • LeetCode 41. 缺失的第一个正数
    • 合并数组
      • LeetCode 88. 合并两个有序数组(从后向前插入)
    • 移动数组
      • LeetCode 189. 轮转数组(利用反转的规律)
    • 二维数组的不同遍历
      • LeetCode 59. 螺旋矩阵 II(二维数组花式遍历)
      • LeetCode 54. 螺旋矩阵(同剑指 Offer 29. 顺时针打印矩阵)
      • LeetCode 48. 旋转图像(找规律)
      • LeetCode 240. 搜索二维矩阵 II(重点是从哪开始遍历,以及遍历方向)
      • LeetCode 73. 矩阵置零(“原地”对矩阵进行修改)
    • 1.快慢指针
      • LeetCode 26. 删除有序数组中的重复项
      • LeetCode 83. 删除排序链表中的重复元素(方法同26)
      • LeetCode 27. 移除元素
      • LeetCode 283. 移动零
      • AcWing 799. 最长连续不重复子序列
      • AcWing 2816. 判断子序列
    • 2.左右指针
      • LeetCode 11. 盛最多水的容器
      • LeetCode 977. 有序数组的平方
      • AcWing 800. 数组元素的目标和
      • LeetCode 15. 三数之和(双指针+剪枝+去重)
      • LeetCode 18. 四数之和(双指针将复杂度+去重+剪枝)
    • 3.二分搜索
      • LeetCode 704. 二分查找
      • LeetCode 74. 搜索二维矩阵
      • LeetCode 162. 寻找峰值
      • LeetCode 852. 山脉数组的峰顶索引(同剑指 Offer II 069. ⼭峰数组的顶部)
      • LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置
      • 剑指 Offer 53 - I. 在排序数组中查找数字 I
      • LeetCode 1011. 在 D 天内送达包裹的能力
      • LeetCode 875. 爱吃香蕉的珂珂(同剑指 Offer II 073. 狒狒吃香蕉)
      • LeetCode 35. 搜索插入位置(同剑指 Offer II 068. 查找插入位置)
      • 剑指 Offer 53 - II. 0~n-1中缺失的数字
    • 4.滑动窗口
      • 滑动窗口模板(来自labuladong)
      • 模板题
        • LeetCode 209. 长度最小的子数组
        • LeetCode 76. 最小覆盖子串(同剑指 Offer II 017. 含有所有字符的最短字符串)
        • LeetCode 567. 字符串的排列(同剑指 Offer II 014. 字符串中的变位词)
        • LeetCode 438. 找到字符串中所有字母异位词(同剑指 Offer II 015. 字符串中的所有变位词)
        • LeetCode 3. 无重复字符的最长子串(同剑指 Offer 48. 最长不含重复字符的子字符串 与 剑指 Offer II 016. 不含重复字符的最长子字符串)
        • LeetCode 904. 水果成篮
        • LeetCode 239. 滑动窗口最大值(同剑指 Offer 59 - I. 滑动窗口的最大值)

在数组中找符合要求的数


LeetCode 剑指 Offer 03. 数组中重复的数字

原题链接

2023.06.03 二刷

思路:
1.HashSet,
时间O(n),空间O(n):需要额外空间,但不改变原数组内容

  • 利用HashSet,每次遍历一个数nums[i]时,先判断是否在set中,如果在,就是重复了;
  • 如果不在,就将这个数添加到set中;

代码如下:

// 1.HashSet,时间O(n),空间O(n)
class Solution {
    public int findRepeatNumber(int[] nums) {
        Set<Integer> set=new HashSet<>();
        for(int num:nums){
            // 在集合中说明元素重复
            if(set.contains(num)){
                return num;
            }else{
                set.add(num);
            }
        }
        return -1;
    }
} 

2.数组模拟hash即可,思路一样,时空复杂度同1

代码如下:

// 2.数组模拟hash,时间O(n),空间O(n)
class Solution {
    public int findRepeatNumber(int[] nums) {
        int[] hash=new int[nums.length];
        for(int num:nums){
            hash[num]++;
            if(hash[num]>1)return num;
        }
        return -1;
    }
}

3.原地交换
  在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的【索引】 和 【值】 是 【一对多】 的关系。
  因此,可遍历数组并通过交换操作,使元素的【索引】与【值】一一对应(即 [nums[i]=i) 。

力扣刷题记录-数组相关问题_第1张图片

  从头扫描数组,遇到下标为i的数字nums[i]如果不是i的话,(假设为x),那么就拿与下标为x(nums[i])的数字交换。在交换过程中,如果想要交换的数字中,有重复的数字发生,那么终止返回重复数字。

代码如下:

// 3.原地交换(会改变nums),时间O(n),空间O(1)
class Solution {
    public int findRepeatNumber(int[] nums) {
        for(int i=0;i<nums.length;i++){
            // 如果nums[i]=i,说明nums[i]在自己对应位置上,不需要交换,直接跳过
            // 当nums[i]不在自己位置上的时候,需要将下标为i的nums[i]交换到它下标为nums[i]处
            // 但是下标为nums[i]处交换过来的数字,也可能不应该待在下标i这里,所以需要持续交换,用while进行
            while(nums[i]!=i){
                //需要交换过去的时候,如果发现下标为nums[i]的地方已经有nums[i]了,说明数字重复,返回要交换的nums[i]
                if(nums[nums[i]]==nums[i])return nums[i];
                int tmp=nums[i];
                nums[i]=nums[tmp];
                nums[tmp]=tmp;
            }                   
        }
        return -1;
    }
}


LeetCode 41. 缺失的第一个正数

原题链接

2023.06.03 一刷
评论区有人说是字节三面题

思路来自liweiwei1419的精选题解:

  由于题目要求我们「只能使用常数级别的空间」,而要找的数一定在 [1, N + 1] 左闭右闭(这里 N 是数组的长度)这个区间里。因此,我们可以就把原始的数组当做哈希表来使用。事实上,哈希表其实本身也是一个数组;

  • 我们要找的数就在 [1, N + 1] 里,最后 N + 1 这个元素我们不用找。因为在前面的 N 个元素都找不到的情况下,我们才返回 N + 1;
  • 那么,我们可以采取这样的思路:就把1这个数放到下标为0的位置, 2 这个数放到下标为1的位置,按照这种思路整理一遍数组。
  • 然后我们再遍历一次数组,第 1 个遇到的它的值不等于下标+1的那个数,就是我们要找的缺失的第一个正数。

  这个思想就相当于我们自己编写哈希函数,这个哈希函数的规则特别简单,那就是数值为 nums[i] 的数映射到下标为 nums[i] - 1 的位置。

需要注意:如果 nums[i] 恰好与 nums[nums[i]−1] 相等,那么就会无限交换下去,因此while循环的时候,当nums[nums[i]-1]==nums[i]时,需要跳出循环,因为这时说明nums[i]这个值已经到了它对应的位置上。

举例如下:

力扣刷题记录-数组相关问题_第2张图片

代码如下:

class Solution {
    public int firstMissingPositive(int[] nums) {
        int n=nums.length;
        for(int i=0;i<n;i++){
            // 满足在指定范围内([1,N])、并且没有放在正确的位置上,才交换
            // 例如:数值 3 应该放在索引 2 的位置上
            while(nums[i]>0&&nums[i]<=n&&nums[nums[i]-1]!=nums[i]){
                int pos=nums[i]-1;
                int tmp=nums[pos];
                nums[pos]=nums[i];
                nums[i]=tmp;
            }
        }
        for(int i=0;i<n;i++){
            if(nums[i]!=i+1){
                return i+1;
            }
        }
        // 都在对应位置上,说明[1,N]都在对应位置,缺失的最小正整数就是N+1
        return n+1;
    }
}

时间复杂度:O(N),这里 N 是数组的长度。

空间复杂度:O(1)

  说明:while 循环不会每一次都把数组里面的所有元素都看一遍。如果有一些元素在这一次的循环中被交换到了它们应该在的位置,那么在后续的遍历中,由于它们已经在正确的位置上了,代码再执行到它们的时候,就会被跳过。

  最极端的一种情况是,在第 1 个位置经过这个 while 就把所有的元素都看了一遍,这个所有的元素都被放置在它们应该在的位置,那么 for 循环后面的部分的 while 的循环体都不会被执行。

  平均下来,每个数只需要看一次就可以了,while 循环体被执行很多次的情况不会每次都发生。这样的复杂度分析的方法叫做均摊复杂度分析。

  最后再遍历了一次数组,最坏情况下要把数组里的所有的数都看一遍,因此时间复杂度是 O(N)。


合并数组

LeetCode 88. 合并两个有序数组(从后向前插入)

原题链接

设置双指针在两个数组末尾,再设置一个p指针指向填入位置。

代码如下:

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int p1=m-1,p2=n-1;
        int p=m+n-1;//p为赋值位置
        while(p1>=0&&p2>=0){
            if(nums1[p1]<=nums2[p2]){
                nums1[p--]=nums2[p2--];
            }else{
                nums1[p--]=nums1[p1--];
            }
        }
        while(p2>=0){
            nums1[p--]=nums2[p2--];
        }
    }
}

移动数组

LeetCode 189. 轮转数组(利用反转的规律)

原题链接

2023.06.03 二刷

题解区里看到有人引用国外的一个短小精悍的题解:
示例:
nums = “----->–>”; k =3
result = “–>----->”;

过程
reverse “----->–>” we can get “<–<-----”
reverse “<–” we can get “–><-----”
reverse “<-----” we can get “–>----->”

代码如下:

class Solution {
    public void rotate(int[] nums, int k) {
        int n=nums.length;
        k%=n;//k可能比nums大,但是nums右移n位还是原来的nums
        reverse(nums,0,n-1);//反转区间两端都为闭
        reverse(nums,0,k-1);
        reverse(nums,k,n-1);
    }
    //对数组指定区间进行反转
    public void reverse(int[] nums,int l,int r ){
        while(l<r){
            // 基于异或运算的交换律和结合律,以及a^a=0,a^0=a;
            nums[l]^=nums[r];//nums[l]=nums[l]^nums[r]
            nums[r]^=nums[l];//nums[r]=nums[r]^nums[l]^nums[r]=nums[l]
            nums[l]^=nums[r];//nums[l]=(nums[l]^nums[r])^nums[l]=nums[r]
            l++;
            r--;
        }
    }
}

二维数组的不同遍历

LeetCode 59. 螺旋矩阵 II(二维数组花式遍历)

原题链接

这题需要注意的就是对各种边界的识别,以及遍历二维数组过程的技巧。

代码如下:

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] res=new int[n][n];
        int[] dx={0,1,0,-1};//存储x坐标偏移量
        int[] dy={1,0,-1,0};//存储y坐标偏移量
        //①[0,1]->行数不变,列数+1(向右移动);②[1,0]->行数+1,列数不变(向下的状态);
        //③[0,-1]->行数不变,列数-1(向左移动);④[-1,0]->行数-1,列数不变(向上的动作);
        int d=0;//用于在dx、dy内循环遍历(起始按①②③④顺序循环)
        int x=0,y=0;//记录赋值的坐标
        int tmpX=0,tmpY=0;//暂存坐标
        for(int i=1;i<=n*n;i++){
            //先给当前坐标赋值
            res[x][y]=i;
            //记录下一步的位置(原坐标加偏移)
            tmpX=x+dx[d];
            tmpY=y+dy[d];
            //不能直接就给下一步赋值,要先判断下一步有没有超出边界,或者走到存过值的位置
            //若下一步位置是非法访问
            if(tmpX<0||tmpX>=n||tmpY<0||tmpY>=n||res[tmpX][tmpY]!=0){
                d=(d+1)%4;//按下一个偏移量走
                tmpX=x+dx[d];//更正下一步位置
                tmpY=y+dy[d];
            }
            x=tmpX;//实际走下去
            y=tmpY;
        }
        return res;
    }
}

LeetCode 54. 螺旋矩阵(同剑指 Offer 29. 顺时针打印矩阵)

原题链接

2023.06.05 三刷

这题和59题差不多,只不过从向二维数组中填值,变成了遍历二维数组,向外输出值。

代码如下:

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        int m=matrix.length,n=matrix[0].length;
        List<Integer> res=new ArrayList<>();
        // 用于偏移,改变下标前进的方向
        int[] dx={0,1,0,-1};
        int[] dy={1,0,-1,0};
        // 用于改变下一步dx、dy的下标
        int d=0;
        // 暂存下一步的数组下标,如果非法(超界或已访问过),则改变dx、dy(改变前进方向)
        int tmpX=0,tmpY=0;
        // 实际遍历数组的下标
        int x=0,y=0;

        for(int i=0;i<m*n;i++){
            res.add(matrix[x][y]);
            matrix[x][y]=101;//数据范围在-100~100;设置标志,如果是101就是访问过的
            // 暂存下一步位置,看看是否合法
            tmpX=x+dx[d];
            tmpY=y+dy[d];
            // 如果超界或者已访问过,则改变前进方向
            if(tmpX<0||tmpX>=m||tmpY<0||tmpY>=n||matrix[tmpX][tmpY]==101){
                d=(d+1)%4;
                tmpX=x+dx[d];
                tmpY=y+dy[d];
            }
            // 前进
            x=tmpX;
            y=tmpY;
        }
        return res;
        
    }
}

LeetCode 48. 旋转图像(找规律)

原题链接

2023.06.05 二刷

思路:
找规律:

  • n维矩阵顺时针旋转90°就相当于原矩阵关于主对角线对称互换之后再逐行反转
  • 如果是逆时针旋转90°,相当于原矩阵关于副对角线对称互换之后逐行反转

代码如下:

class Solution {
    public void rotate(int[][] matrix) {
        int n=matrix.length;
        //以主对角线为轴,进行元素互换
        for(int i=0;i<n-1;i++){
            for(int j=i+1;j<n;j++){
                //利用异或进行原地交换
                matrix[i][j]^=matrix[j][i];
                matrix[j][i]^=matrix[i][j];
                matrix[i][j]^=matrix[j][i];
            }
        }

        //再对互换后的矩阵逐行反转
        for(int[] row:matrix){
            int l=0,r=row.length-1;
            while(l<r){
                row[l]^=row[r];
                row[r]^=row[l];
                row[l]^=row[r];
                l++;
                r--;
            }
        }
    }
}

LeetCode 240. 搜索二维矩阵 II(重点是从哪开始遍历,以及遍历方向)

原题链接

2023.06.05 一刷

思路:
1.从右上角到左下角遍历(最优):
特点:每行从左到右升序排列,每列从上到下升序列,这样左上角最小,右下角最大;

  • 想高效在 matrix 中搜索一个元素,肯定需要从某个角开始,比如说从左上角开始,然后每次只能向右或向下移动,不要走回头路。但是从左上角开始,无论向右还是向下走,元素大小都会增加,到底向右还是向下走是不确定的(动态规划可能可以解决)。
  • 从右上角开始的话,规定只能向左或向下移动。当前元素如果小于target,那么当前所在行就无需考虑,因为该元素左边的一定全都小于当前元素,此时应该向下移动,元素增大;
  • 当前元素如果大于target,那么当前所在列就无须考虑,因为该元素下方元素一定大于当前元素,需要向左移动,元素在减小。
  • 这样的话我们就可以根据当前位置的元素和 target 的相对大小来判断应该往哪移动,不断接近从而找到 target 的位置。

时间O(m+n),空间O(1)

代码如下:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m=matrix.length,n=matrix[0].length;
        int row=0,col=n-1;
        while(row<m&&col>=0){
            if(matrix[row][col]==target)return true;
            else if(matrix[row][col]<target)++row;
            else --col;
        }
        return false;
    }
}

LeetCode 73. 矩阵置零(“原地”对矩阵进行修改)

原题链接

2023.06.04 二刷

思路:

用第0行和第0列来作为flag,只要后面遍历二维数组(从第一行第一列开始)的过程中,遇到为0的元素matrix[i][j],就把该元素下标所指的matrix[i][0]与matrix[0][j]置0,表明这一行/列需要全部置0。

后面再分别遍历第0行与第0列,只要碰到0,就把其所指向的列/行置0。

不过需要注意,第0行与第0列如果一开始就有0,需要预先标记出来,等其flag作用发挥完之后,检查其row0与col0标记是否为1,如果为1,说明其一开始就含0,需要把第0行/列全部置0。

时间O(n^2),空间O(1)

代码如下:

class Solution {
    public void setZeroes(int[][] matrix) {
        int m=matrix.length;
        int n=matrix[0].length;

        int col0=0;//标记第0列是否初始含0
        for(int i=0;i<m;i++){
            if(matrix[i][0]==0)col0=1;
        }

        int row0=0;
        for(int j=0;j<n;j++){
            if(matrix[0][j]==0)row0=1;
        }

        // 遍历数组,找0,置第0行/列对应位置为0
        for(int i=1;i<m;i++)
            for(int j=1;j<n;j++){
                if(matrix[i][j]==0){
                    matrix[i][0]=0;
                    matrix[0][j]=0;
                }                
            }
        //根据第0行/列的情况,重新遍历数组,对元素赋0
        for(int i=1;i<m;i++)
            for(int j=1;j<n;j++){
                if(matrix[i][0]==0||matrix[0][j]==0){
                    matrix[i][j]=0;
                }
            }
        // 最后看看第0行/列是否一开始就含有0
        if(col0==1){
            for(int i=0;i<m;i++){
                matrix[i][0]=0;
            }
        }
        
        if(row0==1){
            for(int j=0;j<n;j++){
                matrix[0][j]=0;
            }
        }
        
    }
}


1.快慢指针

数组中常见的快慢指针通常用于数组原地修改。


LeetCode 26. 删除有序数组中的重复项

原题链接

设置快慢指针,慢指针slow指向最后真正存储数值的nums的位置,快指针用来遍历nums数组;

当快慢指针指向元素不同的时候,慢指针向前一步,然后慢指针前进一步后的位置存储快指针fast指向的元素。

当快慢指针指向元素相同时,快指针向前,慢指针不动;

代码如下:

//快慢指针
class Solution {
    public int removeDuplicates(int[] nums) {
        int slow=0,fast=0,len=nums.length;
        while(fast<len){
        	//不相等时,新组成的nums数组最新的一个元素就是此时fast指向的
            if(nums[slow]!=nums[fast]){
                nums[++slow]=nums[fast];//slow先+1
            }
            fast++;
        }
        return slow+1;
    }
}

LeetCode 83. 删除排序链表中的重复元素(方法同26)

原题链接

设置快慢指针和26基本一样,不过要注意头结点为空的情况,以及最后要将slow指针的next指向空。

//快慢指针写法
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if(head==null)return head;
        ListNode slow=head,fast=head;
        while(fast!=null){
            if(slow.val!=fast.val){
                slow.next=fast;//先确定好新的指针链接
                slow=slow.next;//再把slow往后挪
            }
            fast=fast.next;
        }
        slow.next=null;//要断开slow的连接
        return head;
    }
}

还有一种只需要一个cur指针的方法,因为题目要去去重,其实只要把重复的结点跳过去就行:

//cur指针写法
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode cur=head;
        //cur或cur.next为空那么就没必要继续去重了(以及走到尾巴了)
        while(cur!=null&&cur.next!=null){
            //遇到cur与cur.next相同情况,跳过cur.next
            if(cur.val==cur.next.val){
                cur.next=cur.next.next;
            }else{
                cur=cur.next;//否则继续走
            }
        }
        return head;
    }
}

LeetCode 27. 移除元素

原题链接

//双指针写法:如果 fast 遇到值为 val 的元素,则直接跳过,否则就赋值给 slow 指针,并让 slow 前进⼀步。
class Solution {
    public int removeElement(int[] nums, int val) {
        int slow=0,fast=0;
        while(fast<nums.length){
            if(nums[fast]!=val){
                nums[slow++]=nums[fast];
            }
            fast++;
        }
        return slow;
    }
}

注意这里和有序数组去重是有区别的,就是先进行slow+1再进行nums[slow]赋值,还是先进行nums[slow]赋值再slow+1的区别。我们这⾥是先给 nums[slow] 赋值然后再给 slow++,这 样可以保证 nums[0…slow-1] 是不包含值为 val 的元素的,最后的结果数组⻓度就是 slow.


LeetCode 283. 移动零

原题链接

2023.05.29四刷

其实可以在27. 移除元素基础上进行,这题可以看作在nums数组中原地删除0,然后再把后面的元素都赋值为 0 即可。

设置count记录nums中不为0的数字的个数,每遇到一个不为0的数,就先让nums[count]=nums[i],再让count+1;全部赋值完之后,把索引count即之后的数字全置0即可。

class Solution {
    public void moveZeroes(int[] nums) {
        int count=0;//记录nums中不为0的数字的个数
        for(int i=0;i<nums.length;i++){
            if(nums[i]!=0){
                nums[count++]=nums[i];
            }
        }
        for(int i=count;i<nums.length;i++)nums[i]=0;
    }
}

AcWing 799. 最长连续不重复子序列

原题链接

思路:
有点类似滑动窗口,设置双指针,r指针用于遍历nums数组,l指针总是落后于r指针。
针对r的每个位置,都要判断l到r这个区间内是否有重复的数字,如果有重复的数字,l指针要向前走一步继续判断,直到l到r区间没有重复数字;如果没有重复,则r向前走一步,继续下一个循环判断。

本来按照暴力的解法,对于每个r指针位置,l指针都要从0到r范围内进行检索,这样的时间复杂度是O(n^2),但是按照前面提到的思路,当r指针向前移动一步后,l指针无需回退到0位置,而是在原来位置向前检索。这样可以保证总的时间复杂度控制在O(n)。l指针之所以不用回退,是因为对于每个r指针位置,l指针都需要保证它自身到r指针位置区间到没有重复数字,如果有重复数字,l指针就要向前移动。所以每个l指针在当次循环中,总是处于它的最左的位置,当r指针在下一次循环中前移后,l指针只有向前移动的可能。

在这个思路中,很重要的一点是如何判断[l,r]区间内有重复的数字。因为每一次循环中都会保证[l,r]之间没有重复数字,在下一个循环中,r指针只会前移一位,遍历到一个新的数,所以如果下一轮循环出现重复数字,必定是nums[r]这个数字重复了。所以只要在每一轮循环开始的时候,判断nums[r]这个数字出现的次数就行。如果nums中每个数字的范围不大,可以直接用一个较大的数组用于每个数字的计数,如果数字范围较大,并且分布比较分散,就可以使用HashMap统计。

import java.util.Scanner;
import java.util.HashMap;

public class Main{
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] nums=new int[n];
        for(int i=0;i<n;i++)nums[i]=sc.nextInt();
        
        //统计每个数字出现的次数
        HashMap<Integer,Integer> hashmap=new HashMap<>();
        
        int res=0;//存储结果
        for(int l=0,r=0;r<n;r++){
            //循环开始时先检查新遍历到的数有没有在[l,r]中出现过
            //如果在HashMap中不存在该键值,则先设其value为0,再+1
            hashmap.put(nums[r],hashmap.getOrDefault(nums[r],0)+1);
            
            //如果新遍历到的数在hashmap中出现过,也就是[l,r]中存在重复数字,则l需要向前移
            while(hashmap.get(nums[r])>1){
                hashmap.put(nums[l],hashmap.get(nums[l])-1);//并且l对应的数字的value要在hashmap中-1
                l++;
            }
            res=Math.max(res,r-l+1);//用res记录每次循环中[l,r]区间最大长度
        }
        System.out.print(res);
    }
}


AcWing 2816. 判断子序列

原题链接

代码如下:

import java.util.Scanner;

public class Main{
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int m=sc.nextInt();
        int[] a=new int[n];
        int[] b=new int[m];
        for(int i=0;i<n;i++)a[i]=sc.nextInt();
        for(int i=0;i<m;i++)b[i]=sc.nextInt();
        
        int i=0,j=0;//i遍历a数组,j遍历b数组
        while(i<n&&j<m){
            //只有当数字匹配上时,i指针向前移动
            if(a[i]==b[j])i++;
            j++;//无论是否匹配上,j指针都必须向前移动
        }
        
        if(i==n)System.out.print("Yes");
        else System.out.print("No");
    }
}

2.左右指针


LeetCode 11. 盛最多水的容器

原题链接

2023.05.29 一刷

思路:转自Alba
  一开始两个指针一个指向开头一个指向结尾,此时容器的底是最大的,接下来随着指针向内移动,会造成容器的底变小,在这种情况下想要让容器盛水变多,就只有在容器的高上下功夫。 那我们该如何决策哪个指针移动呢?我们能够发现不管是左指针向右移动一位,还是右指针向左移动一位,容器的底都是一样的,都比原来减少了 1。这种情况下我们想要让指针移动后的容器面积增大,就要使移动后的容器的高尽量大,所以我们选择指针所指的高较小的那个指针进行移动,这样我们就保留了容器较高的那条边,放弃了较小的那条边,以获得有更高的边的机会。

代码如下:

//双指针(用时4ms,击败60.13%;内存54.5MB,击败5%)
class Solution {
    public int maxArea(int[] height) {
        int maxArea=0;
        int l=0,r=height.length-1;
        while(l<r){
            int curArea=(r-l)*Math.min(height[l],height[r]);
            maxArea=Math.max(curArea,maxArea);
            if(height[l]<=height[r])
                ++l;
            else
                --r;
        }
        return maxArea;
    }
} 

while循环中间还可以通过判断,再跳过一些状态:

代码如下:

//双指针+快速跳过(用时1ms,击败100%;内存54.5MB,击败5.5%)
class Solution {
    public int maxArea(int[] height) {
        int maxArea=0;
        int l=0,r=height.length-1;
        while(l<r){
            int curArea=(r-l)*Math.min(height[l],height[r]);
            maxArea=Math.max(curArea,maxArea);
            //记录下当前最小的高度,后面小于等于这个高度的都不考虑了(因为向内缩之后一定会小于等于当前面积),直接分别向中间移动
            int minH=Math.min(height[l],height[r]);
            while(height[l]<=minH&&l<r)++l;
            while(height[r]<=minH&&l<r)--r;
        }
        return maxArea;
    }
}

LeetCode 977. 有序数组的平方

原题链接

思路:

数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,i指向起始位置,j指向终止位置。
定义一个新数组res,和nums数组一样的大小,让k指向res数组终止位置。
如果nums[i] * nums[i] <=nums[j] * nums[j] 那么res[k–] = A[j] * A[j–]; 。
如果nums[i] * nums[i] >nums[j] * nums[j] 那么res[k–] = A[i] * A[i++]; 。

代码如下:

class Solution {
    public int[] sortedSquares(int[] nums) {
        int i=0,j=nums.length-1;
        int[] res=new int[nums.length];
        int k=nums.length-1;
        while(i<=j){
            if(nums[i]*nums[i]<=nums[j]*nums[j]){
                res[k--]=nums[j]*nums[j--];
            }else{
                res[k--]=nums[i]*nums[i++];
            }
        }
        return res;
    }
}

AcWing 800. 数组元素的目标和

原题链接

提示思路:i指针从左开始遍历a数组,j指针从右开始遍历b数组

代码如下:

import java.util.Scanner;
public class Main{
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int m=sc.nextInt();
        int x=sc.nextInt();
        int[] a=new int[n];
        int[] b=new int[m];
        for(int i=0;i<n;i++)a[i]=sc.nextInt();
        for(int i=0;i<m;i++)b[i]=sc.nextInt();
        
        //i指针从左开始遍历a数组,j指针从右开始遍历b数组
        for(int i=0,j=m-1;i<n;i++){
            //若两数之和大于x,说明b[j]偏大,需要继续减小
            while(j>=0&&(a[i]+b[j])>x)j--;
            if((a[i]+b[j])==x){
                System.out.print(i+" "+j);
                break;
            }
        }
        
    }
} 

LeetCode 15. 三数之和(双指针+剪枝+去重)

原题链接
2023.05.29 三刷

  题目中要求不能包含重复的三元组,所以就不能简单照搬454.四数之和Ⅱ的分组哈希做法

  • 先将数组排序,用i作为索引遍历nums数组,对每一个i,left=i+1,right=nums.length-1;

  • left和right向中间收缩,当sum<0,说明当前三个数太小,nums[i]固定,只能增大left;同理sum>0,减小right。

  • 另外在遍历的时候需要注意三元组的去重。

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);//先排序才能用双指针
        List<List<Integer>> res=new ArrayList<>();
        for(int i=0;i<nums.length-2;i++){
            //三元组第一个数都比0大,后面加上后两个数不可能等于0,所有后面的都不用考虑
            if(nums[i]>0)break;
            //当前数和前一个一样,那么得到的三元组也会和前一个数得到的三元组一样,直接跳过
            if(i>0&&nums[i]==nums[i-1])continue;//去重
            int left=i+1,right=nums.length-1;
            while(left<right){
                int sum=nums[i]+nums[left]+nums[right];
                if(sum==0){
                    //符合条件,加入res,索引向中间移动
                    res.add(Arrays.asList(nums[i],nums[left++],nums[right--]));
                    //如果nums[left]和nums[left-1]一样,得到的三元组也会一样
                    //为了去重,直接跳过当前这个数。但是要在left
                    while(left<right&&nums[left]==nums[left-1])left++;
                    while(left<right&&nums[right]==nums[right+1])right--;
                }else if(sum<0){
                    left++;
                }else if(sum>0){
                    right--;
                }
            }
        }
        return res;
    }
}

LeetCode 18. 四数之和(双指针将复杂度+去重+剪枝)

原题链接

其实就是在三数之和的基础上,再多一个指针j,三数之和中是nums[i]为确定值,这题里面就用nums[i]+nums[j]作为确定值,然后再利用首尾两个指针left和right向中间收缩。

中间有一些剪枝以及去重操作是需要注意的,可以很好提高代码效率

代码如下:

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res=new ArrayList<>();
        Arrays.sort(nums);
        int n=nums.length;
        for(int i=0;i<n-3;i++){
            //去重
            if(i>0&&nums[i]==nums[i-1])continue;
            //剪枝,当最小的4个数相加都超过,后面肯定找不到符合条件的
            if((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target)break;
            //剪枝,当当前最大的4个数都小于,当前nums[i]肯定不够,直接用下一个
            if((long)nums[i]+nums[n-3]+nums[n-2]+nums[n-1]<target)continue;
            for(int j=i+1;j<n-2;j++){
                //与i同样道理,去重
                if(j>i+1&&nums[j]==nums[j-1])continue;
                //与i一样的道理,剪枝
                if((long)nums[i]+nums[j]+nums[j+1]+nums[j+2]>target)break;
                if((long)nums[i]+nums[j]+nums[n-2]+nums[n-1]<target)continue;
                int left=j+1,right=n-1;
                while(left<right){
                    //四个10亿相加会爆int
                    long sum=(long)nums[i]+nums[j]+nums[left]+nums[right];
                    if(sum==target){
                        res.add(Arrays.asList(nums[i],nums[j],nums[left++],nums[right--]));
                        //去重
                        while(left<right&&nums[left]==nums[left-1])left++;
                        while(left<right&&nums[right]==nums[right+1])right--;
                    }else if(sum<target){
                        left++;
                    }else if(sum>target){
                        right--;
                    }
                }
            }
        }
        return res;

    }
}

3.二分搜索


有关二分搜索的代码模板选择可以参考另一篇文章,链接在此:二分法模板选择


LeetCode 704. 二分查找

原题链接

这题是很基础很典型的二分法的题目,直接套用模板即可。

代码如下:

class Solution {
    public int search(int[] nums, int target) {
        int l=0,r=nums.length-1;
        while(l<r){
            int mid=l+r+1>>1;
            if(nums[mid]<=target)l=mid;
            else r=mid-1;
        }
        //while结束判断最后的l是不是target下标,不是则返回-1
        return nums[l]==target ? l:-1;
    }
}

LeetCode 74. 搜索二维矩阵

原题链接

这题只需要把二维矩阵映射成一维矩阵,然后套用最基础的二分即可。只需要写一个get函数,获取对应位置上二维矩阵的值。

代码如下:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m=matrix.length;
        int n=matrix[0].length;
        int l=0,r=m*n-1;
        while(l<r){
            int mid=l+r+1>>1;
            if(get(matrix,mid)<=target)l=mid;
            else r=mid-1;
        }
        return get(matrix,l)==target;
    }
    //找到二维矩阵对应位置的元素
    public int get(int[][] matrix,int index){
        int n=matrix[0].length;
        int i=index/n;
        int j=index%n;
        return matrix[i][j];
    }
}

LeetCode 162. 寻找峰值

原题链接

可以从题目下面的要求中得到提示:对于所有有效的 i 都有 nums[i] != nums[i + 1],可以通过相邻元素的大小关系去二分搜索逼近峰值。

代码如下:

class Solution {
    public int findPeakElement(int[] nums) {
        int l=0,r=nums.length-1;
        while(l<r){
            int mid=l+r>>1;
            //说明当前最近的峰值在mid右边,向右搜索
            if(nums[mid]<nums[mid+1])
                l=mid+1;
            else//题目要求nums[i]!=nums[i + 1],所以只剩nums[mid]>nums[mid+1]情况
                r=mid;
        }
        return l;
    }
}

LeetCode 852. 山脉数组的峰顶索引(同剑指 Offer II 069. ⼭峰数组的顶部)

原题链接

此题与LeetCode 162. 寻找峰值解法一样,唯一不同在于此题“山峰”唯一,不过用同样的二分法可以逼近山峰。

代码如下:

//根据山脉数组的定义,“山峰”应该是唯一的,因此山峰前后的数值都是单调的
//可以根据arr[mid]与arr[mid+1]之间的关系来确定二分搜索的区间选择
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int l=0,r=arr.length-1;
        while(l<r){
            int mid=l+r>>1;
            if(arr[mid]<arr[mid+1])l=mid+1;
            else r=mid;
        }
        return l;
    }
}

LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置

原题链接

利用二分模板,先找目标范围的左边界,然后再用一遍模板找右边界即可。

代码如下:

class Solution {
    public int[] searchRange(int[] nums, int target) {
        
        int[] res=new int[]{-1,-1};//初始化res,假设找不到目标值的返回数组
        if(nums.length==0)return res;//如果数组为空直接返回

        //先找左边界
        int l=0,r=nums.length-1;
        while(l<r){
            int mid=l+r>>1;
            //一直向左边找(最终找到目标值的左边界)
            if(nums[mid]>=target)r=mid;
            else l=mid+1;
        }
        //while循环结束可能找不到target,需要判断一下
        if(nums[l]==target)res[0]=l;
        else 
            return res;//如果最终的nums[l]≠target,就说明数组中不存在target,直接返回

        //恢复二分区间初值,重新找右边界
        l=0;
        r=nums.length-1;
        while(l<r){
            int mid=l+r+1>>1;
            //一直向右边找(最终停在目标值右边界)
            if(nums[mid]<=target)l=mid;
            else r=mid-1;
        }
        res[1]=l;
        
        return res;
    }
}

剑指 Offer 53 - I. 在排序数组中查找数字 I

原题链接

此题和LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置基本一样,只要把目标数target出现的左右边界求出,用右边界索引减去左边界的索引+1即可求出目标数target出现的次数。

代码如下:

class Solution {
    public int search(int[] nums, int target) {
        if(nums.length==0)return 0;
        int l=0,r=nums.length-1;
        int left=0,right=0;
        //向右逼近
        while(l<r){
            int mid=l+r+1>>1;
            if(nums[mid]<=target)l=mid;
            else r=mid-1;
        }
        if(nums[l]!=target)return 0;
        else right=l;

        //向左逼近
        l=0;r=nums.length-1;
        while(l<r){
            int mid=l+r>>1;
            if(nums[mid]>=target)r=mid;
            else l=mid+1;
        }
        left=l;

        return right-left+1;
    }
}

LeetCode 1011. 在 D 天内送达包裹的能力

原题链接

针对每一个运载能力,其实都可以求证在该运载能力下,需要多少天才可以将货物运输完。题目要求的是最低运载能力,就是要求能在days天内运输完所有货物的运载能力。

思路:
相当于在一个运载量范围内,找到能符合条件的最小值,可以考虑采用二分法。

二分搜索的范围就是运载能力的范围

l=最低运力:只确保所有包裹能够被运送,自然也包括重量最大的包裹,此时理论最低运力为 max,max 为数组 weights 中的最大值

r=最高运力:使得所有包裹在最短时间(一天)内运送完成,此时理论最高运力为 sum,sum 为数组 weights 的总和

代码如下:

class Solution {
    //检查在运载能力为mid时,能否在days天内把包裹全运输完
    public boolean check(int[] weights,int carryCapacity,int days){
        int countDay=1;//记录当前是第几天
        int countWeight=0;//记录当前运载重量
        for(int weight:weights){
            if(countWeight+weight>carryCapacity){
                countDay++;
                countWeight=0;
            }
            countWeight+=weight;
        }
        return countDay<=days;

    }
    public int shipWithinDays(int[] weights, int days) {
        int maxWeight=0;//记录最大重量
        int sumWeight=0;//记录总重
        for(int x:weights){
            if(x>maxWeight)maxWeight=x;
            sumWeight+=x;
        }
        //在载重能力区间进行二分搜索
        int l=maxWeight,r=sumWeight;
        while(l<r){
            int mid=l+r>>1;
            //如果当前载重能力可以在days天内运完,可以向更小的运载能力搜索
            if(check(weights,mid,days))r=mid;
            else l=mid+1;
        }
        return l;
    }
}

LeetCode 875. 爱吃香蕉的珂珂(同剑指 Offer II 073. 狒狒吃香蕉)

原题链接

这题解法思路和LeetCode 1011. 在 D 天内送达包裹的能力差不多。

思路:

如果珂珂在h小时内吃掉所有香蕉的最小速度是每小时k个香蕉,则当吃香蕉的速度大于每小时k个香蕉时一定可以在h小时内吃掉所有香蕉,当吃香蕉的速度小于每小时k个香蕉时一定不能在h小时内吃掉所有香蕉。

由于吃香蕉的速度和是否可以在规定时间内吃掉所有香蕉之间存在单调性,因此可以使用二分查找的方法得到最小速度 k。

由于每小时都要吃香蕉,即每小时至少吃 1 个香蕉,因此二分查找的下界是1;由于每小时最多吃一堆香蕉,即每小时吃的香蕉数目不会超过最多的一堆中的香蕉数目,因此二分查找的上界是最多的一堆中的香蕉数目。

代码如下:

class Solution {
    public int check(int[] piles,int speed){
        int hours=0;//记录在速度k下,需要多少小时吃完
        for(int pile:piles){
            //相当于pile/speed的向上取整(比通过取模判断要不要+1更快)
            hours+=(pile-1)/speed+1;
        }
        return hours;
    }
    public int minEatingSpeed(int[] piles, int h) {
        int maxPile=1;//香蕉堆最大的堆的香蕉数量
        for(int pile:piles){
            maxPile=Math.max(maxPile,pile);
        }
        int l=1,r=maxPile;
        while(l<r){
            int mid=l+(r-l)/2;
            //在mid速度下,可以在h小时内吃完,则速度可以保持或者更小
            if(check(piles,mid)<=h)r=mid;
            else l=mid+1;
        }
        return l;
    }
}

其中值得注意的是速度为speed时,吃每一堆的耗时是pile/speed(向上取整),直接调用Math的ceil方法可以向上取整,但是效率会偏慢一些。可以采用(pile-1)/speed+1进行计算,会更快一些。因为java中"/"是向下取整的,当pile能整除时,因为被先-1了,向下取整后会比pile/speed小1,再+1可以补回来;当pile不能被speed整除时,需要向上取整,(pile-1)/speed会得到向下取整的结果,再+1就是向上取整了。


LeetCode 35. 搜索插入位置(同剑指 Offer II 068. 查找插入位置)

原题链接

这题是寻找目标值的位置,如果没找到,就返回它应该插入数组的位置。不同于
LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置,34题在循环退出时,只需要判断退出的位置(l/r)是否为target,不是则返回-1。而此题在循环退出时还需要根据具体情况判断退出时的nums[mid]与target的关系:

套入模板,代码如下:

class Solution {
    public int searchInsert(int[] nums, int target) {
        int l=0,r=nums.length-1;
        while(l<r){
            int mid=l+r+1>>1;
            if(nums[mid]<=target)l=mid;
            else r=mid-1;
        }
        //退出时一定是l=r,此时nums[mid]与target关系还不确定,需要分类讨论
        //比target小,插入位置就是在l右边一步
        if(nums[l]<target)return l+1;
        else return l;//比target大,或者等于target,插入位置都在当前的l上
    }
}

另一种是借助while循环的终止条件以及内部的return条件来简化出循环之后的判断代码,简而言之就是在while内部就把之后需要判断的情况给分开,使得出循环后不必再进行分类讨论。

代码如下:

class Solution {
    public int searchInsert(int[] nums, int target) {
        int l=0,r=nums.length-1;

        /**注意这里退出条件不能还是按照模板的ltarget,需要往左边一步(mid=r-1),改变的是r指针,插入位置就是当前的mid所在位置,也就是l
        所以当nums[mid]!=target时,要找到插入位置,一定是在l>r的情况下发生的,while循环发生的条件就设置为l<=r.
         */
        while(l<=r){
            int mid=l+r>>1;
            if(nums[mid]==target)return mid;
            else if(nums[mid]<target)l=mid+1;
            else r=mid-1;
        }
        //走到while外面才返回,一定是找不到target,要找插入位置
        return l;
    }
}

剑指 Offer 53 - II. 0~n-1中缺失的数字

原题链接

思路:
长度为n-1的递增排序数组中的所有数字都是唯一的,范围0~n-1内的n个数字中有且只有一个数字不在该数组中。从递增排序数组中找数,典型的二分搜索题目。

对中点nums[mid] (缺失一个数字之后的中点)与mid(完整数组的中点)的关系进行分类讨论:

1.如果nums[mid]==mid,说明缺mid之前的数字都没缺失,缺失的数字在mid右边,l=mid+1;

2.如果缺失的数字在mid处,或者缺失的数字在mid左边,会导致nums[mid]>mid。也就是当nums[mid]>mid时,下一个搜索区间向左走的,并且包括当前mid(因为缺失的数字可能在mid处),也就是r=mid。

代码如下:

class Solution {
    public int missingNumber(int[] nums) {
        int l=0,r=nums.length-1;
        while(l<r){
            int mid=l+r>>1;
            if(nums[mid]==mid)l=mid+1;
            else if(nums[mid]>mid)r=mid;
        }//退出时l=r

        //如果是nums[mid]==mid,缺失数字就在l(mid)的右边一个
        if(nums[l]==l)return l+1;
        else//nums[mid]不可能小于mid,只会发生nums[mid]>mid
            return l;//这时候l=r=mid
    }
}

4.滑动窗口

滑动窗口的核心就是不断调节左右窗口的边界,并保持一个方向移动的趋势。

重点就在于左右窗口边界是如何调整的。


滑动窗口模板(来自labuladong)

模板如下:

/* 滑动窗口算法框架 */
void slidingWindow(String s) {
    Map<Character, Integer> window = new HashMap<>();
    
    int left = 0, right = 0;
    while (right < s.length()) {
        // c 是将移入窗口的字符
        char c = s.charAt(right);
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
        System.out.printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s.charAt(left);
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

其中两处 … 表示的更新窗口数据的地方,到时候直接往里面填自己的操作就行了。

而且,这两个 … 处的操作分别是扩大和缩小窗口的更新操作,会发现它们操作是完全对称的。

另外,虽然滑动窗口代码框架中有一个嵌套的 while 循环,但算法的时间复杂度依然是 O(N),其中 N 是输入字符串/数组的长度。

模板是c++语言的,转换成java需要以下知识:

  • unordered_map 就是哈希表(字典),相当于 Java 的 HashMap,它的一个方法 count(key) 相当于 Java 的 containsKey(key) 可以判断键 key 是否存在。

  • 可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。所以代码中多次出现的 map[key]++ 相当于 Java 的 map.put(key, map.getOrDefault(key, 0) + 1)。

  • 另外,Java 中的 Integer 和 String 这种包装类不能直接用 == 进行相等判断,而应该使用类的 equals 方法


模板题

LeetCode 209. 长度最小的子数组

原题链接

这题想要使用滑动窗口,要弄懂3个问题:

  • 窗口里装什么
  • 窗口左边界如何动
  • 窗口右边界如何动

很明显,我们需要用到窗口里装的数的总和sum,当sum大于等于target时,就记录下此时的窗口大小,如果这是最小的窗口,就将res赋值为当前窗口大小,同时应该从左边界开始缩小窗口。如果sum小于target,就从窗口右边界扩大窗口。

代码如下:

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int start=0,end=0;//窗口起始位置,遍历过程左闭右开
        int sum=0;
        int res=100001;//窗口大小不可能超过数组长度
        int subLenth=0;//实时记录符合要求的窗口大小
        //窗口终止位置为end,用于遍历数组nums
        while(end<nums.length){
            //窗口扩张+右边界前移+更新窗口内数据
            sum+=nums[end++];
            //窗口内数据符合要求,就可以收缩
            while(sum>=target){
                //记录当前窗口内数据
                //本来是end-start+1,但是由于窗口左闭右开(最开始end++了)
                //这时候的end-start就是实际长度
                subLenth=end-start;
                res= res<subLenth ? res:subLenth;
                //窗口左边界缩小
                sum-=nums[start];
                start++;
            }
        }
        if(res==100001)return 0;
        return res;
    }
}

LeetCode 76. 最小覆盖子串(同剑指 Offer II 017. 含有所有字符的最短字符串)

原题链接

2023/06/02 三刷

这题是相对复杂的滑动窗口题,掌握之后,套用模板再做后面的题就会比较容易了。

主要需要解决的问题:
1、什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?

2、什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?

3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

关于窗口边界左闭右开的选择理由:

理论上可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。

代码如下:

//1.HashMap写法
class Solution {
    public String minWindow(String s, String t) {
    	/*注意:里面value存储的类型是包装类,比较value时不能用==,而是要用equals*/
        Map<Character,Integer> need=new HashMap<>();//存储t串字符,及对应字符出现次数
        Map<Character,Integer> window=new HashMap<>();//存储窗口内字符,及对应出现次数

        /** 最开始要先遍历t串,统计每个字符出现的次数(以键值对形式存储)*/
        for(char c:t.toCharArray()){
            //getOrDefault(c,0)作用:如果c这个键上有值,则获取其value;若无值,则赋0
            //后面补上的+1则表示,遍历到c这个字符,就将c对应的value+1;
            need.put(c,need.getOrDefault(c,0)+1);
        }

        int valid=0;//记录窗口中,符合需求的字符的个数
        int wStart=0,wEnd=0;//窗口边界,左闭右开[start,end),初始[0,0)为空
        int wLen=100001;//记录窗口大小
        int subStart=0;//最后返回的子串的边界(substring,左闭右开)

        /**开始滑动窗口代码 */
        while(wEnd<s.length()){
            /**①窗口扩张,右边界移动 */
            char c=s.charAt(wEnd);//记录新加入窗口内的字符
            wEnd++;//

            /**②接下来更新窗口内数据*/
            //判断当前字符c是不是t串中需要的,如果字符c是t串中需要的
            if(need.containsKey(c)){
                //就要把窗口内对应的字符数量+1
                window.put(c,window.getOrDefault(c,0)+1);
                //加完后需要看这个字符的数量达到要求了没,达到了说明满足要求的字符数量+1
                if(window.get(c).equals(need.get(c)))valid++;
            }

            /**③然后就需要考虑窗口缩小问题了,即当窗口内元素符合要求时,左边界前进 */
            //当valid值达到need中包含的字符数量时,说明窗口已经涵盖了t所有字符了
            while(valid==need.size()){
                //记录下当前窗口大小,如果更小
                //本来wEnd-wStart+1才是窗口长度,但是在最前面wEnd已经向前了(左闭右开)
                //所以这里不用+1就是真实窗口长度
                if(wEnd-wStart<wLen){
                    wLen=wEnd-wStart;//就记录更小的窗口长度
                    subStart=wStart;//并且记录更小字串的起始位置(方便最后返回字串)
                }
                char l=s.charAt(wStart++);//记录下当前窗口左边界字符,然后窗口左边界缩小

                /**④开始更新缩小后的窗口内数据 */
                //窗口内字符可能不是t中需要的,就不用对window特别处理,是t需要的才进行处理
                if(need.containsKey(l)){
                    //只有当window中字符l与need中字符l个数相同,去掉l才会导致valid-1
                    if(window.get(l).equals(need.get(l)))valid--;
                    //要去掉的l字符是t需要的,就要在window中将该字符的value-1
                    window.put(l,window.get(l)-1);
                }
            }
        }

        return wLen == 100001 ? "" : s.substring(subStart,subStart+wLen);
    }
}

此外可以用数组模拟hash方法,思路与HashMap一样。代码如下:

// 2.数组模拟hash,字符集大小为k,时间O(tLen+sLen+k),空间O(k)
class Solution {
    public String minWindow(String s, String t) {
        int sLen=s.length(),tLen=t.length();
        int[] need=new int[128];
        for(int i=0;i<tLen;i++){
            char c=t.charAt(i);
            ++need[c];
        }
        int tCount=0;
        for(int i=0;i<128;i++){
            if(need[i]!=0)++tCount;
        }
        int[] window=new int[128];
        int wStart=0,wEnd=0,wCount=0;
        int subLen=100001,subStart=0;
        while(wEnd<sLen){
            char r=s.charAt(wEnd++);
            if(need[r]!=0){
                ++window[r];
                if(window[r]==need[r])wCount++;
            }
            while(wCount==tCount){
                if(wEnd-wStart<subLen){
                    subStart=wStart;
                    subLen=wEnd-wStart;
                }
                char l=s.charAt(wStart++);
                if(need[l]!=0){
                    if(window[l]==need[l]){
                        --wCount;
                    }
                    --window[l];
                }
            }
            
        }
        return subLen==100001 ? "" : s.substring(subStart,subStart+subLen);
    }
}

LeetCode 567. 字符串的排列(同剑指 Offer II 014. 字符串中的变位词)

原题链接

题目理解:

在s2中找到这样一种字串(连续的):包含s1所有字符,包括重复的字母,这些字母的顺序可以打乱。因此只要在s2找到一段连续的字符串,每种字母的数量以及字串长度和s1相同即可。

上一题是要我们找出这样条件的最短的字串,而这题只要求判断有没有存在这样的字串,所以会更简单一些。

同样套用模板,代码如下:

class Solution {
    //这题试下用数组来模拟hash
    public boolean checkInclusion(String t, String s) {
        int[] need=new int[26];//统计s1每个字母数量,初始全为0
        int[] window=new int[26];//统计窗口内字母数量
        int count=0;//记录need中有几种字母
        //统计s1每个字母个数
        for(char c:t.toCharArray()){
            need[c-'a']++;
        }

        for(int i=0;i<26;i++){
            if(need[i]!=0)++count;//记录need中字母有几种
        }

        int valid=0;//记录窗口(window)内和need值相同的字母个数
        int wStart=0,wEnd=0;//窗口边界,左闭右开

        while(wEnd<s.length()){
            char c=s.charAt(wEnd++);
            /*调整窗口内数据 */
            //首先确定s1中有这个字母
            if(need[c-'a']!=0){
                ++window[c-'a'];//窗口内该字母数量+1
                if(window[c-'a']==need[c-'a'])++valid;//加到和s1一样,能匹上的字母多一个
            }

            //什么时候收缩窗口?
            /*这题滑动窗口大小固定为s1长度,因为符合条件的字串长度一定为s1长度 */
            //只有当滑动窗口的长度超过s1长度时才需要从左收缩一位,这里用while和if都行
            while(wEnd-wStart>t.length()){
                char l=s.charAt(wStart++);
                /**修改窗口数据 */
                //当need有左边界这个字母才会对window和valid修改
                if(need[l-'a']!=0){
                    if(window[l-'a']==need[l-'a'])
                        --valid;
                    --window[l-'a'];
                }
            }
            /**必须在窗口收缩之后判断,如果放在收缩前,长度可能会一直不符合要求*/
            //如果窗口内字母种数和need相同,并且窗口长度和s1一样),就是s1的排列了
            if(valid==count&&wEnd-wStart==t.length())return true;
            
        }
        return false;
    }
}

这题尝试了用数组去模拟hashMap,用时会更快一些。


LeetCode 438. 找到字符串中所有字母异位词(同剑指 Offer II 015. 字符串中的所有变位词)

原题链接

2023.05.30 二刷

这题要找的—异位词,其实和前面两题要找的是一样的,只不过这题要求返回所有异位词的起始位置,套用同样的代码即可。

思路:滑动窗口
1.p的异位词要求长度和pLen一样,且每个字母出现次数也要一样
2.用need数组统计p中每个字母出现的次数,用pCount统计p字符串有多少种字母
3.窗口内是什么?–窗口长度保持和p的长度一致
4.窗口扩张–最开始先扩张到和p的长度一致,然后每次扩张一步。在扩张过程中需要统计窗口内的各字母数量(只有扩展的字母是need中的才能计入window中),当窗口内该种字母数量达到need中需求时,wCount+1,表示窗口内符合异位词字母出现次数的字母种类+1。
5.窗口收缩–窗口大小超过p就需要收缩,被收缩的字母数-1,如果-1后不满足need的数量,那么wCount就需要-1,然后window中对应字母数量-1.
6.最后要求长度相同(pLenwStart),并且符合要求的字母数量相同(pCountwCount)

代码如下:

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int sLen=s.length(),pLen=p.length();
        List<Integer> res=new ArrayList<>();
        int[] need=new int[26];
        int[] window=new int[26];
        int count=0;

        for(int i=0;i<pLen;i++)++need[p.charAt(i)-'a'];
        for(int i=0;i<26;i++){
            if(need[i]!=0)count++;
        }

        int wStart=0,wEnd=0;
        int valid=0;//window中能与need数量一样的字母数

        while(wEnd<sLen){
            char c=s.charAt(wEnd++);
            //首先确定加入窗口的这个字符有没有在p中
            if(need[c-'a']!=0){//如果该字符在p中
                ++window[c-'a'];//窗口内该字符数量+1
                if(window[c-'a']==need[c-'a'])++valid;//当数量加到与p中相同
            }
            //符合题目要求的窗口长度一定和p长度相同,当长度超过时,需要从左边缩小
            if(wEnd-wStart>pLen){
                char l=s.charAt(wStart++);
                //左边界字符要在p中,才需要修改窗口中有关异位词的数据
                if(need[l-'a']!=0){
                    //如果移出窗口的字符在窗口中时,该字符数量和p中相同
                    //那么其移出之后,符合条件的字符数量-1
                    if(window[l-'a']==need[l-'a'])
                        --valid;
                    --window[l-'a'];//窗口内该字符数量-1
                }
            }
            //只有长度相同,并且符合要求的字符数量相同时,窗口内才是异位词
            if(wEnd-wStart==pLen&&valid==count)res.add(wStart);
        }
        return res;
    }
}

LeetCode 3. 无重复字符的最长子串(同剑指 Offer 48. 最长不含重复字符的子字符串 与 剑指 Offer II 016. 不含重复字符的最长子字符串)

原题链接

这题用一个hashmap类型的window存储所有字符的个数,当加入的字符重复时,就缩小窗口,将左边界向右移动。当窗口收缩完之后,再统计字串的长度,更新res。(其实用HashSet更方便一点,因为HashSet不允许有重复值)

代码如下:

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int res=0;
        int sLen=s.length();
        Map<Character,Integer> window=new HashMap<>();
        int start=0,end=0;
        while(end<sLen){
            char c=s.charAt(end++);
            window.put(c,window.getOrDefault(c,0)+1);

            while(window.get(c)>1){
                char l=s.charAt(start++);
                window.put(l,window.get(l)-1);
            }
            res=Math.max(res,end-start);
        }
        return res;
    }
}

此外还可以用数组模拟hash的方法,代码如下:

/* 思路:
1.题目只要求长度,可以用maxLen来记录遍历过程中的最大长度;
2.利用滑动窗口,窗口内的字符不重复,那么窗口大小就是不含有重复字符的最大长度
3.如何保证窗口内字符不重复?--可以用数组模拟hash,用来记录窗口内各种字符的数量
4.窗口扩张--模拟hash数组对应+1,窗口长度+1,窗口右边界+1
5.窗口收缩--当前面扩张时进入窗口的字符数量大于1收缩,窗口长度-1,模拟hash数组对应-1,左边界+1.

2023.05.30 三刷
 */

//时间O(n),空间O(字符集大小,一般为128)
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int maxLen=0;//最大长度
        int[] hash=new int[128];
        int l=0,r=0;
        int sLen=s.length();
        while(r<sLen){
            char c=s.charAt(r++);//窗口左闭右开(一开始r++)
            ++hash[c];
            while(hash[c]>1){
                char cc =s.charAt(l++);
                --hash[cc];
            }
            maxLen=maxLen>r-l ? maxLen:r-l;//收缩之后窗口才符合没有重复字符的要求
        }
        return maxLen;
    }
}

LeetCode 904. 水果成篮

原题链接

理解题目:
要求在fruit上找到最长的连续区间,区间满足这样的要求:有且只有两种数字(代表水果种类),题目要求返回这个最长区间的长度。

思路:从连续区间、最长,可以想到采用滑动窗口,需要考虑以下几个问题:
①窗口内装什么:窗口内是水果的种类
②窗口左边界何时缩小:当窗口内部水果种类数目超过2种,就要缩小左边界

需要用一个数组模拟的hash表存储当前窗口内每种水果的数量;
当窗口扩张(右边界向前),判断这种水果数量是不是0,如果是,窗口内部水果种类count+1,
并且hash[fruit[end]]++;不是0则直接hash[fruit[end]]++。
当窗口内count>2时,需要缩小左边界,更新count直到count==2
(注意只有当窗口内hash[fruit[start]]==0的时候count才会-1)

代码如下:时间O(n),空间O(n)

class Solution {
    public int totalFruit(int[] fruits) {
        int n=fruits.length;
        int[] hash=new int[n];//统计窗口内每种水果有几个
        int count=0;//统计窗口内部水果种类数量
        int res=0;
        int start=0,end=0;

        while(end<n){
            //先右边界扩张
            if( hash[fruits[end]]==0)++count;
            ++hash[fruits[end]];
            ++end;

            //再收缩左边界
            while(count>2){
                --hash[fruits[start]];
                //先判断,再start++,要不会越界
                if(hash[fruits[start]]==0)--count;
                start++;
            }//出了while,count一定<=2,可以统计水果数量了
            res = res<end-start ? end-start:res;
        }
        return res;
    }
}

LeetCode 239. 滑动窗口最大值(同剑指 Offer 59 - I. 滑动窗口的最大值)

原题链接

2023.06.01 三刷

思路:

这题无法用简单的滑动窗口得出题目要求的结果,因为针对每一个长度为k的窗口,都需要记录当前该窗口中的最大值,随着窗口的滑动,之前的最大值可能会掉出这个窗口,导致需要重新在这个窗口内重新寻找最大值。所以需要一个数据结构来有序存储窗口内的元素。

可能会想到优先级队列,但是普通的优先级队列在这题里行不通,因为优先级队列出队只按照元素大小,无法根据元素先进先出的规则进行出队(滑动窗口内元素遵循先进先出),想要在这题里用优先级队列还需要一点特殊的处理(见Java PriorityQueue(优先级队列/二叉堆)的使用及题目应用)。

所以,现在需要一种新的队列结构,既能够维护队列元素「先进先出」的时间顺序,又能够正确维护队列中所有元素的最值,这就是「单调队列」结构。

总结一下这个滑动窗口里单调队列需要实现的功能:
void push(int num):
单调队列中存的是窗口内的元素,要保证队头元素总是当前队列中最大的,这就需要可以在队尾插入元素(offerLast())的时候,总是将队尾中小于要插入的num的元素删除(removeLast()),这样就可以保证队列从队头到队尾保持从大到小的状态。
void pop(int num):
需要注意的是,为了保证逆序队列,在入队的时候已经删了一些元素(窗口所有元素并不需要都在队列中,队列只要留存有机会成为最大值的元素即可),因此在后续需要将窗口左边界元素弹出队列(removeLast())时,需要判断这个元素是不是队头元素(peek()),是的话才弹出,否则不操作(不操作是因为这个元素在之前已经弹出了)。

int peek():
最后还需要一个函数用于返回队头元素(return deque.peek());

代码如下:

//单调队列--时间O(n),空间O(k)
class Solution {
    //自己实现针对此题的单调队列
    class Monotonic{
        //用双端队列来实现单调队列(因为队头队尾元素都需要插入删除操作)
        Deque<Integer> deque=new LinkedList<>();
        int peek(){
            return deque.peek();
        }
        void push(int num){
            // 所有队尾小于val的都删除(注意队列非空,否则可能会报错)
            while(!deque.isEmpty()&&deque.getLast()<num)deque.pollLast();
            deque.addLast(num);
        }
        //队列中元素逆序,要删除的左边界元素可能在push的时候就已经被删除了
        //队头元素一定这一批元素中最早进入的
        //要弹出的左边界元素,如果等于队头元素,说明要删的就是队头
        // 如果和队头不同,说明可能之前push的时候就已经被删除了,就不需要操作
        void pop(int num){
            // 注意需要队列非空
            if(!deque.isEmpty()&&num==deque.peek())deque.pollFirst();
        }
    }
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n=nums.length;
        int[] res=new int[n-k+1];
        Monotonic dq=new Monotonic();
        //存入k个(可能实际不到k个,因为被后来的给删除了)
        for(int i=0;i<k;i++)dq.push(nums[i]);
        res[0]=dq.peek();
        //遍历剩下的n-k个
        for(int i=k;i<n;i++){
            //要先把左边界弹出再push,如果左边界是上个窗口最大值,很大
            //不先弹出去直接push,队头里留存的还是上一个窗口的最大值
            dq.pop(nums[i-k]);
            dq.push(nums[i]);//不能先push
            res[i-k+1]=dq.peek();
        }
        return res;
    }
}

还有一种更简洁的写法:

/*
  思路: 遍历数组 L R 为滑窗左右边界 只增不减
        双向队列保存当前窗口中最大的值的数组下标 双向队列中的数从大到小排序,
        新进来的数如果大于等于队列中的数 则将这些数弹出 再添加
        当R-L+1=k 时 滑窗大小确定 每次R前进一步L也前进一步 保证此时滑窗中最大值的
        数组下标在[L,R]中,并将当前最大值记录
  举例: nums[1,3,-1,-3,5,3,6,7] k=3
     1:L=0,R=0,队列【0】 R-L+1 < k
            队列代表值【1】
     2: L=0,R=1, 队列【1】 R-L+1 < k
            队列代表值【3】
     解释:当前数为3 队列中的数为【1】 要保证队列中的数从大到小 弹出1 加入3
          但队列中保存的是值对应的数组下标 所以队列为【1】 窗口长度为2 不添加记录
     3: L=0,R=2, 队列【1,2】 R-L+1 = k ,result={3}
            队列代表值【3,-1】
     解释:当前数为-1 队列中的数为【3】 比队列尾值小 直接加入 队列为【3,-1】
          窗口长度为3 添加记录记录为队首元素对应的值 result[0]=3
     4: L=1,R=3, 队列【1,2,3】 R-L+1 = k ,result={3,3}
            队列代表值【3,-1,-3】
     解释:当前数为-3 队列中的数为【3,-1】 比队列尾值小 直接加入 队列为【3,-1,-3】
          窗口长度为4 要保证窗口大小为3 L+1=1 此时队首元素下标为1 没有失效
          添加记录记录为队首元素对应的值 result[1]=3
     5: L=2,R=4, 队列【4】 R-L+1 = k ,result={3,3,5}
            队列代表值【5】
     解释:当前数为5 队列中的数为【3,-1,-3】 保证从大到小 依次弹出添加 队列为【5】
          窗口长度为4 要保证窗口大小为3 L+1=2 此时队首元素下标为4 没有失效
          添加记录记录为队首元素对应的值 result[2]=5
    依次类推 如果队首元素小于L说明此时值失效 需要弹出
*/
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums==null||nums.length<2) return nums;
        // 双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数按从大到小排序
        LinkedList<Integer> list = new LinkedList();
        // 结果数组
        int[] result = new int[nums.length-k+1];
        for(int i=0;i<nums.length;i++){
            // 保证从大到小 如果前面数小 弹出
            while(!list.isEmpty()&&nums[list.peekLast()]<=nums[i]){
                list.pollLast();
            }
            // 添加当前值对应的数组下标
            list.addLast(i);
            // 初始化窗口 等到窗口长度为k时 下次移动在删除过期数值
            if(list.peek()<=i-k){
                list.poll();   
            } 
            // 窗口长度为k时 再保存当前窗口中最大值
            if(i-k+1>=0){
                result[i-k+1] = nums[list.peek()];
            }
        }
        return result;
    }
}

你可能感兴趣的:(力扣刷题记录,leetcode,算法)