1. 两数之和
class Solution { public int[] twoSum(int[] nums, int target) { int[] result = new int[2]; Map<Integer, Integer> map = new HashMap(); for (int i=0;i<nums.length;i++) { map.put(nums[i], i); } for (int i=0;i<nums.length;i++) { Integer index = map.get(target-nums[i]); // 注意条件 index !=i,因为不能是两个自己 if (index != null && index != i) { result[0] = i; result[1] = index; return result; } } return result; } }
128. 最长连续序列
两种最朴素的解法之一:
- 先排序,从前往后找最长连续上升序列即可。该思路简单有效,但是复杂度已经至少有 O(nlogn)。实现起来也比较简单,在此不讨论该解法。
- 遍历数组中的每个元素num,然后以num为起点,每次+1向后遍历num+1,num+2,num+3...,判断这些元素是否存在于数组中。假设找到的最大的连续存在的元素为num+x,那么这个连续序列的长度即为x+1。最后将每个num所开始序列长度取个最大值即可。
解题思路1:哈希集合
方法 2 不用想,是肯定超时的。它的最坏时间复杂度已经达到了 O(n^3)
我们需要优化代码。优化的点主要有两个:
- 判断num+1,num+2,num+3...是否在数组中。上面的代码是用直接遍历的方式去查找的,时间复杂度为 O(n) 。我们可以改为哈希表查找,时间复杂度为 O(1)
- 遍历数组中每个元素num。逐一遍历每个元素会产生很多冗余工作,实际上我们无需一次针对每个元素num去判断num+1,num+2,num+3...是否在数组中。如果num-1已经在数组中的话,那么num-1肯定会进行相应的+1遍历,然后遍历到num,而且从num-1开始的+1遍历必定比从num开始的+1遍历得到的序列长度更长。因此,我们便可将在一个连续序列中的元素进行删减,让其只在最小的元素才开始+1遍历。比如,现有元素[1,2,4,3,5],当2,3,4,5发现均有比自己小1的元素存在,那么它们就不会开始+1遍历,而1是连续序列中最小的元素,没有比自己小1的元素存在,所以会开始+1遍历。通过上述方式便可将时间复杂度优化至O(n)。
解法描述:
1、先排序再找最长递增子序列
最长递增子序列在已排序 且 没有重复元素的情况下可用 dp 解决
- dp[0] = 1
- if(nums[i]==nums[i-1]+1) dp[i] = dp[i-1]+1
- if(nums[i] !=nums[i-1]+1) dp[i] = 1
- res = Max(res, dp[i])
但是对于有重复元素的情况,
2、哈希表
- 先将所有元素都加入到 HashSet 中
- 遍历 HashSet,当前数字记为 num:
- 如果 Set 中不存在 num-1,那么往后找 num+1,num+2,num+3.... 直到没有,那么结果就是 每次 在这段连续最长里面 找最大的那个
class Solution { //哈希表实现。哈希表的 插入、删除、查找的时间复杂度近似为 O(1) public int longestConsecutive(int[] nums) { if (nums.length == 0) { return 0; } Set<Integer> set = new HashSet(); // 每个加到哈希表里(去重) for (int i=0;i<nums.length;i++) { set.add(nums[i]); } int ans = 1; for (int num:set) { int cur = num; // 如果 num-1 存在,那么 num 就不用搜索了,搜索 num-1 的时候一直往后 +1 搜索会把 num 包括进去 // 当 num-1 不存在,才开始往后搜索 if (!set.contains(num-1)) { // 只有当num-1不存在时,才开始向后遍历num+1,num+2,num+3...... while (set.contains(cur+1)) { cur++; } // [num, cur] 是连续的区间 ans = Math.max(ans, cur-num+1); } } return ans; } // 这样复杂度取决于排序的复杂度 public int longestConsecutive2(int[] nums) { if (nums.length == 0) { return 0; } // 排序 Arrays.sort(nums); int longestLen = 1; int curLen = 1; for (int i=0;i<nums.length;i++) { if (i > 0) { // 0 1 1 1 2 这种,前面重复的1都跳过跳过 if (nums[i] == nums[i-1]) { continue; } // 0 1 1 1 2 这种,到 2 的时候,不能和前一个 1 比较,要一直跳到第一个 1 去比较 int lastDiffIndex = i-1; while (lastDiffIndex > 0 && nums[lastDiffIndex] == nums[lastDiffIndex-1]) { lastDiffIndex-=1; } // 已排序,与前一个的差值为 1 则说明是连续的 if (nums[i] - nums[lastDiffIndex] == 1) { longestLen = Math.max(longestLen, curLen+=1); } else { // 不连续了,curLen 重新开始计算 curLen = 1; } } } return longestLen; } }
49. 字母异位词分组
字母异位的词排序之后都是一样的
所以对每个字符串先排序
排序后的字符串作为 HashMap 的 key
而 value 则是字母异位词的 List
注意对字符串排序的方法是
char[] orderStrArr = str.toCharArray(); Arrays.sort(orderStrArr); String orderStr = String.valueOf(orderStrArr);
class Solution { public List<List<String>> groupAnagrams(String[] strs) { Map<String, List<String>> orderStr2StrListMap = new HashMap(); for (String str : strs) { char[] orderStrArr = str.toCharArray(); Arrays.sort(orderStrArr); String orderStr = String.valueOf(orderStrArr); List<String> strList = orderStr2StrListMap.get(orderStr); if (strList == null) { strList = new ArrayList(); orderStr2StrListMap.put(orderStr, strList); } strList.add(str); } List<List<String>> res = new ArrayList(); for(Map.Entry<String, List<String>> entry : orderStr2StrListMap.entrySet()) { res.add(entry.getValue()); } return res; } }
560. 和为 K 的子数组
假设 left 到 right 下标的子数组和为 k
- nums[left...right] = k
- preSum[right] - preSum[left] = k
- preSum[left] = preSum[right] - k
所以要 每次到 right 的时候,找到等于 preSum[right] - k 的 preSum[left] 有多少个
用一个 map 来记录,前缀和的 count (见官方题解动画)
key: 前缀和的值
value: 前缀和为这个值的个数
注意!!!:
- map 要放入一个初始值 {0,1}
- 一定要先 getPreSumCount ,再把加上当前节点的 PreSum 放进 map +1(因为当前节点的和不属于当前节点的前缀和)
class Solution { public int subarraySum(int[] nums, int k) { Map<Integer, Integer> leftPreSum2CntMap = new HashMap(); leftPreSum2CntMap.put(0, 1); int rightPreSum = 0; // 总的满足条件的子数组个数的计数,最后的结果 int count = 0; for (int right=0;right<nums.length;right++) { rightPreSum += nums[right]; // [...i] // left 到 right 的子数组和为 k // nums[left...right] = k // preSum[right] - preSum[left] = k // preSum[left] = preSum[right] - k // 所以每次到 right 的时候,找到等于 preSum[right] - k 的 preSum[left] 有多少个 Integer leftPreSumCnt = leftPreSum2CntMap.get(rightPreSum - k); if (leftPreSumCnt != null) { count += leftPreSumCnt; } // 将这次的和放到 map 里 Integer rightPreSumCnt = leftPreSum2CntMap.get(rightPreSum); if (rightPreSumCnt == null) { rightPreSumCnt = 0; } leftPreSum2CntMap.put(rightPreSum, ++rightPreSumCnt); } return count; } }
437. 路径总和 III
Map 不是 Integer 那种每次都 new 一个新对象的无状态类型,所以 dfs 完后得恢复现场
用一个 map 来记录,前缀和的 count
- key: 前缀和的值
- value: 前缀和为这个值的个数
注意!!!:
- map 要放入一个初始值 {0,1}
- 一定要先 getPreSumCount ,再把加上当前节点的 PreSum 放进 map +1(因为当前节点的和不属于当前节点的前缀和)
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private static Integer res = 0; public Solution() { res = 0; } public int pathSum(TreeNode root, int targetSum) { // Long 是因为有极端用例 Map<Long, Integer> preSum2CountMap = new HashMap(); // 一定要提前放进去 (0, 1) 即不加前面任何数字的话,就是0 preSum2CountMap.put(0L, 1); dfs(root, preSum2CountMap, 0L, targetSum); return res; } private void dfs(TreeNode root, Map<Long, Integer> preSum2CountMap, Long curSum, Integer targetSum) { if (root == null) { return; } curSum+=root.val; res += preSum2CountMap.getOrDefault(curSum - targetSum, 0); // 将现在的 curSum 放进 map 一定要在 map.get(preSum) 即 map.get(curSum-targetSum) 之后 // 因为到自己这里的所有和,不属于自己的前缀和 preSum2CountMap.put(curSum, preSum2CountMap.getOrDefault(curSum, 0)+1); dfs(root.left, preSum2CountMap, curSum, targetSum); dfs(root.right, preSum2CountMap, curSum, targetSum); preSum2CountMap.put(curSum, preSum2CountMap.getOrDefault(curSum, 0)-1); } }