文章目录
-
- [1 哈希](#1 哈希)
-
- [1.1 1-1.两数之和🟢](#1.1 1-1.两数之和🟢)
- [1.2 2-49.字母异位词分组🟡](#1.2 2-49.字母异位词分组🟡)
- [1.3 3-128.最长连续序列🟡](#1.3 3-128.最长连续序列🟡)
- [2 双指针](#2 双指针)
-
- [2.1 4-283.移动零🟢](#2.1 4-283.移动零🟢)
- [2.2 6-15.三数之和🟡](#2.2 6-15.三数之和🟡)
- [2.3 7-11.盛最多水的容器🟡](#2.3 7-11.盛最多水的容器🟡)
- [2.4 8-42.接雨水🔴](#2.4 8-42.接雨水🔴)
- [3 滑动窗口](#3 滑动窗口)
-
- [3.1 9-3.无重复字符的最长子串🟡](#3.1 9-3.无重复字符的最长子串🟡)
- [3.2 10-438.找到字符串中所有字母异位词🟡](#3.2 10-438.找到字符串中所有字母异位词🟡)
1 哈希
1.1 1-1.两数之和🟢
题目:给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
链接:1. 两数之和
示例 :
plain
输入:nums = [3,3], target = 6
输出:[0,1]
思路:
两个 for 循环可以解决,但第2个 for 循环可以用哈希表来快速查,不用一个个遍历
代码:
java
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> hashMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 查表,看看是否有能和 nums[i] 凑出 target 的元素
int need = target - nums[i];
if (hashMap.containsKey(need)) {
return new int[]{hashMap.get(need), i};
}
// 查不到则存入映射,这样只用一次for循环
hashMap.put(nums[i], i);
}
return null;
}
}
1.2 2-49.字母异位词分组🟡
题目:给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
链接:49. 字母异位词分组
示例 :
plain
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
思路:
难点在于怎么判断哪些单词属于异味词,因为不能直接用 ==
来判断。
观察异位词的特点可以看出他们排序后可以用 ==
来直接判断
代码:
java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, List<String>> hashMap = new HashMap<>();
for (String str : strs) {
// 字符串转换成数组对字符串的字符排序
char[] array = str.toCharArray();
Arrays.sort(array);
String key = new String(array);
// 获取key对应的集合,若不存在则返回一个空集合
List<String> list = hashMap.getOrDefault(key, new ArrayList<String>());
list.add(str);
hashMap.put(key, list);
}
return new ArrayList<>(hashMap.values());
}
}
1.3 3-128.最长连续序列🟡
题目:给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
链接:128. 最长连续序列
示例 :
plain
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
思路:
利用哈希集和可以快速判断 nums
是否存在某个数字。
遍历 nums
,若存在 num-1
,说明当前数字不是该连续序列的起始值,从而过滤掉一些情况。之后不断用哈希集和判断是否存在序列的下一个数值
代码:
java
class Solution {
public int longestConsecutive(int[] nums) {
// 转化成哈希集合,不需要HashMap,只关注是否存在
Set<Integer> set = new HashSet<Integer>();
for (int num : nums) {
set.add(num);
}
int res = 0;
for (int num : set) {
// num 不是连续子序列的第一个,跳过
if (set.contains(num - 1)) {
continue;
}
// num 是连续子序列的第一个,开始向后计算连续子序列的长度
int curNum = num;
int curLen = 0;
while (set.contains(curNum)) {
curNum += 1;
curLen += 1;
}
// 更新最长连续序列的长度
res = Math.max(res, curLen);
}
return res;
}
}
2 双指针
2.1 4-283.移动零🟢
题目:给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
链接:283. 移动零
示例 :
plain
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
思路:
快指针遍历数组,慢指针记录数组下一个不为0元素的位置。
快指针遇到不为0的元素时,与慢指针的位置进行交换即可
代码:
java
class Solution {
public void moveZeroes(int[] nums) {
int left = 0, right = 0;
while (right < nums.length) {
if (nums[right] != 0) {
// 这里也可以用nums[left] = nums[right]
// 然后把left及其后面的元素赋为0
swap(nums, left, right);
left++;
}
right++;
}
}
public void swap(int[] nums, int left, int right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
2.2 6-15.三数之和🟡
题目:给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
链接:15. 三数之和
题解详细解释:三数之和
示例 :
plain
输入: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] 。
注意,输出的顺序和三元组的顺序并不重要。
思路:
3个 for
循环会超时,所以要优化。有重复元素,用哈希表也会比较麻烦
可以一个 for
循环遍历第1个数,剩余两个数不能用 for
循环的话,可以用双指针优化查找时间。
事先排序后,左右各一个指针。计算当前3个数字的和比目标值0大还是小,进而移动左指针(和会变大)或右指针(和会变小)。这样就可以减少循环次数,但要注意去重
代码:
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组
if (nums[i] > 0) {
return res;
}
// 去重a
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重b和c,应该放在找到一个三元组之后
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return res;
}
}
2.3 7-11.盛最多水的容器🟡
题目:给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明: 你不能倾斜容器。
链接:11. 盛最多水的容器
示例 :
plain
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
思路:
最直观的是两个 for 循环遍历所有结果,但这样会超时,所以要进行优化
用双指针,一个指向左边,一个指向右边。最大水量取决于宽度和高度。而高度取决于 left
和 right
中较小的那个,比如 left
较小,那么移动 right
只会让高度不变或者更小,所以要移动高度较低的那条线,这样虽然宽度减小,但是高度有可能增大,容量才有可能变大
抽象成二维数组,因为每次移动都会排除一行或一列,所以不会遗漏
代码:
java
class Solution {
public int maxArea(int[] height) {
int res = 0, left = 0, right = height.length - 1;
while (left < right) {
// 每次移动高度最短的那条线
if (height[left] < height[right]) {
res = Math.max(res, (right - left) * height[left]);
left++;
} else {
res = Math.max(res, (right - left) * height[right]);
right--;
}
}
return res;
}
}
2.4 8-42.接雨水🔴
题目:给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
链接:42. 接雨水
示例 :
plain
输入: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 个单位的雨水(蓝色部分表示雨水)。
思路:
可以只关注某一个位置能接到多少雨水,应该是 min(左边最高的柱子,右边最高的柱子)-当前高度
但是计算某一侧最高的柱子要是每次都一个个遍历太耗时,可以使用备忘录的方式先提前算出所有位置两侧最高柱子的高度
不过上述题解要定义数组,空间复杂度为 O(n),可以使用双指针进一步优化。思路还是对于每个位置用上面的公式计算当前位置能接多少雨水,不过是左右两个指纹向内移动。用两个变量取代之前的两个备忘录数组。
代码:
1.动态规划解法
java
class Solution {
public int trap(int[] height) {
if (height.length == 0) {
return 0;
}
int n = height.length;
int res = 0;
// 数组充当备忘录
int[] l_max = new int[n];
int[] r_max = new int[n];
// 初始化 base case
l_max[0] = height[0];
r_max[n - 1] = height[n - 1];
// 从左向右计算 l_max
for (int i = 1; i < n; i++)
l_max[i] = Math.max(height[i], l_max[i - 1]);
// 从右向左计算 r_max
for (int i = n - 2; i >= 0; i--)
r_max[i] = Math.max(height[i], r_max[i + 1]);
// 计算答案 当前位置能接到的雨水取决于min(左,右最高的柱子)-当前高度
for (int i = 1; i < n - 1; i++)
res += Math.min(l_max[i], r_max[i]) - height[i];
return res;
}
}
2.双指针
java
class Solution {
int trap(int[] height) {
int left = 0, right = height.length - 1;
int lMax = 0, rMax = 0;
int res = 0;
while (left < right) {
lMax = Math.max(lMax, height[left]);
rMax = Math.max(rMax, height[right]);
// res += min(lMax, rMax) - height[i]
if (lMax < rMax) {
res += lMax - height[left];
left++;
} else {
res += rMax - height[right];
right--;
}
}
return res;
}
}
3 滑动窗口
滑动窗口就两步:
- 右指针不断右移,每一次右移要不要做些额外处理
- 循环判断左侧窗口是否要收缩,若收缩应该怎么处理
无论右移还是收缩都是更改字符在window 对应的数值并更新指针
3.1 9-3.无重复字符的最长子串🟡
题目:给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 :
plain
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
思路:
子串问题那就要用双指针,这题更准确点是用双指针维护一个滑动窗口。right
维护窗口右边并不断向右延伸
当遇到和当前窗口重复的值时,left
不断向右缩小滑动窗口,直到包含当前重复值在内的新的滑动窗口没有重复值
该题用 HashSet
也能做,为了使用模板所以这里用的是 HashMap
代码:
java
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.length()) {
// 窗口右指针不断右移
char cur = s.charAt(right);
window.put(cur, window.getOrDefault(cur, 0) + 1);
right++;
// 判断左侧窗口是否要收缩
while (window.get(cur) > 1) {
char d = s.charAt(left);
window.put(d, window.get(d) - 1);
left++;
}
// 在这里更新答案
res = Math.max(res, right - left);
}
return res;
}
}
3.2 10-438.找到字符串中所有字母异位词🟡
题目:给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 :
plain
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
思路:
子串的处理仍然是用滑动窗口解决。本题要找的子串的长度是固定的,也就是可以看作是一个固定长度的滑动窗口不断右移,判断当前窗口的子串是否满足条件即可。
因为只有小写字母,所以可以用长度为26的数组记录每个字母出现的个数。然后判断两个字符串是否满足条件就可以转换为判断两个数组内的值是否相等,需要用 Arrays.equals(arr1,arr2)
,注意不能用 arr1.equals(arr2)
用模板则是用一个 count
来记录有效数量是否达到要求,右移的时候判断是否要 ++
,左侧收缩时判断是否要 --
。
注意要做两步判断,首先是判断当前字符是否在目标字符串 p
中,即使在还要判断当前字符的个数是否和 p
的相同。例如,abb 的第3位 b 虽然也在 abc 中,但 b 的数量不同
代码:
1.滑动窗口-数组版
java
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length(), pLen = p.length();
if (sLen < pLen) {
return new ArrayList<Integer>();
}
List<Integer> ans = new ArrayList<Integer>();
int[] sCount = new int[26];
int[] pCount = new int[26];
for (int i = 0; i < pLen; ++i) {
++sCount[s.charAt(i) - 'a'];
++pCount[p.charAt(i) - 'a'];
}
if (Arrays.equals(sCount, pCount)) {
ans.add(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s.charAt(i) - 'a'];
++sCount[s.charAt(i + pLen) - 'a'];
if (Arrays.equals(sCount, pCount)) {
ans.add(i + 1);
}
}
return ans;
}
}
2.滑动窗口-模板版
java
class Solution {
public List<Integer> findAnagrams(String s, String p) {
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (char c : p.toCharArray())
need.put(c, need.getOrDefault(c, 0) + 1);
int left = 0, right = 0;
int count = 0;
List<Integer> res = new ArrayList<>();
while (right < s.length()) {
// 1.窗口右指针不断右移
char cur = s.charAt(right);
right++;
// 右移的时候判断是否更新count
if (need.containsKey(cur)) {
window.put(cur, window.getOrDefault(cur, 0) + 1);
// abb 的第3位b虽然也在abc中,但b的数量不同
if (window.get(cur).equals(need.get(cur)))
count++;
}
// 2.判断左侧窗口是否要收缩
while (right - left >= p.length()) {
// 当窗口符合条件时,把起始索引加入 res
if (count == need.size())
res.add(left);
// 收缩时的处理
char d = s.charAt(left);
left++;
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d)))
count--;
window.put(d, window.get(d) - 1);
}
}
}
return res;
}
}