文章目录
LeetCode精选题之哈希表
总结:
1、在解题中对Set和Map的使用,Set只能存储元素,如果要保存位置,次数等信息就需要使用Map。
2、灵活选择键key,应该根据题目来定,比如距离,和,元素等等,进行灵活选择。
1 两数之和–LeetCode1
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
import java.util.HashMap;
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int diff = target - nums[i];
if (map.containsKey(diff)) {
res[0] = map.get(diff);
res[1] = i;
return res;
}else {
map.put(nums[i], i);
}
}
return res;
}
}
2 存在重复元素–LeetCode217
给定一个整数数组,判断是否存在重复元素。如果任意一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。
class Solution {
public boolean containsDuplicate(int[] nums) {
if (nums == null || nums.length == 0) {
return false;
}
HashSet<Integer> set = new HashSet<>();
for (int num : nums) {
if (set.contains(num)) {
return true;
}
set.add(num);
}
return false;
}
}
3 存在重复元素II–LeetCode219
给定一个整数数组和一个整数 k
,判断数组中是否存在两个不同的索引i
和 j
,使得 nums [i] = nums [j]
,并且 i
和j
的差的 绝对值 至多为 k
。
示例 1:
输入: nums = [1,2,3,1], k = 3
输出: true
示例 2:
输入: nums = [1,0,1,1], k = 1
输出: true
示例 3:
输入: nums = [1,2,3,1,2,3], k = 2
输出: false
思路:滑动窗口+查找表
注意:这里的滑动窗口是固定长度的滑动窗口。
import java.util.HashSet;
class Solution {
public boolean containsNearbyDuplicate(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return false;
}
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
if (set.contains(nums[i])) {
return true;
}
set.add(nums[i]);
if (set.size() == k+1) {
set.remove(nums[i-k]);
}
}
return false;
}
}
4 存在重复元素III–LeetCode220(Medium)
题目:给定一个整数数组,判断数组中是否有两个不同的索引 i
和 j
,使得 nums [i]
和 nums [j]
的差的绝对值最大为 t
,并且 i
和 j
之间的差的绝对值最大为 ķ
。
示例 1:
输入: nums = [1,2,3,1], k = 3, t = 0
输出: true
示例 2:
输入: nums = [1,0,1,1], k = 1, t = 2
输出: true
示例 3:
输入: nums = [1,5,9,1,5,9], k = 2, t = 3
输出: false
代码如下:
import java.util.TreeSet;
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
if (nums == null || nums.length == 0) {
return false;
}
TreeSet<Long> set = new TreeSet<>();
for (int i = 0; i < nums.length; i++) {
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]);
if (set.size() >= k+1) {
set.remove((long)nums[i-k]);
}
}
return false;
}
}
总结:
1、这道题目涉及到元素的差值,就要求Set中的元素是有序的,所以使用TreeSet,对TreeSet的使用不熟悉。这里用到了ceiling()方法,对应还有floor()方法。
2、自己考虑的时候考虑的条件是set中最小值(first()方法)是否小于nums[i]+t
,以及最大值(last()方法)是否大于nums[i]-t
,这样考虑是不行的。一定要理解题目的意思,只要有一个数在[nums[i]-t, nums[i]+t]
范围内就可以了,所以考虑的是set.ceiling(nums[i]-t)
,TreeSet的ceiling(E e)方法是返回大于或者等于e的最小元素。
3、对数字越界的考虑不够,所以有一个特殊用例没通过,也就是t=2147483647的时候发生了错误,所以要采用long型。
5 最长和谐子序列–LeetCode594
和谐数组是指一个数组里元素的最大值和最小值之间的差别正好是1。现在,给定一个整数数组,你需要在所有可能的子序列中找到最长的和谐子序列的长度。
示例 1:
输入: [1,3,2,2,5,2,3,7]
输出: 5
原因: 最长的和谐数组是:[3,2,2,2,3].
说明: 输入的数组长度最大不超过20,000.
思路:先用哈希表存储每个数字出现的次数,然后对于哈希表里面的每个键,查看哈希表里面是否存在该键加一的键值,如果存在,计算并比较该和谐子序列是否为最长的序列。
class Solution {
public int findLHS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, 1+map.get(num));
}else {
map.put(num, 1);
}
}
int res = 0;
for (int key : map.keySet()) {
if (map.containsKey(key+1)) {
res = Math.max(res, map.get(key)+map.get(key+1));
}
}
return res;
}
}
6 最长连续序列–LeetCode128(Hard)
给定一个未排序的整数数组,找出最长连续序列的长度。要求算法的时间复杂度为 O(n)。
示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
思路一:用哈希表存储每个元素所在的连续区间的长度,即键为数组元素,值为元素所在的连续区间的长度。若元素已在哈希表中,表示是重复元素,直接跳过;若是新的元素:
- 取出其左右相邻数已有的连续区间长度
left
和right
- 计算当前数的区间长度:
currLen = left + right + 1
- 根据
currLen
更新最大长度longestLen
的值 - 更新区间两端点的连续序列长度值
参考思路:jalan的题解
代码如下:
class Solution {
public int longestConsecutive(int[] nums) {
HashMap<Integer, Integer> map = new HashMap<>();
int longestLen = 0;
for (int num : nums) {
if (!map.containsKey(num)) {
int left = map.get(num-1)==null ? 0 : map.get(num-1);
int right = map.get(num+1)==null ? 0 : map.get(num+1);
int currLen = 1+left+right;
longestLen = Math.max(longestLen, currLen);
map.put(num, currLen);
map.put(num-left, currLen);//更新区间两端点的连续序列长度值
map.put(num+right, currLen);
}
}
return longestLen;
}
}
其中map.put(num, currLen)
是用于表示这个元素已经被访问过,因为区间长度用两端的元素来确定就可以了,故map.put(num,1)
也是没问题的。
思路二:使用HashSet来保存数组元素,实现O(1)时间复杂度的查找。其次若当前元素为curr
,只有当curr-1
不在Set中的时候,才将curr
作为连续序列的第一个数字去查找。解释:如果curr-1
在Set中,那么curr
肯定不是连续序列的第一个数字。
代码如下:
class Solution {
public int longestConsecutive(int[] nums) {
HashSet<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}
int longest = 0;
for (int num : nums) {
if (!set.contains(num-1)) {
int currNum = num;
int currCnt = 1;
while (set.contains(currNum+1)) {
currNum++;
currCnt++;
}
longest = Math.max(longest, currCnt);
}
}
return longest;
}
}
复杂度分析:
- 时间复杂度:
O(n)
。尽管在 for 循环中嵌套了一个 while 循环,时间复杂度看起来像是二次方级别的。但其实它是线性的算法。因为只有当currNum
遇到了一个序列的开始, while 循环才会被执行(也就是currNum-1
不在数组nums
里), while 循环在整个运行过程中只会被迭代n
次。这意味着尽管看起来时间复杂度为O(n*n)
,实际这个嵌套循环只会运行O(n+n)=O(n)
次。所有的计算都是线性时间的,所以总的时间复杂度是O(n)
的。 - 空间复杂度:
O(n)
。为了实现O(1)
的查询,我们对哈希表分配线性空间,以保存 nums 数组中的O(n)
个数字。除此以外,所需空间与暴力解法一致。
参考题解:LeetCode官方题解