1. 两数之和 ------》哈希表,快速寻找目标元素位置
java
class Solution {
public int[] twoSum(int[] nums, int target) {
/*
要找出对应下标,显然暴力不行,不能排序,否则下标会变动
已经知道target, 假设枚举数值a,这个时候我们知道另外一个数值b = target - a
那怎么快速找到这个b在数组nums中的下标呢?显然可以使用哈希表
这里使用的哈希表就是Map,存储键值(b, 对应的索引)
可以发现,每次如果没有查到的话,就将当前数值放入map中,
因为只有两个不重复的对应位置,所以从左往右放入map不冲突。不会漏
*/
int n = nums.length;
// map进行存储
Map<Integer, Integer> map = new HashMap<>();
// 存储结果
int[] ans = new int[2];
// 遍历数组
for (int i = 0; i < n; i++) {
// 计算b值
int b = target - nums[i];
if (map.containsKey(b)) {
// 说明有,
// 获取索引
int j = map.get(b);
// 存储两个下标
ans[0] = i;
ans[1] = j;
break;
}
// }else {
map.put(nums[i], i);
// }
}
return ans;
}
}
知识点:
Java中的哈希表,通常是指Map<K, V>接口实现的类。
最核心的类有三个:HashMap, LinkedHashMap(可实现队列), ConcurrentHashMap。(HashTable,已被淘汰)。
49 字母异位词分组 ------》哈希表,值存储字符串数组
java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 使用哈希表,存储排序后的字符串,同一组的原始字符串列表
Map<String, List<String>> map = new HashMap<>();
// 遍历字符串数组
for (String s : strs) {
// 转换为字符数组
char[] chs = s.toCharArray();
Arrays.sort(chs);
// 变换为字符串
String key = new String(chs);
// 维护哈希表
// 这里因为都要存入map,所以可以先判断如果没有这个键,先创建一个键值对,后续只需要插入就可以了
if (!map.containsKey(key)) {
map.put(key, new ArrayList<>());
}
map.get(key).add(s);
}
// 最后取出来放入结果数组中
List<List<String>> ans = new ArrayList<>();
// 直接将map的值转换为列表集合
// 因为值本身是List<String>类型,因此封装一个List,就是List<List<String>>类型
return new ArrayList<>(map.values());
}
}
知识点:
怎么对一个字符串排序:先变为字符数组,然后排序,然后new String()变为字符串
map直接将values变为数组
128 最长连续序列 ------》哈希表光有集合不行,还有逻辑,判断是否是一段新的开始
java
class Solution {
public int longestConsecutive(int[] nums) {
// 将数存入集合
Set<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num); // 去重
}
// 遍历元素
int longest = 0;
// 遍历集合,否则遍历没有去重的nums会超时
for (int num : set) {
// 如何判断是新的一段起点
// 每一次要从断开连续的数字开始
// 如何判断是不是不连续的可以通过集合判断
if (!set.contains(num- 1)){
int curNum = num;
int curLen = 1;
while (set.contains(curNum+1)){
curNum++;
curLen++;
}
longest = Math.max(longest, curLen);
}
}
return longest;
}
}
知识点:
怎么将一个数组转换为Set集合呢?转换为List,注意要使用HashSet构造函数。
Set set = new HashSet<>(Arrays.asList(array));
注意HashSet是无序的不支持排序。
283 移动零 ------》双指针,while循环,找到不为0的数,交换
java
class Solution {
public void moveZeroes(int[] nums) {
int n = nums.length, left = 0, right = 0;
while (right < n) {
if (nums[right] != 0){
swap(nums, left, right);
left++;
}
right++;
}
}
public void swap(int[] nums, int left, int right){
int tmp = nums[right];
nums[right] = nums[left];
nums[left] = tmp;
}
}
11 盛最多水的容器 ------》双指针,左右指针,从较低高度一方更新指针
java
class Solution {
public int maxArea(int[] height) {
// 注意边界,这里题目给定数据是可以的
int n = height.length;
// 定义双指针
int left = 0, right = n - 1;
// 维护结果
int ans = 0;
// 更新双指针
while (left < right){
// 先计算当前位置可装水
// 我要怎么知道是哪边高度低一点呢
ans = Math.max(ans, (right - left) * Math.min(height[left], height[right]));
if (height[left] < height[right]){
left++;
}else {
right--;
}
}
return ans;
}
}
这里双指针从左右向内收缩,常用的是while(left < right),进行判断
15 三数之和 ------》枚举三数中第一个数,后两个数双指针维护,
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
/*
关键要厘清逻辑,可以发现只需要数组元素,与索引无关,
因此可以先排序,然后固定中间的一个数,这样就简化为了左右两边的数,进行双指针查找是否满足条件
*/
int n = nums.length;
List<List<Integer>> ans = new ArrayList<>();
if (n < 3 || nums == null) return ans;
// 排序
Arrays.sort(nums);
// 枚举
for (int i = 0; i < n; i++) {
// 如果当前元素大于0,则后面元素都大于0,不可能和为0
if (nums[i] > 0) break;
//如果相邻重复的就需要跳过
if (i > 0 && nums[i] == nums[i-1]) continue;
// 定义左右指针
int L = i+1;
int R = n - 1;
while (L < R){
int sum = nums[i] + nums[L] + nums[R];
if (sum == 0){
// 这个判断,是否存在重复
ans.add(Arrays.asList(nums[i], nums[L], nums[R]));
while (L < R && nums[L] == nums[L+1]) L++;
while (L < R && nums[R] == nums[R-1]) R--;
L++;
R--;
}else if (sum < 0) L++;
else if (sum > 0) R--;
}
}
return ans;
}
}
42 接雨水 ------》不要陷入区间,结合动规,预处理左右高度,
java
class Solution {
public int trap(int[] height) {
int n = height.length;
if (n == 0 || height == null) return 0;
// 动态规划预处理
int[] leftMax = new int[n], rightMax = new int[n];
leftMax[0] = height[0];
rightMax[n-1] = height[n-1];
// 从左到右处理leftMax
for (int i = 1; i < n; i++){
leftMax[i] = Math.max(leftMax[i-1], height[i]);
}
// 从右往左处理rightMax
for (int i = n-2; i >= 0; i--){
rightMax[i] = Math.max(rightMax[i+1], height[i]);
}
// 然后处理维护接雨水
int ans = 0;
for (int i = 0; i < n; i++){
// 左右两边最大值的最小值
int tmp = Math.min(leftMax[i], rightMax[i]);
ans += tmp - height[i];
}
return ans;
}
}
3 无重复字符的最长子串 ------》滑动窗口,右边重复了,从左边删除直到没有重复,维护最长
直接用while循环解决,直到没有重复的,
java
class Solution {
public int lengthOfLongestSubstring(String s) {
// 要不含有重复字符可以维护一个窗口保存当前已遍历的字符串
// 用集合存储如果已经存在该字符则收敛左边界
// if (s.length() == 0 || s == null) return 0;
int n = s.length();
if (n == 0 || n == 1) return n;
// 创建集合
Set<Character> set = new HashSet<>();
// 维护左边界
int left = 0;
// 遍历字符串
int longest = 0;
for (int i = 0; i < s.length(); i++) {
if (set.contains(s.charAt(i))){
// 包含该字符
while (left < s.length() && set.contains(s.charAt(i))){
// 说明存在重复的先从左边删除直到没有重复的
// left++;
set.remove(s.charAt(left));
left++;
}
}
set.add(s.charAt(i));
// 更新最长长度
longest = Math.max(longest, i - left + 1);
}
return longest;
}
}
while循环
java
class Solution {
public int lengthOfLongestSubstring(String s) {
/*
看到这种就要肌肉记忆,联想出不重复,假设当前有一个子串了,如果下一个字符在这个子串中重复了,一定是子串的最左边的元素重复了,这样我们只需要维护最左边的元素,并更新长长度
判断字符是否在字符串中,可以直接判断s.contains()
但由于需要动态维护子串,因此可以使用哈希集合判断是否重复
*/
int n = s.length();
if (n == 0 || n == 1) {
return n;
}
int ans = 0;
// 维护一个滑动窗口
int left = 0;
Set<Character> set = new HashSet<>();
// set.add(s.charAt(0));
for (int right = 0; right < n; right++) {
char ch = s.charAt(right);
while (set.contains(ch)) {
// 说明存在重复的
// 先进行删除
// set.remove(ch);
// 不是说删除ch,而是从左指针删除,知道没有重复的
// 例如示例pwwkew
set.remove(s.charAt(left));
left++;
}
// 不存在重复的,都要进行添加
set.add(ch);
// 更新最长长度
ans = Math.max(ans, set.size());
// ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
438 找到字符串中所有字母异位词
中等
下面是自己枚举的做法,击败5%
java
class Solution {
public List<Integer> findAnagrams(String s, String p) {
/**
要求找出所有p的异位词的子串,显然子串长度一定等于p的长度,所以只需要枚举等长的子串
然后拿到子串变换为字符数组排序变换回字符串判断是否相等
*/
int len1 = s.length(), len2 = p.length();
// 对p先进行排序
char[] pchs = p.toCharArray();
Arrays.sort(pchs);
p = new String(pchs);
// 枚举len2长度的子串
List<Integer> ans = new ArrayList<>();
for (int i = 0; i <= len1 - len2; i++) {
String sub = s.substring(i, i + len2);
char[] subchs = sub.toCharArray();
// 排序
Arrays.sort(subchs);
sub = new String(subchs);
if (sub.equals(p)){
ans.add(i);
}
}
return ans;
}
}
实际上可以在窗口大小固定的情况下,枚举当前窗口中对应的字符数量是否和p中的字符数量相同,如果相同则记录子串的起始索引,反之则窗口向前滑动,更新当前窗口的字符数量。
java
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 边界判断
int n1 = s.length(), n2 = p.length();
if (n1 < n2) {
return new ArrayList<>();
}
// 存储结果
List<Integer> ans = new ArrayList<>();
/**
如何判断是否两个字符串相等,由于这里都是小写字母,所以可以维护两个长为26的数组
如果两个字符串中字符频率数组相同则说明两个字符相同
相比频繁创建substring,创建子串会消耗内存和时间
*/
int[] sCnt = new int[26];
int[] pCnt = new int[26];
// 初始化第一个窗口出现的频率
for(int i = 0; i < n2; i++){
sCnt[s.charAt(i) - 'a']++;
pCnt[p.charAt(i) - 'a']++;
}
// 判断第一个窗口是否匹配
if (matches(pCnt, sCnt)) {
ans.add(0);
}
// 滑动窗口
// 窗口范围,索引为n2到n1-1
// 每次加一个,左边界对应字符频率减一个
for (int i = n2; i < n1; i++) {
// 新字符
sCnt[s.charAt(i) - 'a']++;
// 左边界旧字符
sCnt[s.charAt(i-n2) - 'a']--;
// 判断是否匹配
if (matches(pCnt, sCnt)){
ans.add(i-n2+1);
}
}
return ans;
}
public boolean matches(int[] pCnt, int[] sCnt){
// 判断对应字符的频率是否相同
for (int i = 0; i < 26; i++) {
if (pCnt[i] != sCnt[i]){
return false;
}
}
return true;
}
}
Java中判断两个字符串是否相等。
560 和为K的子数组
中等
解题思路:
从暴力循环------》想到前缀和,进一步将前缀和数组理解为两数只和,利用哈希表统计 ------》
java
class Solution {
public int subarraySum(int[] nums, int k) {
int n = nums.length;
int cnt = 0, pre = 0;
// 创建map 存储
Map<Integer, Integer> map = new HashMap<>();
// 初始化第一个
map.put(0, 1);
// 边计算前缀和,然后更新map
for (int i = 0; i < n; i++){
pre += nums[i];
if (map.containsKey(pre - k)) {
cnt += map.get(pre - k);
}
map.put(pre, map.getOrDefault(pre, 0) + 1);
}
return cnt;
}
}
239 滑动窗口最大值
或者自己实现一个双端队列,自己保持队列的单调性
java
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0 || k <= 0) {
return new int[0];
}
int n = nums.length;
// 初始化结果数组
int[] res = new int[n - k + 1];
// 不用优先队列自己利用双端队列实现
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 1. 判断队首已经滑出窗口的过期元素
while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 2. 维护单调递减特性移除队尾所有比当前元素小的元素
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 3. 将当前元素的下标加入队尾
deque.offerLast(i);
// 4. 然后当前队列中就是保持单调的
if (i >= k - 1) {
res[i-k+1] = nums[deque.peekFirst()];
}
}
return res;
}
}
76 最小覆盖子串
困难
滑动窗口思路到位,就很好理解。
java
class Solution {
public String minWindow(String s, String t) {
/**
首先判断s长度是否大于等于t的长度,
有点类似滑动窗口最大值、无重复的最长子串的做法思路可以维护一个窗口,每次窗口先向右扩充,判断当前窗口字符数能否满足包含t
如果满足则左边收敛
*/
int m = s.length();
int n = t.length();
// 判断边界条件
if (m < n) return "";
// 使用哈希表存储字符及其对应次数
Map<Character, Integer> tMap = new HashMap<>();
Map<Character, Integer> sMap = new HashMap<>();
// 统计t
for (char c : t.toCharArray()) {
tMap.put(c, tMap.getOrDefault(c, 0) + 1);
}
// // 初始化sMap
// for (int i = 0; i < n; i++) {
// sMap.put(s.charAt(i), sMap.getOrDefault(s.charAt(i), 0) + 1);
// }
// 直接定义左右指针
int left = 0;
int valid = 0; // 记录窗口中满足条件的字符种类数量
int start = 0, minLen = Integer.MAX_VALUE; // 记录最小子串的起点和终点
// 右指针向前移动
for (int right = 0; right < m; right++) {
char cright = s.charAt(right);
// 判断当前字符是否是在t中
if (tMap.containsKey(cright)){
// 存入当前窗口中
sMap.put(cright, sMap.getOrDefault(cright, 0) + 1);
// 并统计当前窗口中包含t的字符数量
if (sMap.get(cright).intValue() <= tMap.get(cright).intValue()){
valid++;
}
}
// 判断是否valid == n说明此时已经包含了t了
// 向左收缩窗口
while (valid == n) {
if (right - left + 1 < minLen) {
// 更新
start = left;
minLen = right - left + 1;
}
// 收缩左边界
char cleft = s.charAt(left);
// 判断左边界的字符是不是包含t的字符
if (tMap.containsKey(cleft)){
// 判断移除该字符是否会导致包含t的字符减少也就是当窗口内字符数与t相同的情况下
if (sMap.get(cleft).intValue() == tMap.get(cleft).intValue()){
valid--;
}
// 窗口内变化
sMap.put(cleft, sMap.getOrDefault(cleft, 0) - 1);
}
left++;
}
}
// 判断第一个窗口是否满足
// 遍历后续的每加入一个都要重新判断吗?
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
}
53 最大子数组和
中等
解题思路:
动态规划
java
class Solution {
public int maxSubArray(int[] nums) {
/**
暴力循环可以求得最大子数组和
如何优化呢使用动态规划?
使用i标识当前位置的最大子数组和是多少
应该是dp[i] = Math.max(dp[i-1] + nums[i], nums[i])
可以这样的原因是前面的都小了肯定起点从当前开始
*/
// 使用dp[i]表示当前位置的最大子数组和
int[] dp = new int[nums.length];
dp[0] = nums[0];
int ans = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
56 合并区间
中等
解题思路:
合并区间问题,可以先将区间按照起始位置进行排序,涉及到二维数组的排序
初始化合并区间时,没有必要先存入第一个元素,而是初始化为变量,当第一个合并区间截止,才存入结果数组中,然后更新变量。
java
class Solution {
public int[][] merge(int[][] intervals) {
// 可以理解为是二维数组将二维数组先按照起始位置排序
// 遍历从第二个开始看第一个结尾和第二个起始位置是否重叠,重叠则合并反之则加入
// 先排序
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
// 创建一个动态二维数组
List<int[]> ans = new ArrayList<>();
// 存储第一个
// ans.add(intervals[0]);
// 先不要存到结果数组中,而是初始化
int start = intervals[0][0], end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
// int start = intervals[i][0];
// int end = intervals[i][1];
// 判断是否有交集
if (intervals[i][0] <= end) {
end = Math.max(end, intervals[i][1]);
}else {
// 无重叠
// 将当前区间存入
ans.add(new int[]{start, end});
// 更新
start = intervals[i][0];
end = intervals[i][1];
}
}
// 结束后还有最后一个区间
ans.add(new int[]{start, end});
return ans.toArray(new int[ans.size()][]);
}
}
或者直接从下标0元素遍历即可
java
class Solution {
public int[][] merge(int[][] intervals) {
/**
显然要合并区间,需要先对区间进行排序
然后依次遍历区间,假设起始结尾区间大小为pre = 0,
如果当前区间的inter[0] <= pre,则要合并当前区间,
反之存储上一段区间,然后这一段区间作为新的区间
*/
// 注意边界
int n = intervals.length;
// 排序
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
// 假设当前一个区间的起点和终点start, end
// int start = inter[0], end = inter[1];
// List<List<Integer> ans = new ArrayList<>();
// for (int i = 1; i < n; i++) {
// if (intervals[i][0] <= end) {
// // 说明需要合并
// end = Math.max(end, intervals[i][1]);
// }else {
// // 保存,更新
// List<Integer> tmp =
// start = intervals[i][0];
// end = intervals[i][1];
// }
// }
// 但是末尾的怎么处理呢?
List<int[]> ans = new ArrayList<>();
for (int[] inter : intervals) {
// 这种思路解决了动态添加区间的问题
int m = ans.size();
if (m > 0 && inter[0] <= ans.get(m-1)[1]) {
// 可以合并
ans.get(m-1)[1] = Math.max(ans.get(m-1)[1], inter[1]);
}else {
ans.add(inter);
}
}
// 转换为int[][]
return ans.toArray(new int[ans.size()][]);
}
}
基础知识:创建二维数组List<int[]> ans = new ArrayList<>(),文章解析。
189 轮转数组
中等
有多种思路,可以额外用一个数组进行存储变换后的数组。此时空间复杂度O(n),为了空间复杂度为O(1),可以直接在原地操作。
规律是先整体翻转,然后是按照k的位置,分别进行翻转。需要制定一个翻转方法,将指定区间进行翻转。
java
class Solution {
public void rotate(int[] nums, int k) {
reverse(nums, 0, nums.length - 1);
// reverse(nums, 0, k-1);
// reverse(nums, k, nums.length - 1);
// k求余之后的位置
reverse(nums, 0, k % nums.length - 1);
reverse(nums, k% nums.length, nums.length - 1);
}
// 指定区间翻转
private void reverse(int[] nums, int start, int end) {
while (start < end) {
int tmp = nums[start];
nums[start] = nums[end];
nums[end] = tmp;
start++;
end--;
}
}
}