LeetCode 面试经典 150 题-哈希表
哈希表
- 对magzine中的字符做统计。
- 遍历ransomNote字符串,对出现的字符,哈希表-1,如果出现小于0的情况则返回false,否则最后返回true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public boolean canConstruct(String ransomNote, String magazine) { int[] cnt = new int[26]; for (int i = 0; i < magazine.length(); i++) { cnt[magazine.charAt(i) - 'a']++; } for (int i = 0; i < ransomNote.length(); i++) { cnt[ransomNote.charAt(i) - 'a']--; if (cnt[ransomNote.charAt(i) - 'a'] < 0) { return false; } }
return true; } }
|
- 时间复杂度:O(m + n)
- 空间复杂度:$O(|\Sigma|)$
哈希表
考虑统计两字符串不同字符的个数,经测试会有测试点不通过:s =”bbbaaaba”,t =”aaabbbba”。t =
“aaabbbba”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public boolean isIsomorphic(String s, String t) { int[] hash = new int[128]; int cntS = 0; for (int i = 0; i < s.length(); i++) { if (hash[s.charAt(i)] == 0) { cntS++; } hash[s.charAt(i)]++; } Arrays.fill(hash, 0); int cntT = 0; for (int i = 0; i < t.length(); i++) { if (hash[t.charAt(i)] == 0) { cntT++; } hash[t.charAt(i)]++; } return cntS == cntT; } }
|
- 维护两张哈希表,第一张哈希表 s2t 以 s 中字符为键,映射至 t 的字符为值,第二张哈希表 t2s 以 t 中字符为键,映射至 s 的字符为值。
- 从左至右遍历两个字符串的字符,不断更新两张哈希表,如果出现冲突(即当前下标 index 对应的字符 s[index] 已经存在映射且不为 t[index] 或当前下标 index 对应的字符 t[index] 已经存在映射且不为 s[index])时说明两个字符串无法构成同构,返回 false。
- 如果遍历结束没有出现冲突,则表明两个字符串是同构的,返回 true 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public boolean isIsomorphic(String s, String t) { Map<Character, Character> s2t = new HashMap<>(); Map<Character, Character> t2s = new HashMap<>();
int n = s.length(); for (int i = 0; i < n; i++) { char x = s.charAt(i); char y = t.charAt(i); if ((s2t.containsKey(x) && s2t.get(x) != y) || (t2s.containsKey(y) && t2s.get(y) != x)) { return false; } s2t.put(x, y); t2s.put(y, x); }
return true; } }
|
- 时间复杂度:O(n)
- 空间复杂度:$O(|\Sigma|)$
哈希表
与上一题类似,但对应关系变为character -> string。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Solution { public boolean wordPattern(String pattern, String s) { String[] words = s.split("\\s+"); if (pattern.length() != words.length) { return false; }
Map<Character, String> p2s = new HashMap<>(); Map<String, Character> s2p = new HashMap<>();
for (int i = 0; i < pattern.length(); i++) { char x = pattern.charAt(i); String y = words[i];
if (p2s.containsKey(x) && !p2s.get(x).equals(y) || s2p.containsKey(y) && !s2p.get(y).equals(x)) { return false; }
p2s.put(x, y); s2p.put(y, x); }
return true; } }
|
注意判断字符串是否相等要用.equals方法而不能直接用==。
- 时间复杂度:O(m + n)
- 空间复杂度:O(m + n),最坏情况下,我们需要存储 pattern 中的每一个字符和 str 中的每一个字符串。
哈希表
判断两个字符串的哈希情况是否一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { public boolean isAnagram(String s, String t) { int m = s.length(); int n = t.length(); if (m != n) { return false; }
int[] hash = new int[26]; for (int i = 0; i < m; i++) { hash[s.charAt(i) - 'a']++; } for (int i = 0; i < n; i++) { hash[t.charAt(i) - 'a']--; if (hash[t.charAt(i) - 'a'] < 0) { return false; } }
for (int i = 0; i < 26; i++) { if (hash[i] != 0) { return false; } } return true; } }
|
- 时间复杂度:O(n)
- 空间复杂度:$O(|\Sigma|)$
排序
- 把每个单词进行排序,将排序后的字符串作为键,字符串作为值加入到该键对应的字符串数组中。
- 遍历上面的哈希表,加入结果集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution { public List<List<String>> groupAnagrams(String[] strs) { Map<String, List<String>> hash = new HashMap<>(); for (String str : strs) { char[] charArray = str.toCharArray(); Arrays.sort(charArray); String sortedStr = new String(charArray); if (!hash.containsKey(sortedStr)) { hash.put(sortedStr, new ArrayList<>()); } hash.get(sortedStr).add(str); } return new ArrayList<>(hash.values()); } }
|
- 时间复杂度:O(nklogk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。
- 空间复杂度:O(nk)
计数
- 对于每个字符串,首先使用哈希表统计每个字母出现的频率,然后将频率表转换为字符串作为键。
- 这样处理完之后就得到与上一种方法类似的哈希表。最后将哈希表中的结果移到结果数组中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Solution { public List<List<String>> groupAnagrams(String[] strs) { Map<String, List<String>> hash = new HashMap<>(); for (String str : strs) { int[] cnt = new int[26]; for (int i = 0; i < str.length(); i++) { cnt[str.charAt(i) - 'a']++; } StringBuffer sb = new StringBuffer(); for (int i = 0; i < 26; i++) { sb.append(cnt[i]); sb.append('#'); } String key = sb.toString(); if (!hash.containsKey(key)) { hash.put(key, new ArrayList<String>()); } hash.get(key).add(str); } return new ArrayList<>(hash.values()); } }
|
- 时间复杂度:O(n(k+∣Σ∣))
- 空间复杂度:O(n(k+∣Σ∣))
排序+双指针
之前做过两数、三数之和,四数之和与三数之和的思路类似,都是先排序,固定数,然后剩下两个数利用双指针找。
- 每一种循环枚举到的下标必须大于上一重循环枚举到的下标;
- 同一重循环中,如果当前元素与上一个元素相同,则跳过当前元素。
一些剪枝操作:
- 在确定第一个数之后,如果 nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target,说明此时剩下的三个数无论取什么值,四数之和一定大于 target,因此退出第一重循环;
- 在确定第一个数之后,如果 nums[i]+nums[n−3]+nums[n−2]+nums[n−1]<target,说明此时剩下的三个数无论取什么值,四数之和一定小于 target,因此第一重循环直接进入下一轮,枚举 nums[i+1];
- 在确定前两个数之后,如果 nums[i]+nums[j]+nums[j+1]+nums[j+2]>target,说明此时剩下的两个数无论取什么值,四数之和一定大于 target,因此退出第二重循环;
- 在确定前两个数之后,如果 nums[i]+nums[j]+nums[n−2]+nums[n−1]<target,说明此时剩下的两个数无论取什么值,四数之和一定小于 target,因此第二重循环直接进入下一轮,枚举 nums[j+1]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| class Solution { public List<List<Integer>> fourSum(int[] nums, int target) { List<List<Integer>> res = new ArrayList<>(); int n = nums.length; if (n < 4) { return res; } Arrays.sort(nums); for (int i = 0; i < n - 3; i++) { if (i > 0 && nums[i] == nums[i-1]) { continue; } if ((long) nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target) { break; } if ((long) nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target) { continue; } for (int j = i + 1; j < n - 2; j++) { if (j > i + 1 && nums[j] == nums[j-1]) { continue; } if ((long) nums[i] + nums[j] + nums[j+1] + nums[j+2] > target) { break; } if ((long) nums[i] + nums[j] + nums[n-2] + nums[n-1] < target) { continue; } int left = j + 1, right = n - 1; while (left < right) { long sum = (long) nums[i] + nums[j] + nums[left] + nums[right]; if (sum == target) { res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right])); while (left < right && nums[left] == nums[left+1]) { left++; } left++; while (left < right && nums[right] == nums[right-1]) { right--; } right--; } else if (sum < target) { left++; } else { right--; } } } }
return res; } }
|
- 时间复杂度:$O(n^3)$
- 空间复杂度:O(logn)
注意求和的地方需要转为long,否则会溢出。
哈希表
不断对数字的每一位进行平方求和,如果这个数不是快乐数,那么它一定会产生循环。使用哈希表存储已经访问过的情况,如果再次遇到这个数字,就说明出现循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public boolean isHappy(int n) { Set<Integer> set = new HashSet<>(); set.add(n); while (n != 1) { int num = 0; while (n != 0) { num += (n % 10) * (n % 10); n /= 10; } n = num; if (set.contains(n)) { return false; } set.add(n); } return true; } }
|
- 时间复杂度:O(logn)
- 空间复杂度:O(logn)
快慢指针法
可以看成是链表,起始数字是链表的头 “节点”,链中的所有其他数字都是节点。问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution { public boolean isHappy(int n) { int slow = n, fast = getNext(n); while (fast != 1 && slow != fast) { slow = getNext(slow); fast = getNext(getNext(fast)); } return fast == 1; }
public int getNext(int n) { int num = 0; while (n > 0) { int d = n % 10; n /= 10; num += d * d; } return num; } }
|
哈希表
一边遍历数组,一边把索引存到哈希表中。如果当前元素已经存在于哈希表,则判断其索引值之差的绝对值是否小于等于k,如果小于直接返回true,否则更新索引为当前元素下标,因为需要索引差尽可能接近。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Solution { public boolean containsNearbyDuplicate(int[] nums, int k) { int n = nums.length; Map<Integer, Integer> hash = new HashMap<>(); for (int i = 0; i < n; i++) { if (hash.containsKey(nums[i]) && Math.abs(i - hash.get(nums[i])) <= k) { return true; } hash.put(nums[i], i); } return false; } }
|
滑动窗口
考虑数组 nums 中的每个长度不超过 k+1 的滑动窗口,同一个滑动窗口中的任意两个下标差的绝对值不超过 k。如果存在一个滑动窗口,其中有重复元素,则存在两个不同的下标 i 和 j 满足 nums[i]=nums[j] 且 ∣i−j∣≤k。如果所有滑动窗口中都没有重复元素,则不存在符合要求的下标。因此,只要遍历每个滑动窗口,判断滑动窗口中是否有重复元素即可。
如果一个滑动窗口的结束下标是 i,则该滑动窗口的开始下标是 max(0,i−k)。可以使用哈希集合存储滑动窗口中的元素。从左到右遍历数组 nums,当遍历到下标 i 时,具体操作如下:
- 如果 i>k,则下标 i−k−1 处的元素被移出滑动窗口,因此将 nums[i−k−1] 从哈希集合中删除;
- 判断 nums[i] 是否在哈希集合中,如果在哈希集合中则在同一个滑动窗口中有重复元素,返回 true,如果不在哈希集合中则将其加入哈希集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public boolean containsNearbyDuplicate(int[] nums, int k) { int n = nums.length; Set<Integer> set = new HashSet<>(); for (int i = 0; i < n; i++) { if (i > k) { set.remove(nums[i-k-1]); } if (!set.add(nums[i])) { return true; } } return false; } }
|
哈希表
- 用集合进行去重
- 遍历集合:
- 确定序列起始,即判断当前数字-1是否存在于集合中,如果不存在,则当前数字就是序列起始;
- 更新连续序列长度;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Solution { public int longestConsecutive(int[] nums) { Set<Integer> set = new HashSet<>(); int n = nums.length; for (int i = 0; i < n; i++) { set.add(nums[i]); } int ans = 0; for (int num : set) { if (!set.contains(num - 1)) { int curNum = num, curLen = 1; while (set.contains(curNum + 1)) { curNum++; curLen++; } ans = Math.max(ans, curLen); } }
return ans;
} }
|
- 时间复杂度:O(n),外层循环需要 O(n) 的时间复杂度,只有当一个数是连续序列的第一个数的情况下才会进入内层循环,然后在内层循环中匹配连续序列中的数,因此数组中的每个数只会进入内层循环一次。
- 空间复杂度:O(n)
补充
Java的集合(Collection)是Java提供的一种数据结构,用于存储和操作数据。集合框架提供了一种高效、灵活的方式来处理对象的数据,可以替代数组等传统的数据结构。Java集合框架包括了各种不同的接口和类,可以满足不同的数据存储和操作需求。
Java集合框架的核心接口包括:
1. Collection接口
Collection
是所有集合类的根接口。它定义了常见的集合操作,如添加、删除、迭代等。Collection
接口的子接口包括Set
、List
、Queue
等。
2. List接口
List
是一个有序的集合,允许元素重复。元素可以根据索引访问。List
接口的常见实现类有:
- ArrayList:基于动态数组实现,支持快速随机访问,但在插入或删除元素时性能较差。
- LinkedList:基于双向链表实现,插入和删除操作较快,但访问元素时速度较慢。
- Vector:类似于
ArrayList
,但它是同步的,因此线程安全,性能相对较差,通常不推荐使用。
3. Set接口
Set
是一个不允许重复元素的集合。Set
接口的常见实现类有:
- HashSet:基于哈希表实现,插入和查找效率较高,但元素的顺序是不确定的。
- LinkedHashSet:基于哈希表和链表实现,保证元素的插入顺序。
- TreeSet:基于红黑树实现,支持排序和快速查找。
4. Queue接口
Queue
是一个按先进先出(FIFO)原则处理元素的集合。它适合用来实现任务调度、消息队列等。常见的Queue
实现类有:
- LinkedList:既是
List
的实现类,又实现了Queue
接口,可以作为队列使用。
- PriorityQueue:基于优先级堆实现,队列中的元素按优先级顺序排列。
Deque(双端队列,Double-Ended Queue)是一个允许在两端(头部和尾部)进行插入和删除操作的队列。与传统的队列(Queue)不同,Deque 允许在两端进行插入和删除,因此它既可以作为队列(FIFO)使用,也可以作为栈(LIFO)使用。
在 Java 中,Deque 是 java.util 包下的一个接口,继承自 Queue,并提供了双端队列的相关方法。它的常见实现类包括 ArrayDeque 和 LinkedList。
5. Map接口
Map
是一个映射关系接口,用于存储键值对(key-value)。Map
不继承自Collection
接口,因此是一个独立的接口。Map
接口的常见实现类有:
- HashMap:基于哈希表实现,允许
null
作为键和值,元素无顺序保证。
- LinkedHashMap:与
HashMap
类似,但保持插入顺序。
- TreeMap:基于红黑树实现,键值对按键的自然顺序或自定义顺序排序。
- Hashtable:类似于
HashMap
,但它是同步的,因此线程安全,但性能较差,通常不推荐使用。
6. SortedSet和SortedMap接口
SortedSet
和SortedMap
接口分别是Set
和Map
的有序版本,提供对元素的排序支持。
- SortedSet:如
TreeSet
,可以保证集合中的元素有序。
- SortedMap:如
TreeMap
,可以保证Map
中的键有序。
7. Iterator接口
Iterator
接口用于遍历集合中的元素。几乎所有的集合都提供了iterator()
方法,返回一个Iterator
对象。Iterator
有三大方法:
hasNext()
:检查是否有下一个元素。
next()
:返回下一个元素。
remove()
:移除当前元素(仅在支持此操作的集合中有效)。
8. 常见的集合操作
集合框架提供了多种操作数据的方法,常见的操作包括:
- 添加元素:
add(E e)
(Set
、List
等),put(K key, V value)
(Map
)。
- 删除元素:
remove(Object o)
(Set
、List
等),remove(Object key)
(Map
)。
- 查找元素:
contains(Object o)
(Set
、List
等),containsKey(Object key)
(Map
)。
- 遍历元素:使用
Iterator
接口或增强for
循环。
- 大小:
size()
。
- 清空集合:
clear()
。
9. 集合与泛型
Java集合框架广泛使用了泛型,使得集合能够存储任何类型的对象,并且能够在编译时提供类型检查。例如:
1 2 3
| List<String> list = new ArrayList<>(); list.add("Hello"); list.add("World");
|
上面的代码使用了泛型,List
只能存储String
类型的数据,避免了类型转换错误。
10. 集合工具类
Collections
类是一个工具类,提供了多种静态方法来操作集合,例如排序、查找、反转等。
Collections.sort(List<T> list)
:对List
进行排序。
Collections.shuffle(List<?> list)
:对List
进行随机打乱。
Collections.reverse(List<?> list)
:反转List
的元素顺序。
Collections.max(Collection<? extends T> coll)
:返回集合中的最大元素。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import java.util.*;
public class CollectionExample { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Java"); list.add("Python"); list.add("C++"); System.out.println("List: " + list);
Set<String> set = new HashSet<>(); set.add("Apple"); set.add("Banana"); set.add("Apple"); System.out.println("Set: " + set);
Map<String, Integer> map = new HashMap<>(); map.put("Alice", 30); map.put("Bob", 25); map.put("Charlie", 35); System.out.println("Map: " + map);
Queue<Integer> queue = new PriorityQueue<>(); queue.add(5); queue.add(2); queue.add(8); System.out.println("Queue: " + queue);
System.out.println("Iterating over list:"); for (String item : list) { System.out.println(item); } } }
|
总结
Java集合框架提供了丰富的数据结构和接口,能够满足不同的应用场景。选择合适的集合类型和方法,可以提高代码的效率和可读性。在实际开发中,合理地选择合适的集合类型及其操作是非常重要的。