Hot 100总结(逐行注释版)
哈希篇
1.两数之和
用哈希表优化
class Solution {
public int[] twoSum(int[] nums, int target) {
// new一个哈希表,键为数组值,值为索引
Map<Integer,Integer> hs=new HashMap();
for(int i=0;i<nums.length;i++){
// 相当与在数组里找到一个数是目标值-第一个数
int dou=target-nums[i];
// 如果哈希表里有这个数直接return结果
if(hs.containsKey(dou)){
return new int[]{i,hs.get(dou)};
}
// 如果没有就把自己加进去(这样可以再判断是否有数的时候避免用自己)
// (这样可以再判断是否有数的时候避免用自己,相当于先判断有没有再加自己进去)
hs.put(nums[i], i);
}
// 题目要求走不到这里,随便return
return new int[]{0,0};
}
}
2.字母异位词分组
因为abc,cba经过排序后都是abc,就可以看排序后是否相同分组,用哈希表优化
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// new一个哈希表,键是排序后的字符串,值为所有排序为键str的字符串集合,也就是一个分组
Map<String,List<String>> map=new HashMap();
// 处理每一个字符串
for(String s:strs){
// 转成字符数组排序
char[] ss=s.toCharArray();
Arrays.sort(ss);
// 将排序后的数组转为字符串得到键
String str=new String(ss);
// 如果map包含这个键
if(map.containsKey(str)){
// 就拿到这个键对应的值集合,把当前字符串加进去
map.get(str).add(s);
// 这里记得要加continue跳过本次循环
continue;
}
// 如果没有这个键就new一个集合,相当于新建一个分组
List<String> re=new ArrayList();
re.add(s);
// 连键加值一起放到map里面
map.put(str,re);
}
// 结果就是map的值集合
return new ArrayList<>(map.values());
}
}
当然如果熟练使用api可以简化代码
可以用map的computeIfAbsent一行搞定
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> m = new HashMap<>();
for (String str : strs) {
char[] s = str.toCharArray();
Arrays.sort(s);
// s 相同的字符串分到同一组
m.computeIfAbsent(new String(s), k -> new ArrayList<>()).add(str);
}
return new ArrayList<>(m.values());
}
}
3.最长连续序列
class Solution {
public int longestConsecutive(int[] nums) {
int res = 0;
// 要求时间复杂度O(n),所以用哈希优化
Set<Integer> set = new HashSet();
// 先将所有元素都加入set集合
for (int a : nums) {
set.add(a);
}
// 对set里面的一个元素x来说,如果它作为起点,则x-1一定不在set里面
for (int x : set) {
// 初始化长度为1,即只包含一个元素
int cnt = 1;
if (set.contains(x - 1)) {
// 如果里面包含x-1则不讨论
continue;
}
// 从x的下一个数x+1开始
int y = x + 1;
// 如果里面包含y
while (set.contains(y)) {
// 再看有没有y的下一个元素,且cnt加1以此类推
y++;
cnt++;
}
res = Math.max(res, cnt);
}
return res;
}
}
双指针篇
4.移动零
class Solution {
public void moveZeroes(int[] nums) {
// 思路:双指针,找到第一个不是0的元素交换
int n = nums.length;
// 慢指针
// j要从i开始往后面扫,每扫到一个不为0的数就停下交换,这个时候就能把i+1
// 并且交换过来的一定是不为0的数,i如果不等于j则i和j之间一定是0
for (int i = 0, j = i; i < n && j < n; j++) {
if (nums[j] != 0) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
i++;
}
}
}
}
5.盛水最多的容器
双指针
class Solution {
public int maxArea(int[] height) {
// 相向双指针
int i = 0, j = height.length - 1;
int res = 0;
while (i < j) {
// 盛水多少主要看矮的那一边
// 如果右边矮,那么右边的柱子左移可能变更矮也可能变高,变高的话相当于容量可以变大
// 相反,如果移动高的柱子的话,因为容量与高的柱子无关,无论高的柱子怎么变化因为横坐标减少了容量一定减少
// 所以我们要移动矮的那一边
if (height[i] >= height[j]) {
// 右边矮,先取当前最大容量,然后移动右边
res = Math.max(res, (j - i) * height[j]);
j--;
} else {
// 左边矮,先取当前最大容量,再移左边
res = Math.max(res, (j - i) * height[i]);
i++;
}
}
return res;
}
}
6.三数之和
方法:排序加双指针,要比哈希+两数之和快很多,面试就用排序加双指针
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 此思路需排序,让小的或者说负数在左边,大的或者说正数在右边
Arrays.sort(nums);
// 初始化一个结果list
List<List<Integer>> res = new ArrayList();
// 如果连三个元素都没有,直接return空集合
if (nums.length < 3) {
return res;
}
// 开始遍历,nums[i]是第一个数
for (int i = 0; i < nums.length; i++) {
// 因为题目要求不重复,所以如果下一个数和这个数相等,直接i++移到下个数,这里体现在continue;
if (i != 0 && nums[i] == nums[i - 1]) {
continue;
}
// 第一个数是nums[i];
// 双指针,左指针指向第一个数的右边那个数,右指针指向最右边那个数
int left = i + 1;
int right = nums.length - 1;
// 进入循环
while (left < right) {
// 三者的和为sum
// 如果sum>0则说明右边的数大了,需要左移
// 如果sum<0则说明左边的数小了,需要右移
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
left++;
} else if (sum > 0) {
right--;
} else { // sum=0说明存在三元组满足要求,此时要记住题目的不重复要求
// 判断left<right是防止left加high了不满足外循环条件
// 如果左指针右边那个数等于左指针指向的数,则可以右移左指针
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
// 如果右指针左边那个数等于右指针左边那个数,则可以左移左指针
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
// 此时能保证没有重复的
res.add(new ArrayList(Arrays.asList(nums[i], nums[left], nums[right])));
// 找到一个三元组,正常移动指针
left++;
right--;
}
}
}
return res;
}
}
本题还可以剪枝,即如果nums[0]>0则说明全是正数直接return,同理nums[n-1]<0,return
7.接雨水
本题有很种思路,先给出双指针思路,也是常见思路,后面再补充动态规划、栈等。
双指针:
class Solution {
public int trap(int[] height) {
int ans = 0;
int left = 0;
int right = height.length - 1;
// 左右两边最高的墙,保证这两座墙直接的凹处一定能接到水
int lmax = 0, rmax = 0;
while (left < right) {
// 每次移动后都要更新当前的两端最高柱子
lmax = Math.max(lmax, height[left]);
rmax = Math.max(rmax, height[right]);
// 以矮的那边为主,接水高度不会超过矮的那一边
// 这里有点像第5题
// 如果左边矮,则移动左边
if (lmax < rmax) {
// 如果移动后的柱子比最高柱子低,说明有凹处,且右边一定可以拦住,直接计算当前列的水位
ans += lmax - height[left];
// 计算完当前列后右移
left++;
} else {
// 同理
ans += rmax - height[right];
right--;
}
}
// 当left=right的时候说明所有列都已经计算完毕,得到最后结果
return ans;
}
}
滑动窗口篇
滑动窗口模板总结:滑动窗口模版总结
8.无重复字符的最长子串
class Solution {
public int lengthOfLongestSubstring(String s) {
// 判断重复,所用用数组哈希表
int[] hs = new int[128];
// 找最长,初始化为0;找最小,初始化为Integer.MAX_VALUE
int max = 0;
// 窗口模板:(i = 0, j = 0; j < s.length(); j++)
// 注意:在for循环内只拓展右边界,让更多元素进来,满足题目要求后收缩左边界。
for (int i = 0, j = 0; j < s.length(); j++) {
// 将右边界放进来的元素个数进行计数,char直接转成ASCII码
hs[s.charAt(j)]++;
// 因为不含重复字符,所以当放进来的元素已经在窗口内存在的时候,收缩左边界
// 收缩左边界,此时有两种情况:
// 1.左边界出去的元素和右边界进来的元素不一致,则继续收缩左边界
// 2.左边界出去的元素正好是右边界进来的元素,右边界元素个数降为1,退出while循环,继续j++拓展右边界
while (hs[s.charAt(j)] > 1) {
// 将左边界元素个数减一
hs[s.charAt(i)]--;
// 收缩左边界
i++;
}
// 每次都要对满足条件(退出while)的窗口的长度进行取较大值操作
// 窗口长度为j-i+1
max = Math.max(max, j - i + 1);
}
return max;
}
}
子串
9.和为 K 的子数组
刷完滑动窗口后这个题第一反应就是用滑动窗口,但实际上因为数组有正有负,无法满足滑动窗口所需要的单调性,所以无法使用,这里引用评论区大佬的一句话以及思路:
如果全部是负数也是可以的。全是正数的情况下滑动窗口主要是保证了右窗口入数据时,窗口内的和是增加的,左窗口出数据时,窗口内的和是减少的,这样就保证了单调性,这样我们就可以通过控制左右窗口在让窗口内的和接近目标值,也就是可以确保我们找都目标值。而如果数值有正有负的情况,单调性就被打破,入数据和不一定增加,出数据和也不一定减少,这样就无法人窗口内的和接近目标值了。
所以这题用前缀和加哈希表
思路:
和为k的子数组,看到子数组的和,首先想到的就是滑动窗口或者前缀和,但是因为这里的数据有负数,不满足滑动窗口的单调性,那么我们来看前缀和。
前缀和:p[i] = p[i-1]+nums[i] (i>0,p[0] = nums[0])
有了前缀和,我们可以求到所有的子数组的和。
比如:我们要求l到r区间的和
可以使用p[r]-p[l-1]求得。
了解这个原理后,我们在来看题目要求:和为k的数组的数目。
也就是p[r]-p[l-1] = k
p[l-1] = p[r] - k
替换l-1为i,r为j,(i<j)
p[i] = p[j] - k
得到这个式子,你有想法了吗
p[i]是p[j]前面的前缀和,那么我们可以使用一个哈希表来存储p[i],val为p[i]的数目,因为p[j]是之后的前缀和,所有我们可以去哈希表中去找有有没有满足p[j]-k
的之前的前缀和
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer,Integer> map=new HashMap();
// 这个地方很重要,一定要把键0的初始化为1
// 它的作用是处理那些从数组开头到某个位置的子数组和为k的情况。如果不进行这样的初始化,算法可能会遗漏这些重要的子数组。
map.put(0,1);
// 初始化答案
int cnt=0;
// 原地修改成前缀和数组
for(int i=0,j=1;j<nums.length;i++,j++){
nums[j]=nums[i]+nums[j];
}
// 对于满足要求的子数组就转换成了满足nums[i]-k
for(int i=0;i<nums.length;i++){
// 对于满足要求的子数组就转换成了满足nums[i]-k
int target=nums[i]-k;
// 下面就用两数之和的思路
if(map.containsKey(target)){
cnt+=map.get(target);
}
map.put(nums[i],map.getOrDefault(nums[i],0)+1);
}
return cnt;
}
}
有不用原地修改数组,边用边拿和的版本就不贴了
10.滑动窗口的最大值
这题需要用到双向队列(单调版),也就是单调队列,也就是说我们需要新建一个队列,然后维护它里面的数值是单调递减的,这样每次都能保证单调队列的队首元素就是当前窗口中的最大值。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// new一个单调队列
Deque<Integer> q = new LinkedList();
// new一个结果数组,结果数组的长度应该是n-k+1,即窗口个数
int[] res = new int[nums.length - k + 1];
// 循环遍历元素
// 注意!队列里装的不是元素值,而是索引,因为根据索引取元素是数组的正常操作,远远简单与根据元素取索引
for (int i = 0; i < nums.length; i++) {
// 如果当前队列不为空,且当前遍历的元素要大于队尾元素,就把队尾元素弹出,因为队尾元素肯定不是答案
// 并且要满足单调性
while (!q.isEmpty() && nums[q.peekLast()] <= nums[i]) {
q.pollLast();
}
// 然后就可以把现在的索引加进去
q.addLast(i);
// 此时窗口尾索引就是遍历的i,则窗口头索引是i-k+1,如果队列的头的小于窗口头索引,说明队列头的元素已经不在窗口内
// 所以弹出队列头的元素
if (q.peek() < i - k + 1) {
q.poll();
}
// 这里判断窗口有没有形成,即窗口头有没有出现,即大于0.
// 假设窗口大小为3,则只有i遍历到2的时候,才形成了大小为3的窗口
if (i + 1 >= k) {
// 窗口形成后,每次的队列最大元素,即队首元素,就是当前窗口的最大值元素索引
// 把这份元素加到结果宿主
res[i + 1 - k] = nums[q.peek()];
}
}
return res;
}
}