4-查找问题
目录
1 综述
LeetCode-349-两个数组的交集
LeetCode-350-两个数组的交集||
LeetCode-242-有效的字母异位词
LeetCode-202-快乐数
LeetCode-290-单词规律
LeetCode-205-同构字符串
LeetCode-451-根据字符出现频率排序
2 查找的经典问题
LeetCode-1-两数之和
LeetCode-15-三数之和
LeetCode-18-四数之和
LeetCode-16-最接近的三数之和
LeetCode-454-四数相加||
LeetCode-49-字母异位词分组
LeetCode-447-回旋镖的数量
LeetCode-149-直线上最多的点数
3 滑动窗口+查找表
LeetCode-219-存在重复元素||
LeetCode-217-存在重复元素
4 查找表的顺序性
LeetCode-220-存在重复元素|||
1 综述
查找问题可以分为两大类:(1)查找是否存在某一元素,此时常用的数据集合是set集合;(2)查找某一元素出现的频率,此时常用的数据集合是map集合。以LeetCode 349问题为例,注意说明中有一个不考虑输出结果的顺序,那么利用set集合就可以了。在解决具体问题时,可以具体考虑一些情况来优化代码:
class Solution { public int[] intersection(int[] nums1, int[] nums2) { //由于题目中说明了可以不考虑输出结果的顺序因此选用hashSet是最好的方案 //这个方法返回的是数组,应该把集合中的元素再存成数组的形式 Set<Integer> set = new HashSet<>(); for (int i : nums1) { set.add(i); } List<Integer> list = new ArrayList<>(); //在添加到set时就进行判断,这样比先添加完再进行比较时间复杂度更少 for (int i : nums2) { if (set.contains(i)) { list.add(i); set.remove(i); } } int[] arr = new int[list.size()]; for (int i = 0; i < list.size(); i++) { arr[i] = list.get(i); } return arr; } }
与此类似有LeetCode 350在上面的问题中是不需要记录频次的,但在这里对于重复的元素也要输出,这就说明需要记录频次了因此考虑使用map集合:
class Solution { public int[] intersect(int[] nums1, int[] nums2) { //在题目349中由于不考虑出现的次数因此可以用set集合,但是现在要考虑频率了,选用set集合就不合适了考虑map映射 //同样的由于不考虑输出顺序因此选用HashMap实现底层以便获得最优效率,映射key设定为元素值,频率设定为value Map<Integer, Integer> map = new HashMap<>(); for (int i : nums1) { if (map.containsKey(i)) { map.put(i, map.get(i) + 1); }else{ map.put(i, 1); } } //返回的是一个数组因此必然要进行转存,进行转存的时候先用动态数组充当中间阶段 List<Integer> list = new ArrayList<>(); for (int i : nums2) { if (map.containsKey(i)) { list.add(i); map.put(i, map.get(i) - 1);//修改了频率 //当频率为0时就移除了 if (map.get(i) == 0) { map.remove(i); } } } int[] arr = new int[list.size()]; for (int i = 0; i < list.size(); i++) { arr[i] = list.get(i); } return arr; } }
与此类似的题目有:LeetCode 242、202、290、205、451
2 查找的经典问题
例1:以LeetCode 1 为例,当采用求两个数的和时暴力解法为O(n^2)的时间复杂度,或者先排序再求和为O(nlogn)的时间复杂度,但因为题目中两个数的之和已经是给出来的,那么用target减去数组中当前遍历的数再在剩下的数字中进行查找,这样就把求和问题转换为查找问题了,时间复杂度相应也会下降很多,其具体代码如下:
class Solution { public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); // 如果一开始便把所有的元素都放入查找表中那么相同的元素可能就会产生覆盖 for(int i = 0; i < nums.length; i++) { int temp = target - nums[i]; if (map.containsKey(temp)) { //定义了一个数组,并添加了元素 return new int[] { map.get(temp), i}; } map.put(nums[i], i); } return null; } }
与此类似的题目有LeetCode 15、18、16。
例2:以LeetCode 454为例,题目中已经给出了数据规模500,所以直接暴力解法即O(n^4)在一秒内肯定无法解决这种数量级问题,那么采用上面的思路,把数组D放入查找表中为O(n^3)这样的数量级也是无法接受的,那么完全可以把C与D数组组合之和放入查找表中,那么只需要遍历数组A与B即可,即O(n^2)的时间复杂度在n=500时这样的数据规模时完全可以接受的,其代码如下:
class Solution { public int fourSumCount(int[] A, int[] B, int[] C, int[] D) { // 首先把C与D组合之和放入查找表中,因为可能会有重复的数据所以应该放入map集合中 Map<Integer,Integer> map = new HashMap<>(); for (int i = 0; i < C.length; i++) { for (int j = 0; j < D.length; j++) { if (map.get(C[i]+D[j]) == null){ map.put(C[i]+D[j],1); }else{ map.put(C[i]+D[j],map.get(C[i]+D[j])+1); } } } // 最终结果 int res = 0; for (int i = 0; i < A.length; i++) { for (int j = 0; j < B.length; j++) { if (map.containsKey(0-A[i]-B[j])){ res += map.get(0-A[i]-B[j]); } } } return res; } }
与此类似的题目有 49
例3:以LeetCode 447为例,本题中同上面一样也是给出了数据规模,这样就提示我们暴力解法O(n^3)肯定是行不通的,应该想办法转换为O(n^2)来解决问题。可以考虑利用i是一个“枢纽”,对于每个点i,遍历其余点到i的距离,那么就是O(n^2)的时间复杂度了。利用一个查找表来保存距离i不同的点的值是数目,这样直接查表便可以得到有多少种组合了。
class Solution { public int numberOfBoomerangs(int[][] points) { int res = 0; for (int i = 0; i < points.length; i++) { Map<Integer,Integer> map = new HashMap<>(); for (int j = 0; j < points.length; j++) { if (j != i){ if (map.get(dis(points[i],points[j])) == null ){ map.put(dis(points[i],points[j]),1); }else{ map.put(dis(points[i],points[j]),map.get(dis(points[i],points[j]))+1); } } } for (Integer key : map.keySet()) { // 其实这个频率也可以不做判断,因为减1后就为了0了,不影响 if (map.get(key) >= 2){ res += map.get(key) * (map.get(key)-1); } } } return res; } // 如果算真正的距离就要涉及到开根号,这样会有浮点数的浮动可能会有误差,因此在这里不算开平方 // 当有一点要注意,计算出来的平方和是否超过了int的范围,这点可以根据题目进行判断 private Integer dis(int[] point, int[] point1) { return (point[0] - point1[0])*(point[0] - point1[0]) + (point[1]-point1[1])*(point[1]-point1[1]); } }
与此类似的题目有:LeetCode 149
3 滑动窗口+查找表
以LeetCode 219为例,题目中要求i与j的差的绝对值最大为k,其图示如下图所示,只要在[left...left+k]这个区间内找到两个相等的元素,那么其间距一定是小于等于k的。
那么问题就转换为了是否存在一个区间其区间内有两个相等的元素,这样的话便是滑动窗口问题了,一开始区间内是没有相等的两个元素的,这样去查找下一个元素,那么首先应该把左侧索引自增,然后在[left+1...left+k]这个区间内查找是否与红色部分相等的元素,一旦有便找到了答案,当没有时,便让右侧移动一位,对于查找的过程创建一个查找表便可以了。
class Solution { public boolean containsNearbyDuplicate(int[] nums, int k) { // 查找的时候不考虑顺序,为了性能因此创建set集合 Set<Integer> set = new HashSet<>(); for (int i = 0; i < nums.length; i++) { // 当滑动窗口中有重复的元素时便返回true if (set.contains(nums[i])){ return true; } // 查不到才添加元素 set.add(nums[i]); // 保持set中最多有k个元素,因为第k+1个是用来比较的 if (set.size() == k+1){ // 要移除掉最左侧的一个元素 set.remove(nums[i-k]); } } return false; } }
类似题目有:LeetCode 217
4 查找表的顺序性
例:LeetCode 220 本题比上一题目中多了一个限制条件即索引对应的元素之差最大不超过t,同样可以采用滑动窗口的方法进行遍历,如下图所示:
那么对于v而言在查找表中便是最大值为v+t,最小值为v-t,即如果查找表可以很快的找到最大值最小值那么这个问题很快就可以解决了。
class Solution { public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t){ // 查找的时候不考虑顺序,为了性能因此创建set集合,用long为了解决溢出问题 TreeSet<Long> set = new TreeSet<>(); for (int i = 0; i < nums.length; i++) { // 大于nums[i]-t的数值是存在的并且最小值要小于nums[i]+t,在这里有一个小的bug就是nums[i]+t可能会整型溢出,事实上测试用例中的确溢出了 if (set.ceiling((long)nums[i] - (long)t) != null && set.ceiling((long)nums[i] - (long)t) <= (long)nums[i]+(long)t){ return true; } // 查不到才添加元素 set.add((long)nums[i]); // 保持set中最多有k个元素,因为第k+1个是用来比较的 if (set.size() == k+1){ // 要移除掉最左侧的一个元素 set.remove((long)nums[i-k]); } } return false; } }
0