算法训练——力扣
1、两数之和
描述:
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
暴力枚举
public static int[] twoSum1(int[] nums, int target) {
for (int i=0;i<nums.length;i++){
for (int j=i+1;j<nums.length;j++){
if(nums[i]+nums[j]==target){
return new int[]{i,j};
}
}
}
return new int[2];
}
采用哈希表
算法使用描述:
算法:哈希表,即map中的key为数组中的值,key对应的value对应值的下标
两数之和的核心就是在数组中找到tartet-x值的下标,x为数组中(从0-n)的取值
优化:用哈希的方法,这里使用集合key-value对应,将O(n^2)转为O(n)
public static int[] twoSum(int[] nums, int target) {
//构建map来存放数组值
HashMap<Integer, Integer> map = new HashMap<>();
for (int i=0;i<nums.length;i++){
//如果找到对应解直接返回,即target-nums[i]的key在map中有值
if (map.containsKey(target-nums[i])){
return new int[]{map.get(target-nums[i]),i};
}
//如果找不到对应解就将数组中值的下标放入集合中
map.put(nums[i],i);
}
return new int[0];
}
2、字母异位词分组
描述
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
题目翻译
将字符串中每个字母出现个数相同的字符串分为一类,最后输出每类字符串数组
解析
该题由于其特征是每个字符串中初相相同个数的字母作为一组,即分组条件唯一,所以可以用哈希表的结构进行处理,将每个字符产排序后的字符串作为哈希表中(map)唯一的key,value就为该key对应的集合,当遍历发现,key相同时就将该字符串调价到key对应的List集合中
代码
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
//将字符串排序后的的字符串作为唯一key
char[] chars = str.toCharArray();
//排序
Arrays.sort(chars);
String key = String.valueOf(chars);
List<String> list = map.getOrDefault(key, new ArrayList<>());
//将该字符串加入集合
list.add(str);
//将key对应的添加字符串后的集合list放入到map中
map.put(key,list);
}
//遍历完之后将map中所有的value进行返回
//map.values()返回的是Collection类型,要转为ArrayList类型需要new ArrayList<>(map.values());
ArrayList<List<String>> lists = new ArrayList<>(map.values());
return lists;
}
语法注意:
1、map中的方法:map.getOrDefault(key,default)表示:如果集合中由该key那么返回key对应的value值,如果没有就返回自定义的默认值default
如:
List<String> list = map.getOrDefault(key, new ArrayList<>());
2、是Collection类型转为ArrayList
map.values()返回的是Collection类型,要转为ArrayList类型需要new ArrayList<>(map.values());
如:
ArrayList<List<String>> lists = new ArrayList<>(map.values());
3、foreach遍历基本类型变量的集合数组时不会改变原数组/集合中的值,当遍历的是对象时可以改变数组中对象的值
3、最长连续序列
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
方法一:先排序,再从hsah表中找到最长序列
public static int longestConsecutive(int[] nums) {
//将数组排序
Arrays.sort(nums);
//用于保存最大值
int max=0;
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
int currentValue=0;
if(map.containsKey(num-1)){
currentValue=map.get(num-1)+1;
//如果当前序列数值大于最大值,则更换最大值
// max=currentValue>max?currentValue:max;
map.put(num,map.get(num-1)+1);
//如果前一个数不为空,即连续
//将该数的序列置为1
}else{
//不连续
//序列数置为1
map.put(num,1);
currentValue=1;
}
//如果当前序列数值大于最大值,则更换最大值
max=currentValue>max?currentValue:max;
}
return max;
}
方法二:找到序列中的起始位置
思想:找到序列中的起始位置然后倒推回去获得该段序列的长度
具体为:
如果·有小的就让小的来处理知道找不到连续的小的,此时就可以计算出该段的区间长度
如果该数没有连续的比它小的数,那么将该数作为连续区间的右区间
代码:
public static int longestConsecutive1(int[] nums) {
HashSet<Integer> set = new HashSet<>();
//将所有数出入集合
for (int num : nums) {
set.add(num);
}
int max=0;
//找出最大长度
//如果·有小的就让小的来处理知道找不到连续的小的,此时就可以计算出该段的区间长度
for (int l : nums) {
//有小的就让小的来处理
if(set.contains(l-1)) continue;
//如果该数没有连续的比它小的数,那么将该数作为连续区间的右区间
int r=l;
//再r的基础+1循环得到连续区间的长度
while (set.contains(r+1)) r++;
max=Math.max(max,r-l+1);
}
return max;
}
5 盛最多水的容器
p11
题目描述:
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
**说明:**你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
解析
在每个状态下,无论长板或短板向中间收窄一格,都会导致水槽 底边宽度 −1-1−1 变短:
若向内 移动短板 ,水槽的短板 min(h[i],h[j])min(h[i], h[j])min(h[i],h[j]) 可能变大,因此下个水槽的面积 可能增大 。
若向内 移动长板 ,水槽的短板 min(h[i],h[j])min(h[i], h[j])min(h[i],h[j]) 不变或变小,因此下个水槽的面积 一定变小 。
因此,初始化双指针分列水槽左右两端,循环每轮将短板向内移动一格,并更新面积最大值,直到两指针相遇时跳出;即可获得最大面积。
相当于最大值只能在遍历数组时候移动短板的过程中获得
代码
public static int maxArea(int[] height) {
int l=0;//左指针
int r=height.length-1;//右指针
int max=0;
for (;l<=r;){
int contain=(r-l)*Math.min(height[l],height[r]);
max=contain>max?contain:max;
if(height[l]<height[r]) l++;
else r--;
}
return max;
}
7、三数之和
题号、15
题目描述
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
解析
主要思想分为如下几步:
-
1、先将数组进行排序
-
2、固定第一个数(三个数中最小的)num[i],再使用左右指针指向Num[i]之后的数的两端,数字分别为nums[l],nums[r],如果三个数和为0的时候将结果对应的数放入集合
-
3、和sum==0,找到解的同时需要进行去重处理,即由于数组是排序处理过的,则如果l后面紧跟的还有与l相同的数,则这些数和l指针指向的数等价,同理r指针也进行同样的处理,即
-
while (l<r&&nums[l]==nums[l+1]) l++; while (l<r&&nums[r]==nums[r-1]) r--;
-
-
4、sum>0,说明整体偏大,这时只有通过将r端的指针向左移动才可能使sum==0;
-
5、sum<0,说明整体偏大,这时只有通过将左端的指针向右移动(l+1)才可能使sum==0;
代码如下
public static List<List<Integer>> threeSum(int[] nums) {
ArrayList<List<Integer>> lists = new ArrayList<>();
//先判断数组的合理性
if(nums==null||nums.length<3) return lists;
//将数组先进行排序---用于条件判断来跳出循环,减少时间
Arrays.sort(nums);
//先寻找可能的答案再进行去重
for (int i=0;i<nums.length;i++){
//当第一个元素都大于0时直接返回空集合
if (nums[i]>0) return lists;
//if
if(i>0&&nums[i-1]==nums[i]) continue;;
int l=i+1;
int r=nums.length-1;
while (l<r){
int sum=nums[l]+nums[r]+nums[i];
//找到答案
if(sum==0){
List<Integer> list = Arrays.asList(nums[i], nums[l], nums[r]);
lists.add(list);
//去重,找到一组解之后将前面与l位置,后面与r位置相同的元素过滤
while (l<r&&nums[l]==nums[l+1]) l++;
while (l<r&&nums[r]==nums[r-1]) r--;
//此时最左边没有与l相同的元素,左右边没有与R相同的元素,则可以将范围同时缩小1
l++;
r--;
}else if(sum>0){
r--;
} else if (sum<0) {
l++;
}
}
}
return lists;
}
6、接雨水
p42
题目描述:
困难
4.7K
相关企业
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 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 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
解析:
重点解法:将每一列单独拿出来处理
整体可以分为:当前列、当前列为分解的左边最高墙,以当前列为分解的右边最高墙
总共有三种情况
- 1、当前列高度大于 左边最高墙和右边最高强的最矮(木桶装水原理),不积水
- 2、当前列高度等于 左边最高墙和右边最高强的最矮,不积水
- 3、当前列高度小于 左边最高墙和右边最高强的最矮,该列积水的多少为最高的最矮值减去该列的高度
详细解析可参考:42. 接雨水 - 力扣(LeetCode)
代码
解法1
public static int trap1(int[] height) {
int sum = 0;
//算法思想:
//将问题分为当前列,以当前列为分界,找出左边和右边最高的墙,
//再比较出左边右边最高墙中的最矮的墙H
//若当前列高度大于等于H时不会积水
//若当前列高度小于H时积水多少就为H的高度减去当前列的高度
for (int i = 1; i < height.length - 1; i++) {//i表示当前列,当当前列为第一列或最后一列时显然不可能积水,因为不能分为左右两个边界,不能构成木桶形状
//寻找以当前列为分解的左边最大
int maxLeft = 0;
int maxRight = 0;
//左边最大
for (int j = i - 1; j >= 0; j--) maxLeft = maxLeft > height[j] ? maxLeft : height[j];
//右边最大
for (int j = i + 1; j < height.length; j++) maxRight = maxRight > height[j] ? maxRight : height[j];
//取最大中的最小
int minInMax = Math.min(maxLeft, maxRight);
if (height[i] < minInMax) sum += minInMax - height[i];
}
return sum;
}
解法二:优化
优化:将以当前为分解时左边和右边每个位置对应的最高墙的高度直接储存在数组中,要用的时候直接调用即可,
相当于将for里面的循环提到外面,就将O(n^2)降到O(n)
public static int trap(int[] height) {
int sum = 0;
//建立左边以当前列为分界的最高墙组成的数组
int[] leftMax = new int[height.length];
int[] rightMax = new int[height.length];
//以下i都表示当前列的位置,显然当前列到第一列或者最后一列没有意义因为构不成木桶
for (int i = 1; i < height.length-1; i++) {
leftMax[i]= Math.max(height[i-1],leftMax[i-1]);
}
for (int i = height.length - 2; i >= 0; i--) {
rightMax[i]=Math.max(height[i+1],rightMax[i+1]);
}
for (int i = 1; i < height.length - 1; i++) {
int minInMax = Math.min(leftMax[i], rightMax[i]);
if (height[i] < minInMax) sum += minInMax - height[i];
}
return sum;
}
解法三:继续优化
继续优化:由上面的缓存当前列所分割的左右最大值数组可得
在获取左边的数组时,是从1–>n-1而在累加积水时也是从1–>n-1,因此可以将获取左边数组的循环内容直接放入累加积水的循环中
public static int trap3(int[] height) {
int sum = 0;
//建立左边以当前列为分界的最高墙组成的数组
int[] leftMax = new int[height.length];
int[] rightMax = new int[height.length];
//以下i都表示当前列的位置,显然当前列到第一列或者最后一列没有意义因为构不成木桶
for (int i = 1; i < height.length-1; i++) {
}
for (int i = height.length - 2; i >= 0; i--) {
rightMax[i]=Math.max(height[i+1],rightMax[i+1]);
}
for (int i = 1; i < height.length - 1; i++) {
//将建立左边数组的缓存放入积水循环中
leftMax[i]= Math.max(height[i-1],leftMax[i-1]);
int minInMax = Math.min(leftMax[i], rightMax[i]);
if (height[i] < minInMax) sum += minInMax - height[i];
}
return sum;
}
以下i都表示当前列的位置,显然当前列到第一列或者最后一列没有意义因为构不成木桶
for (int i = 1; i < height.length-1; i++) {
}
for (int i = height.length - 2; i >= 0; i--) {
rightMax[i]=Math.max(height[i+1],rightMax[i+1]);
}
for (int i = 1; i < height.length - 1; i++) {
//将建立左边数组的缓存放入积水循环中
leftMax[i]= Math.max(height[i-1],leftMax[i-1]);
int minInMax = Math.min(leftMax[i], rightMax[i]);
if (height[i] < minInMax) sum += minInMax - height[i];
}
return sum;
}
## 8 最长不重复字串
p 2
### 描述
[无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/)
给定一个字符串 `s` ,请你找出其中不含有重复字符的 **最长子串** 的长度。
**示例 1:**
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
**示例 2:**
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
**示例 3:**
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
### 解析:
#### 方法一:
- 1、首先,判断当前字符是否包含在map中,如果不包含,将该字符添加到map(字符,字符在数组下标),此时没有出现重复的字符,左指针不需要变化。此时不重复子串的长度为:i-left+1,与原来的maxLen比较,取最大值;
- 2、如果当前字符 ch 包含在 map中,此时有2类情况:
- 1)当前字符包含在当前有效的子段中,如:abca,当我们遍历到第二个a,当前有效最长子段是 abc,我们又遍历到a,
那么此时更新 left 为 map.get(a)+1=1,当前有效子段更新为 bca;
- 2)当前字符不包含在当前最长有效子段中(即现在当前字符在map中且当前字符在map中的的字符是在滑动窗口最左边left的左边,即不包含在有效字段中),
- ```
如:abba,我们先添加a,b进map,此时left=0,我们再添加b,发现map中包含b,且b包含在最长有效子段中,此时left=map(b)+1,即left=2,更新 left=2,此时子段更新为 b,而且map中仍然包含a,map.get(a)=0;随后,我们遍历到a,发现a包含在map中,且map.get(a)=0,如果我们像1)一样处理(left=map.get(a)+1,就会发现 left1,实际上,left此时 此时会发现left还变小了往左边移动了,显然不对,这种情况,我们应该保持left=原来的值不变
```
-
==为了处理以上2类情况,我们每次更新left,left=Math.max(left , map.get(ch)+1).另外,更新left后,不管原来的 s.charAt(i) 是否在最长子段中,我们都要将 s.charAt(i) 的位置更新为当前的i, 因此此时新的 s.charAt(i) 已经进入到 当前最长的子段中!==
注意此方法是通过计算窗口的左窗口来获取的到岸
#### 代码
```java
public static int lengthOfLongestSubstring(String s) {
int maxlen=0;
int left=0;
HashMap<Character, Integer> map = new HashMap<>();
for(int i=0;i<s.length();i++){
//如果包含当前key
if (map.containsKey(s.charAt(i))){
//abba
//left应该取得原来左边窗口的下标值和当前重复key在移动窗口中的Value+1取最大值
//因为用map来储存字符对应的下标时并没有将窗口左边的并没有将其删去,所以需要获得最大值
//比如:在扫描到最后一个字符a的时候,此时left的值为2而扫描到a的时候如果继续按照left=s.charAt(i))+1来处理
//就会发现窗口的左边left就变成了1从而发生错误
left=Math.max(map.get(s.charAt(i))+1,left);
if (maxlen<i-left+1) maxlen=i-left+1;
}
//不管是否包含都将当前key的value更新
map.put(s.charAt(i),i);
maxlen=maxlen>(i-left+1)?maxlen:(i-left+1);
}
return maxlen;
}
方法二:
遍历每个元素将每个元素都作为滑动窗口的左窗口,依次获取每个元素作为左窗口时的滑动窗口的长度,最后获取最大值
代码
int maxlen=0;
//定义右窗口为-1目的是可以将所有字符都添加到结合中作为移动窗口的元素
int r=-1;
HashSet<Character> set = new HashSet<>();
for(int i=0;i<s.length();i++){
//保持左窗口永远为0
if(i!=0){
//i相当于左窗口
//移除窗口左边的数
set.remove(s.charAt(i-1));
}
//寻找以i为下标的字符为窗口左边时的窗口长度
//当前字符不在窗口内则右窗口向右移动
while (r+1<s.length()&&!set.contains(s.charAt(r+1))){
//并将右窗口指向的字符加入集合
set.add(s.charAt(r+1));
//右窗口++
r++;
}
maxlen=Math.max(maxlen,r-i+1);
}
return maxlen;
}
8 最长不重复字串
p 2
描述
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
解析:
方法一:
-
1、首先,判断当前字符是否包含在map中,如果不包含,将该字符添加到map(字符,字符在数组下标),此时没有出现重复的字符,左指针不需要变化。此时不重复子串的长度为:i-left+1,与原来的maxLen比较,取最大值;
-
2、如果当前字符 ch 包含在 map中,此时有2类情况:
-
1)当前字符包含在当前有效的子段中,如:abca,当我们遍历到第二个a,当前有效最长子段是 abc,我们又遍历到a,
那么此时更新 left 为 map.get(a)+1=1,当前有效子段更新为 bca; -
2)当前字符不包含在当前最长有效子段中(即现在当前字符在map中且当前字符在map中的的字符是在滑动窗口最左边left的左边,即不包含在有效字段中),
-
如:abba,我们先添加a,b进map,此时left=0,我们再添加b,发现map中包含b,且b包含在最长有效子段中,此时left=map(b)+1,即left=2,更新 left=2,此时子段更新为 b,而且map中仍然包含a,map.get(a)=0;随后,我们遍历到a,发现a包含在map中,且map.get(a)=0,如果我们像1)一样处理(left=map.get(a)+1,就会发现 left1,实际上,left此时 此时会发现left还变小了往左边移动了,显然不对,这种情况,我们应该保持left=原来的值不变
-
-
为了处理以上2类情况,我们每次更新left,left=Math.max(left , map.get(ch)+1).另外,更新left后,不管原来的 s.charAt(i) 是否在最长子段中,我们都要将 s.charAt(i) 的位置更新为当前的i, 因此此时新的 s.charAt(i) 已经进入到 当前最长的子段中!
注意此方法是通过计算窗口的左窗口来获取的到岸
代码
public static int lengthOfLongestSubstring(String s) {
int maxlen=0;
int left=0;
HashMap<Character, Integer> map = new HashMap<>();
for(int i=0;i<s.length();i++){
//如果包含当前key
if (map.containsKey(s.charAt(i))){
//abba
//left应该取得原来左边窗口的下标值和当前重复key在移动窗口中的Value+1取最大值
//因为用map来储存字符对应的下标时并没有将窗口左边的并没有将其删去,所以需要获得最大值
//比如:在扫描到最后一个字符a的时候,此时left的值为2而扫描到a的时候如果继续按照left=s.charAt(i))+1来处理
//就会发现窗口的左边left就变成了1从而发生错误
left=Math.max(map.get(s.charAt(i))+1,left);
if (maxlen<i-left+1) maxlen=i-left+1;
}
//不管是否包含都将当前key的value更新
map.put(s.charAt(i),i);
maxlen=maxlen>(i-left+1)?maxlen:(i-left+1);
}
return maxlen;
}
方法二:
遍历每个元素将每个元素都作为滑动窗口的左窗口,依次获取每个元素作为左窗口时的滑动窗口的长度,最后获取最大值
代码
int maxlen=0;
//定义右窗口为-1目的是可以将所有字符都添加到结合中作为移动窗口的元素
int r=-1;
HashSet<Character> set = new HashSet<>();
for(int i=0;i<s.length();i++){
//保持左窗口永远为0
if(i!=0){
//i相当于左窗口
//移除窗口左边的数
set.remove(s.charAt(i-1));
}
//寻找以i为下标的字符为窗口左边时的窗口长度
//当前字符不在窗口内则右窗口向右移动
while (r+1<s.length()&&!set.contains(s.charAt(r+1))){
//并将右窗口指向的字符加入集合
set.add(s.charAt(r+1));
//右窗口++
r++;
}
maxlen=Math.max(maxlen,r-i+1);
}
return maxlen;
}
9 和为K的子数组
题目描述
提示
中等
2.1K
相关企业
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的连续子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
解析
关于mp.put(0, 1); 这一行的作用就是为了应对 nums[0] +nums[1] + … + nums[i] == k 的情况的, 也就是从下标 0 累加到下标 i, 举个例子说明, 如数组 [1, 2, 3, 6], 那么这个数组的累加和数组为 [1, 3, 6, 12] 如果 k = 6, 假如map中没有预先 put 一个 (0, 1) , 如果此时我们来到了累加和为 6 的位置, 这时map中的情况是 (1, 1), (3, 1), 而 mp.containsKey(pre - k) , 这时 pre - k 也就是 6 - 6 = 0, 因为 map 中没有 (0, 1) 所以 count 的值没有加一, 其实这个时候我们就是忽略了从下标 0 累加到下标 i 等于 k 的情况, 我们仅仅是统计了从下标大于 0 到某个位置等于 k 的所有答案,
至于为什么是 count += mp.get(pre - k); 呢 ? 举个例子: k = 6, 数组 [1, 2, 3, 0, 6] 累加和为: [1, 3, 6, 6, 12], 明显答案应该是 4, 当我们来到第一个累加和为 6 的位置上时, pre - k = 0, 也就是说从下标 0 到当前位置的累加和是一个答案, 当来到第二个 6 的位置上时, 也就是说从下标 0 到当前位置的累加和是一个答案, 而当来到 12 位置上时, pre - k = 6, 也就是说从累加和为 6 的子数组的后一个位置到当前位置也是满足条件的答案, 而累加和为 6 的子数组只有一个吗 ? 不 ! 这个例子中他有两个, 所以 count 是 加 mp.get(pre - k);, 而不是加 1,
如果说 mp.put(0, 1); 不好理解, 那么我们也可以换一种思路, 这个东西不就是为了统计从下标 0 到下标 i 累加和刚好等于 k 吗, 那我们可以在累加和刚好等于 k 的时候直接给count + 1, 剩下的操作该怎么样还怎么样, 附上代码
代码
public static int subarraySum1(int[] nums, int k) {
int count=0;
for(int i=0;i<nums.length;i++){
int befSum=0;
befSum+=nums[i];
if(befSum==k){
count++;
}
for(int j=i+1;j<nums.length;j++){
befSum+=nums[j];
if(befSum==k){
count++;
}
}
}
return count;
}
/**
*
* @param nums
* @param k
* @return
*/
public static int subarraySum(int[] nums, int k) {
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0,1);
int count=0;
int pre=0;
for (int i = 0; i < nums.length; i++) {
pre+=nums[i];
if(map.containsKey(pre-k)){
count+=map.get(pre-k);
}
map.put(pre,map.getOrDefault(pre,0)+1);
}
return count;
}
10 最小覆盖字串
题目描述
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 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
由英文字母组成
解析
题解
滑动窗口的思想:
用i,j表示滑动窗口的左边界和右边界,通过改变i,j来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走,当这个窗口包含的元素满足条件,即包含字符串T的所有元素,记录下这个滑动窗口的长度j-i+1,这些长度中的最小值就是要求的结果。
步骤一
不断增加j使滑动窗口增大,直到窗口包含了T的所有元素
步骤二
不断增加i使滑动窗口缩小,因为是要求最小字串,所以将不必要的元素排除在外,使长度减小,直到碰到一个必须包含的元素,这个时候不能再扔了,再扔就不满足条件了,记录此时滑动窗口的长度,并保存最小值
步骤三
让i再增加一个位置,这个时候滑动窗口肯定不满足条件了,那么继续从步骤一开始执行,寻找新的满足条件的滑动窗口,如此反复,直到j超出了字符串S范围。
面临的问题:
如何判断滑动窗口包含了T的所有元素? 我们用一个字典need来表示当前滑动窗口中需要的各元素的数量,一开始滑动窗口为空,用T中各元素来初始化这个need,当滑动窗口扩展或者收缩的时候,去维护这个need字典,例如当滑动窗口包含某个元素,我们就让need中这个元素的数量减1,代表所需元素减少了1个;当滑动窗口移除某个元素,就让need中这个元素的数量加1。 记住一点:need始终记录着当前滑动窗口下,我们还需要的元素数量,我们在改变i,j时,需同步维护need。 值得注意的是,只要某个元素包含在滑动窗口中,我们就会在need中存储这个元素的数量,如果某个元素存储的是负数代表这个元素是多余的。比如当need等于{‘A’:-2,‘C’:1}时,表示当前滑动窗口中,我们有2个A是多余的,同时还需要1个C。这么做的目的就是为了步骤二中,排除不必要的元素,数量为负的就是不必要的元素,而数量为0表示刚刚好。 回到问题中来,那么如何判断滑动窗口包含了T的所有元素?结论就是当need中所有元素的数量都小于等于0时,表示当前滑动窗口不再需要任何元素。 优化 如果每次判断滑动窗口是否包含了T的所有元素,都去遍历need看是否所有元素数量都小于等于0,这个会耗费O(k)O(k)O(k)的时间复杂度,k代表字典长度,最坏情况下,k可能等于len(S)。 其实这个是可以避免的,我们可以维护一个额外的变量needCnt来记录所需元素的总数量,当我们碰到一个所需元素c,不仅need[c]的数量减少1,同时needCnt也要减少1,这样我们通过needCnt就可以知道是否满足条件,而无需遍历字典了。 前面也提到过,need记录了遍历到的所有元素,而只有need[c]>0大于,代表c
就是所需元素
注意
其中移动做窗口有两种情况进行操作
-
1、当前左窗口所代表的字符在目标串中
-
a.更新串口中字符的个数
map[s.charAt(l)]++;
-
b.更新窗口中目标串字符的长度
len++;
-
c.继续将窗口左移动
i++;
-
-
2、当前左窗口所指向的字符不再目标串中
-
a.更新串口中字符的个数
map[s.charAt(l)]++;
-
c.继续将窗口左移动
i++;
-
移动右窗口
-
1右窗口所指向的字符在目标串中
将右窗口右移即可
-
2、右窗口所指向的字符在目标串中,将目标串在窗口中的字符个数减1
char c = s.charAt(r); if (map[c] > 0) { //如果当前右窗口值在目标串中 len--; }
当前字符在不在目标串中最后都要将r++表示窗口右移
代码
public static String minWindow(String s, String t) {
int[] map = new int[128];
for (int i = 0; i < t.length(); i++) {
//初始化窗口中的值,记录每个字符出现的个数
map[t.charAt(i)]++;
}
//
int size = Integer.MAX_VALUE, l = 0, r = 0, len = t.length(), start = 0;
while (r < s.length()) {
//当前右窗口的值
char c = s.charAt(r);
if (map[c] > 0) {
//如果当前右窗口值在目标串中
len--;
}
//将存储的字符个数减一
map[c]--;
if (len == 0) {
//如果匹配到一组解
//缩小窗口,即左窗口右移
//map[s.charAt(l)]<0表示当前左窗口的值不再目标窗口剩余的字符中,直接缩小窗口即可
while (l < r && map[s.charAt(l)] < 0) {
//目标窗口中增加刚移除的字符
map[s.charAt(l)]++;
l++;
}
//程序走到这里说明,左窗口现在的值在目标窗口中
if (size > r - l + 1) {
size = r - l + 1;
start = l;
}
//将左窗口右移
map[s.charAt(l)]++;
len++;
l++;
}
r++;
}
return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
}
11 最大子数组之和
题目描述
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
解析
要求数组中最大子数组之和,只需要判断当前位置的前一个位置是否大于0
如果大于0那么将当前位置上的值改为当前为值+前一个位置的值,即
if(nums[i-1]>0){
nums[i]+=nums[i-1];
}
如果前一个位置不大于0,那么当前位置的值就不需要改变
这样做能保证数组中每个位置的值都是以该位置结尾的和最大的子数组和
代码如下
public static int maxSubArray(int[] nums) {
int maxV=nums[0];
for (int i=1;i<nums.length;i++){
if (nums[i-1]>0){
//如果前一个位置的值大于0,那么当前位置的值为当前位置的值加前一个位置的值
nums[i]+=nums[i-1];
}
//如果之前的值小于0那么抛弃前面数组的值(即不做任何操作),当前位置上的最大值为自己
//检查更新最大值
if (maxV<nums[i]){
maxV=nums[i];
}
}
return maxV;
}
12 合并区间
题目描述
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
1 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 104
解析
1、先将区间按照起点先后进行排序,再按照区间终点大小进行排序
2、排序之后,创建一个集合用于储存答案(之前的想法是在原数组上进行区间的合并,但发现对数组不好进行操作,因为在合并区间后,后一个区间需要删除,如果只是将后一个区间做一个标记表示已被删除,但是后一个区间的合并也会受到影响)所以不如重新创建一个新的集合用来储存答案,每次区间的合并对比用储存答案的集合和当先数组进行对比即可)
代码
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length < 1) {
return new int[0][2];
}
//根据起点大小进行排序
Comparator<int[]> cmp = new Comparator<>() {
@Override
public int compare(int[] o1, int[] o2) {
//如果起点相同那么按照终点大小顺序排列
return o1[0] == o2[0] ? o1[1] - o2[1] : o1[0] - o2[0];
}
};
Arrays.sort(intervals, cmp);
ArrayList<int[]> list = new ArrayList<>();
for (int i = 0; i < intervals.length; i++) {
int R = intervals[i][1];
int L = intervals[i][0];
if (list.size() == 0 || list.get(list.size() - 1)[1] < L) {
//将该时间段放入答案集合中
list.add(new int[]{L, R});
} else {
//另外两种情况将其合并集合,即后一个区间在前一个区间中或者前一个区间的右边>后一个区间左边(有交集)
list.get(list.size() - 1)[1] = list.get(list.size() - 1)[1] > R ? list.get(list.size() - 1)[1] : R;
}
}
return list.toArray(new int[list.size()][2]);
}
13 缺失的第一个正数
题目描述
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解析
方法一:
采用哈希思想:
在原数组的基础上改造哈希,我们要从数组中遍历得到最下缺失的正整数,即答案一定在数组长度内的整数或者数组长度+1的这里面产生答案,我们可以将在数组长度内的数x,将其数组下标x-1标记为负数,代表这个位置代表的数已经出现过了,最后再一次遍历数组,如果数组中还有整数,那么返回第一个整数的下标+1就是缺失的最小正整数,如果整个数组都被标记了,那么缺失的就是1数组长度+1的值
具体步骤:
- 先将负数变为数组的长度+1
- 遍历数组,将在数组长度内出现的数字,将其n-1下标上的数变为负数作为标记
- 遍历最后数组,找到数组长度内下标索引没有被标记过的数(即正数),若全为负数则表示最后答案为长度加一
代码
public static int firstMissingPositive(int[] nums) {
for (int i = 0; i < nums.length; i++) {
//先将负数变为数组的长度+1
nums[i]=nums[i]<=0?nums.length+1:nums[i];
}
//遍历数组,将在数组长度内出现的数字,将其n-1下标上的数变为负数作为标记
for (int i = 0; i < nums.length; i++) {
int absValus=Math.abs(nums[i]);
if (absValus<=nums.length){
//该下标对应值置为负数,做标记,absValus-1是为了将0号下标应用起来,使得原数组空间刚好够用
nums[absValus-1]=nums[absValus-1]>0?-nums[absValus-1]:nums[absValus-1];
}
}
//遍历最后数组,找到数组长度内下标索引没有被标记过的数(即正数),若全为负数则表示最后答案为长度加一
for (int i = 0; i < nums.length; i++) {
if (nums[i]>0){
return i+1;
}
}
return nums.length+1;
}
方法二:
置换法:即将数组中的元素放在正确的位置,即将在nums[i]数组长度内的数放在nums[i]-1的位置上,将两个位置上的数进行交换
需要注意的是,而交换的位置nums[i]-1位置上的值也可能在1~nums.length中,因此也需要对该位置进行处理,即交换i位置就需要一直交换到i位置上的数不再可以交换为止,若要交换的两个数刚好相等那么就好做成死循环,因此在判断时我们应该将其跳开即可
步骤:
- 1、将值在数组长度之内的值交换在正确的位置
- 2、遍历数组如果i下标对应的值不是i+1,那么说明i+1就是为最小缺的正整数,如果全部对应,那么返回值就是数组长度+1
代码
//1、将值在数组长度之内的值交换在正确的位置
for (int i = 0; i < nums.length; i++) {
while (nums[i]>0&&nums[i]<=nums.length&&nums[i]!=nums[nums[i]-1]){
int temp=nums[nums[i]-1];
nums[nums[i]-1]=nums[i];
nums[i]=temp;
}
}
//2、遍历数组如果i下标对应的值不是i+1,那么说明i+1就是为最小缺的正整数,如果全部对应,那么返回值就是数组长度+1
for (int i = 0; i < nums.length; i++) {
if (nums[i]!=i+1) return i+1;
}
return nums.length+1;
}
14 除自身以外数组的乘积
题目描述
p238
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请**不要使用除法,**且在 O(*n*)
时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
提示:
2 <= nums.length <= 105
-30 <= nums[i] <= 30
- 保证 数组
nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内
解析
方法一:
开辟一个数组来储存答案,先遍历数组,将算出整个数组除了0以外的乘积,同时统计出数组中出现0位置的下标,
当出现不止一个0的时候那么返回全是0的数组,
当只出现1个0时那么只有0位置的下标值不为0其他的值全为没有0的时候当前下标的值等于总的值除以原数组中该下标对应的值
代码
int arr[]=new int[nums.length];
int count=0;
int index=0;//记录有一个0的时候0的下标
int sum=0;
//求总的乘积和0的个数,以及0的下标,
for (int i = 0; i < nums.length; i++) {
if (nums[i]==0) {
count++;
index=i;
continue;
}else{
sum=sum==0?nums[i]:sum*nums[i];
}
}
if(count>=2) return arr;
//这里的index只有数组中只有1个0的时候才生效
if (count==1){
arr[index]=sum;
}else {
for (int i = 0; i < nums.length; i++) {
arr[i]=sum/nums[i];
}
}
return arr;
方法二
用前缀、后缀乘积来处理
即:当前位置的值**=**当前位置之前的乘积来乘以当前位置之后的乘积
只需要算出每个位置前的乘积的到一个L数组
和一个当前位置之后的乘积即可,
只需要注意第一个位置的前一个数的值应该为1,L[0]=1,因为是乘法
最后一个位置的后一个数的值应该为1,R[nums.length-1]=1
代码
public static int[] productExceptSelf1(int[] nums) {
int[] L=new int[nums.length];
int[] R=new int[nums.length];
int[] ans=new int[nums.length];
ans[0]=1;
L[0]=1;
//计算i位置之前的乘积
for (int i = 1; i < nums.length; i++) {
L[i]=L[i-1]*nums[i-1];
}
R[nums.length-1]=1;
for (int i = nums.length-2; i >=0 ; i--) {
R[i]=R[i+1]*nums[i+1];
}
//计算总的乘积
for (int i = 0; i < nums.length; i++) {
ans[i]=L[i]*R[i];
}
return ans;
}
方法三
优化方法二:
将L变为最终放答案的地方,i右边的数在循环中来解决,减少R的空间开辟
方法二:用前缀、后缀乘积来处理
当前位置的值**=**当前位置之前的乘积来乘以当前位置之后的乘积
代码
int[] ans=new int[nums.length];
ans[0]=1;
ans[0]=1;
//计算i位置之前的乘积
for (int i = 1; i < nums.length; i++) {
ans[i]=ans[i-1]*nums[i-1];
}
//定义当i为最右边时,他的右边数值为1
int R=1;
//计算总的乘积
for (int i = nums.length-1; i>=0; i--) {
ans[i]=ans[i]*R;
R*=nums[i];
}
return ans;
}