@[TOC](数组篇)
数组理论基础
首先我们需要明白什么是数组?数组在内存中的存储方式是什么样的/
- 数组是集合,并且是存在在连续内存空间上的相同类型数据的集合。
- 数组中每个元素会有一个索引,因此可以通过索引获取对应下标的数据。并且下标都是从0开始的,在内存空间的地址也是连续的。
- 既然数组在存储数据的时候地址是连续的,那在进行增删元素的操作的时候,不可避免地需要移动其他元素的地址,从而保证依旧满足地址连续这一特点。
在一维数组中,我们很容易理解数组在内存中存储的地址是连续的,但是二维数组中是否还是连续的呢?因为不同的编程语言的内存管理是不一样的,所以这就取决于具体的编程语言了。比如在C++中二维数组是连续分布的,但是在JAVA中没有指针,二维数组的地址是没有规则的,自然也谈不上连续。
二分查找/折半查找
` 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,
写一个函数搜索 nums 中的 target,如果 target 存在返回下标,否则返回 -1。
你必须编写一个具有 O(log n) 时间复杂度的算法。`
思路
二分查找是一种在有序数组中查找特定元素的算法,它通过不断将搜索区间减半来快速定位目标元素。
我们通过left和right来间接表示mid对应的元素
- 排除一些特殊情况,目标元素<数组首位元素(最小数) 或者 目标元素>数组末尾元素(最大数);
- 前提条件【while (left ? right)】,这决定区间开闭;
- 通过判断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) 如果使用暴力解法,即遍历数组元素,直到找到一个元素不小于目标元素,那么该元素对应的索引就是我们需要输出的结果。如果都没找到,则返回数组长度。
例如:数组[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 = 16 且 4 是一个整数。
示例 2:
输入:num = 14
输出:false
解释:返回 false ,因为 3.742 * 3.742 = 14 但 3.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 一定变小。这个单调变化的特点,使我们可以通过不断调整左右边界来寻找最短的连续子数组。
滑动窗口的基本思路是:
- 当窗口总和小于target的时候,说明窗口还不够大,右指针需要继续向右移动。
- 当窗口总和已经满足大于等于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中,找出一个最长连续的子数组,且只包含两个不同的水果类型【元素】。因为我们只能从某一棵树开始,一直向右连续采摘,不能跳过中间的树;同时又只有两个篮子,每个篮子只能装一种水果,所以最终问题就转化成了**“求最多包含两种不同元素的最长连续区间”**。
参考前面滑动窗口的思想,
- 我们可以维护一个窗口[left, right],表示正在考虑的一段连续果树。并且记录窗口内水果的种类数。
- 右边界移动时,说明正在尝试采摘新的果树,如果右边界加入一棵新树后,窗口中的水果种类变成了三种,就说明当前这段区间已经不合法了,记录此时可采摘水果数量【即窗口长度】。
- 移动左边界,把窗口最左边的水果逐个移出去,同时减少对应水果的计数。
- 如果某种水果的计数减少到 0,说明这种水果已经不在当前窗口里了,就可以从记录中删除它。我们持续移动左边界,直到窗口中重新只剩下两种水果为止。
- 更新可采摘水果数量,取最大值即可。
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);
}
}

170

被折叠的 条评论
为什么被折叠?



