代码随想录

                                                                                                                                                                                                                                                                                                                                                  @[TOC](数组篇)

数组理论基础

首先我们需要明白什么是数组?数组在内存中的存储方式是什么样的/

  1. 数组是集合,并且是存在在连续内存空间上相同类型数据的集合。
  2. 数组中每个元素会有一个索引,因此可以通过索引获取对应下标的数据。并且下标都是从0开始的,在内存空间的地址也是连续的。
  3. 既然数组在存储数据的时候地址是连续的,那在进行增删元素的操作的时候,不可避免地需要移动其他元素的地址,从而保证依旧满足地址连续这一特点。

在一维数组中,我们很容易理解数组在内存中存储的地址是连续的,但是二维数组中是否还是连续的呢?因为不同的编程语言的内存管理是不一样的,所以这就取决于具体的编程语言了。比如在C++中二维数组是连续分布的,但是在JAVA中没有指针,二维数组的地址是没有规则的,自然也谈不上连续。

二分查找/折半查找

` 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,
写一个函数搜索 nums 中的 target,如果 target 存在返回下标,否则返回 -1。
你必须编写一个具有 O(log n) 时间复杂度的算法。`

思路
二分查找是一种在有序数组中查找特定元素的算法,它通过不断将搜索区间减半来快速定位目标元素。

我们通过leftright来间接表示mid对应的元素

  1. 排除一些特殊情况,目标元素<数组首位元素(最小数) 或者 目标元素>数组末尾元素(最大数);
  2. 前提条件【while (left ? right)】,这决定区间开闭;
  3. 通过判断target与mid对应元素的大小,一步步切分数组定位目标元素【middle、middle+1、middle-1】。

写代码之前,首先要明确target定义在一个什么样的区间(前提条件),这会影响到二分法的边界处理。

第一种:左闭右闭[left, right]

left = 0,right= nums.length-1

class Solution {
    public int search(int[] nums, int target) {
        if(nums==null||nums.length==0){
            return -1;
        }
        int left=0;
        int right= nums.length-1;
        while(left<=right){

            int mid= right + (left - right)/2;
            if(nums[mid]>target){
                right =mid-1;
            } else if (nums[mid]<target){
                left=mid+1;
            }else{
                return mid;
            }
        }
        return -1 ;
    }
}

第二种:左闭右开[left, right)

left = 0,right= nums.length

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length;
        if(target < nums[left] || target > nums[right - 1]){
            return -1;
        }
        while (left < right){
            int mid = left + ((right - left) >> 1);
            if(target == nums[mid]){
                return mid;
            }else if(target > nums[mid]){
                left = mid + 1;
            }else{
                right = mid;
            }
        }
        return -1;
    }
}

进一步巩固,做一些和二分法相关的算法题目。

35. 搜索插入位置

`给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。
如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。

示例 1:
输入: [1,3,5,6], 5
输出: 2

示例 2:
输入: [1,3,5,6], 2
输出: 1

示例 3:
输入: [1,3,5,6], 7
输出: 4

提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 为 无重复元素 的 升序 排列数组
-104 <= target <= 104`

思路
根据题目可知,目标值可以以以下形式被插入:

  1. 数组中存在该目标值
  2. 插入在所有元素之前
  3. 插入在某两个元素之间
  4. 插入在所有元素之后

(1) 如果使用暴力解法,即遍历数组元素,直到找到一个元素不小于目标元素,那么该元素对应的索引就是我们需要输出的结果。如果都没找到,则返回数组长度。
例如:数组[1, 3, 4, 6],目标元素为3,即返回下标1;目标元素为2,数组中第一个不小于目标元素的元素为3,对应下标1。

但是这种遍历,时间复杂度和空间复杂度都为O(n)

(2)观察题目可以发现给定了一个有序数组,满足二分法的基础条件,我们可以尝试这种解法。【题目强调数组中无重复元素,因为有重复元素,使用二分查找法返回的元素下标可能不是唯一的】

和上面的思路一样,我们首先要分清楚target所处的区间类型,这里以左闭右闭为例。

直观上来看,这一题和上面二分查找的差异就是,上一题找不到直接返回-1;这一题不需要考虑特殊情况,因为正是所需要返回的是插入的位置,即right+1。使用这种解法时间复杂度O(logn)空间复杂度O(1)

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while(left <= right){
            int mid = left + ((right - left)/2);
            if(nums[mid] == target){
                return mid;
            }else if(target > nums[mid]){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return right + 1;
    }
}

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

`给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。
请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:
输入:nums = [], target = 0 
输出:[-1,-1]

提示:     
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109

思路
上面我们提到二分查找适用于无重复元素的有序数组中元素的定位,因为元素不唯一,返回的索引也会不唯一。对于这一题,既然元素不唯一,但是有标明有序数组,要求时间复杂度为O(logn),我们可以尝试用二分查找解题。

但是左右两个索引该如何确定呢?那就用两个二分查找来确定左右边界。
假设数组中存在目标元素,在我们找到之后,这意味着它有可能是我们所寻找的左边界,将其标记。然后我们需要继续使用二分法确定他的左侧是否还存在目标元素,从而更新左边界。右边界同理。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int lb = leftboeder(nums, target);
        int rb = rightboeder(nums, target);
        return new int[]{lb,rb};

    }

    public int leftboeder(int[] nums, int target){
        int left = 0, right = nums.length - 1;
        int lb = -1;
        while(left <= right){
            int mid = left + ((right - left) >> 1);
            if(nums[mid] == target){
                lb = mid;
                right = mid - 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return lb;
    }

    public int rightboeder(int[] nums, int target){
        int left = 0, right = nums.length - 1;
        int rb = -1;
        while(left <= right){
            int mid = left + ((right - left) >> 1);
            if(nums[mid] == target){
                rb = mid;
                left = mid + 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return rb;
    }
}

69. x的平方根

给你一个非负整数x,计算并返回x的算术平方根。
由于返回类型是整数,结果只保留整数部分,小数部分将被舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

示例 1:
输入:x = 4
输出:2

示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

思路
这一题要求计算一个非负整数 x 的算术平方根,但返回值是整数,所以真正要找的不是完整的小数平方根,而是平方根向下取整后的结果。换句话说,我们需要找到一个最大整数k,使k * k <= x。

分析可知平方函数具有单调性,我们可以把可能的答案看成一个有序区间,在 0 到 x 之间寻找最终结果。每次取中间值 mid,判断 mid * mid 和 x 的大小关系。如果 mid * mid 正好等于 x,说明 mid 就是准确的平方根,可\以直接返回;如果 mid * mid 小于 x,说明 mid 这个数没有超过平方根,它可能是答案,但右边可能还存在更大的合法答案,所以我们先记录当前的 mid,再继续向右查找;如果 mid * mid 大于 x,说明 mid 已经太大了,它以及它右边的数都不可能是答案,因此需要向左查找。

class Solution {
    public int mySqrt(int x) {
        int left = 0, right = x;
        int result = 0;
        while(left <= right){
            int mid = left + ((right - left) >> 1);
            long square = (long) mid * mid;
            if(square == x){
                return mid;
            }else if(square < x){
                result = mid;
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return result;
    }
}

367. 有效的完全平方数

给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
不能使用任何内置的库函数,如  sqrt 。

示例 1:
输入:num = 16
输出:true
解释:返回 true ,因为 4 * 4 = 164 是一个整数。

示例 2:
输入:num = 14
输出:false
解释:返回 false ,因为 3.742 * 3.742 = 143.742 不是一个整数。

思路
理解了上一道题目之后,这一题思路就比较简单了。只需要判断一下即可。

class Solution {
    public boolean isPerfectSquare(int num) {
        int left = 0, right = num;
        int result = 0;
        while(left <= right){
            int mid = left + ((right - left) >> 1);
            long square = (long) mid * mid;
            if(square == num){
                result = mid;
                break;
            }else if(square < num){
                result = mid;
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        if(result * result == num){
            return true;
        }else{
            return false;
        }
    }
}

移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。
nums 的其余元素和 nums 的大小并不重要。
返回 k。

示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,_,_]
解释:你的函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,_,_,_]
解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

在前面的数理基础知识中,可以知道数组中的元素在内存中存储的地址是连续的,不能进行元素的删除,只能覆盖。

思路
1. 暴力解法 题目中没有给出时间复杂度的限制,我们可以考虑一下最直接粗暴的方式,用一层for循环遍历数组元素,当遇到和目标元素值的元素时,用另一层for循环将后面的元素向前覆盖。

class Solution {
    public int removeElement(int[] nums, int val) {
        int n = nums.length;
        for(int i = 0; i < n; i ++){
            if(nums[i] == val){
                for(int j = i;j < n-1; j++){
                    nums[j] = nums[j+1];
                }
                i--; // i之后的元素都向前移动了一个位置,所以i也向前移动一位
                n--;
            }
        }
        return n;
    }
}

很显然,这种暴力解法的时间复杂度是O(n^2)。

2. 双指针法 上面这种解法相当于,一旦找到一个相同元素,就更新整个数组,然后在继续往后遍历,循环这个步骤。思考一下,当前我有两个指针在遍历,一个快指针一个慢指针,或者更形象一些称之为读写指针。只考虑已经遍历到的元素,不考虑索引 i 之后的元素。读指针在逐个读取数组中的元素,如果不等于目标值,写指针便将该元素赋值准入数组中它所在的位置,指针加一;如果等于目标值,直接跳过,写指针不会动也不会赋值,从而便过滤掉了目标值。

class Solution {
    public int removeElement(int[] nums, int val) {
        int n = nums.length;
        int fast = 0, slow = 0;
        while(fast < n){
            if(nums[fast] != val){
                nums[slow] = nums[fast];
                slow++;
            }
            fast++;
        }
        return slow;
    }
}

这种方法方法的时间复杂度是O(1),也没有改变相对位置。

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

给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k。去重后,返回唯一元素的数量 k。
nums 的前 k 个元素应包含 排序后 的唯一数字。下标 k - 1 之后的剩余元素可以忽略。

示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4,_,_,_,_,_]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

思路
这道题的关键在于理解两个条件:第一,数组本身已经是非严格递增排列,也就是说相同的元素一定是连续挨在一起的;第二,题目要求我们原地删除重复元素,不能新建一个数组来保存答案。由于 Java 数组长度固定,所以这里的“删除”并不是真的把数组变短,而是把不重复的元素依次放到数组前面,最后返回不重复元素的个数 k。数组下标 k 后面的内容不需要关心。

class Solution {
    public int removeDuplicates(int[] nums) {
        int fast = 1, slow = 0;
        int n = nums.length - 1;
        while(fast <= n){
            if(nums[fast] != nums[slow]){
                slow++;
                nums[slow] = nums[fast];
            }
            fast++;  
        }
        return slow+1;
    }
}

283.移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:
输入: nums = [0]
输出: [0]

思路
这一题本质上和上面一样,只是将目标值target换成了固定数0,覆盖之后继续在后面补0即可。

class Solution {
    public void moveZeroes(int[] nums) {
        int fast = 0, slow = 0;
        int n = nums.length;
        while(fast < n){
            if(nums[fast] != 0){
                nums[slow] = nums[fast];
                slow ++;
            }
            fast++;
        }
        while(slow < n){
            nums[slow] = 0;
            slow++;
        }
    }
}

844.比较含退格的字符串

给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true# 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。

示例 1:
输入:s = "ab#c", t = "ad#c"
输出:true
解释:s 和 t 都会变成 "ac"。

示例 2:
输入:s = "ab##", t = "c#d#"
输出:true
解释:s 和 t 都会变成 ""。

示例 3:
输入:s = "a#c", t = "b"
输出:false
解释:s 会变成 "c",但 t 仍然是 "b"。

提示:
1 <= s.length, t.length <= 200
s 和 t 只含有小写字母以及字符 '#'

思路
把 # 当作退格键,用快慢指针模拟文本输入过程。fast 负责扫描原字符串,slow 负责维护最终保留下来的有效内容。遇到普通字符就写入,遇到 # 就删除前一个有效字符。分别处理两个字符串后,再比较最终结果是否一致。

class Solution {
    public boolean backspaceCompare(String s, String t) {
        s = build(s);
        t = build(t);
        return s.equals(t);
    }

    private String build(String str) {
        char[] chars = str.toCharArray(); // Java中字符串本身不能修改,把字符串转成字符数组

        int slow = 0;
        int fast = 0;
        int n = chars.length;

        while (fast < n) {
            if (chars[fast] == '#') {
                if (slow > 0) {
                    slow--;
                }
            } else {
                chars[slow] = chars[fast];
                slow++;
            }
            fast++;
        }

        return new String(chars, 0, slow);
    }
}

977.有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 已按 非递减顺序 排序

进阶:
请你设计时间复杂度为 O(n) 的算法解决本问题

思路
这道题表面上看很简单:把数组中每个数字平方,然后排序。但如果真的先平方再排序,虽然也能做出来,时间复杂度会是O(n log n)。如果我们要完成进阶要求的话,可以看到题目中给了一个重要条件nums 已按 非递减顺序 排序,也就是说,原数组本来就是有序的。我们可以利用这个条件,把时间复杂度优化。

原数组有序,这说明平方后的最大值一定来自于数组的两端中的一个【最左边可能是绝对值很大的负数,最右边可能是很大的正数】。可以使两个指针分别指向数组的两端,结果数组要求从小到大排列,所以每次我们找到当前的最大平方值,从结果数组末尾开始存储。

class Solution {
    public int[] sortedSquares(int[] nums) {
        int n = nums.length;
        int left = 0, right = n-1;
        int[] result = new int[n];
        while(left <= right){
            int leftsquare =nums[left]*nums[left];
            int rightsquare = nums[right]* nums[right];
            if(leftsquare >= rightsquare){
                result[n-1] = leftsquare;
                left++;
            }else{
                result[n-1] = rightsquare;
                right--;
            }
            n--;
        }

        return result;
    }

长度最小的子数组

'给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。
如果不存在符合条件的子数组,返回 0 。

示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:
输入:target = 4, nums = [1,4,4]
输出:1

示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 104

进阶:
如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

思路
不考虑时间复杂度的话,可以使用暴力解法。用两层for循环一点点找符合条件的子数组。

滑动窗口
仔细观察这道题目,可以发现有一个条件是组里的元素都是正整数。正因为都是正数,我们才可以使用滑动窗口来解决。这是因为滑动窗口能成立,靠的是一个很重要的性质:窗口扩大时,sum 一定变大;窗口缩小时,sum 一定变小。这个单调变化的特点,使我们可以通过不断调整左右边界来寻找最短的连续子数组。

滑动窗口的基本思路是:

  1. 窗口总和小于target的时候,说明窗口还不够大,右指针需要继续向右移动。
  2. 窗口总和已经满足大于等于target的条件的时候,说明窗口长度已经够了,我们需要移动左指针,即缩小窗口,寻找是否有更小的窗口【子数组】可以满足。

这样不断扩张右边界、收缩左边界,就能找到最短的符合条件的连续子数组。

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0, right = 0;
        int len = Integer.MAX_VALUE;
        int sum = 0;
        while(right < nums.length){
            sum = sum + nums[right];
            while(sum >= target){
                len = Math.min(len, right - left + 1);
                sum -= nums[left];
                left++;
            }
            right++;
        }
        return len == Integer.MAX_VALUE ? 0 : len;
    }
}

整个过程中,每个元素最多被右指针加入一次,也最多被左指针移出一次,所以时间复杂度是 O(n)。由于只使用了几个变量,没有额外数组,空间复杂度是 O(1)。

904.水果成篮

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,
其中 fruits[i] 是第 i 棵树上的水果 种类 。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
你可以选择任意一棵树开始采摘,你必须从 每棵树(包括开始采摘的树)上恰好摘一个水果 。
采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

示例 1:
输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。

示例 2:
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。
 
提示:
1 <= fruits.length <= 105
0 <= fruits[i] < fruits.length

思路
理解题目之后可以发现,实则是在数组fruits中,找出一个最长连续的子数组,且只包含两个不同的水果类型【元素】。因为我们只能从某一棵树开始,一直向右连续采摘,不能跳过中间的树;同时又只有两个篮子,每个篮子只能装一种水果,所以最终问题就转化成了**“求最多包含两种不同元素的最长连续区间”**。

参考前面滑动窗口的思想,

  1. 我们可以维护一个窗口[left, right],表示正在考虑的一段连续果树。并且记录窗口内水果的种类数。
  2. 右边界移动时,说明正在尝试采摘新的果树,如果右边界加入一棵新树后,窗口中的水果种类变成了三种,就说明当前这段区间已经不合法了,记录此时可采摘水果数量【即窗口长度】。
  3. 移动左边界,把窗口最左边的水果逐个移出去,同时减少对应水果的计数。
  4. 如果某种水果的计数减少到 0,说明这种水果已经不在当前窗口里了,就可以从记录中删除它。我们持续移动左边界,直到窗口中重新只剩下两种水果为止。
  5. 更新可采摘水果数量,取最大值即可。
class Solution {
    public int totalFruit(int[] fruits) {
        // 创建一个哈希表 cnt,用来记录当前窗口中每种水果出现的次数。
        // 键 Integer 表示水果种类。值 Integer 表示这种水果在当前窗口中出现了几次。
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();

        int left = 0, ans = 0;
        int n = fruits.length;

        for(int right = 0; right < n; ++right){
            cnt.put(fruits[right], cnt.getOrDefault(fruits[right], 0) + 1);

            while(cnt.size() > 2){
                cnt.put(fruits[left], cnt.get(fruits[left]) - 1);

                if(cnt.get(fruits[left]) == 0){
                    cnt.remove(fruits[left]);
                } 

                ++left;
            }

            ans = Math.max(ans,right - left + 1);
        }

        return ans;

    }
}

76.最小覆盖字串

给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的 最短窗口 子串,使得该子串包含 t 中的每一个字符(包括重复字符)。
如果没有这样的子串,返回空字符串 ""。

测试用例保证答案唯一。

示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B''C'。

示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示:
m == s.length
n == t.length
1 <= m, n <= 105
s 和 t 由英文字母组成

进阶:你能设计一个在 O(m + n) 时间内解决此问题的算法吗?

思路
这道题要求我们在字符串 s 中找到一个最短的连续子串,使得这个子串能够包含字符串 t 中的所有字符,并且如果 t 中某个字符出现了多次,窗口中也必须至少出现相同次数。比如 t = “AABC”,那么一个合法窗口中至少要有两个 A、一个 B 和一个 C,只包含一个 A 是不够的。因此,我们不仅要知道某个字符是否出现过,还要知道它出现的次数是否足够。

<1> 一种直观的做法是使用两个哈希表,一个统计 t 中每个字符需要出现多少次,另一个统计当前窗口中每个字符已经出现多少次。然后比较两个表,判断窗口是否满足要求。

<2> 不过由于题目说明 s 和 t 都由英文字母组成,字符范围比较小,所以我们可以用数组来代替哈希表,让代码更加简洁高效。**数组下标表示字符,数组值表示数量差,**这样可以减少哈希表的额外开销,代码运行也会更快。

class Solution {
    public String minWindow(String s, String t) {
        // diff[c] = 当前窗口中字符 c 的个数 - t 中字符 c 需要的个数
        int[] diff = new int[128];
        // 所需要满足的字符种类数
        int needkinds = 0;

        for(char c : t.toCharArray()){
            if(diff[c] == 0){
                needkinds++;
            }
            diff[c]--;
        }

        char[] chars = s.toCharArray();
        int n = chars.length;
        int left = 0;
        // satisfiedKinds 表示当前窗口中有多少种字符已经满足 t 的要求
        int satisfiedKinds = 0;

        // 记录最短覆盖子串的左右边界
        int ansLeft = -1, ansRight = n;

        for(int right = 0; right < n; right++){
            // 右边界字符进入窗口
            char c = chars[right];
            diff[c]++;

            // 如果 diff[c] 变成 0,说明字符 c 从“不够”变成“刚好够”
            if(diff[c] == 0){
                satisfiedKinds++;
            }

            // 当所有需要的字符都满足时,尝试收缩左边界
            while (satisfiedKinds == needkinds) {
                // 更新更短的答案
                if (right - left < ansRight - ansLeft) {
                    ansLeft = left;
                    ansRight = right;
                }
                // 左边界字符准备移出窗口
                char x = chars[left];

                // 如果 diff[x] == 0,说明移出前 x 刚好满足要求
                // 移出后,x 的数量就会不够
                if (diff[x] == 0) {
                    satisfiedKinds--;
                }

                diff[x]--;
                left++;
            }
        }

        return ansLeft == -1 ?  "" : s.substring(ansLeft, ansRight + 1);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值