哈希表是一种键值对的存储。通过哈希函数,也就是一种特殊的映射函数,通过计算哈希值,将该键锁对应的值存储到相应的地址中。
x
为键 y
为值 len
为存储地址的最大值 则存储 y
的位置为 hash(x) % len
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
哈希碰撞 :
- 两个键经过哈希函数分配要存储的存储地址时,得到了相同的存储地址
hash(x) = hash(y)
避免哈希碰撞的方法的常用方法为 拉链法 Java Map 中使用的方法
常见哈希结构:
- Map
- Set
- 数组
Java 中的 Map
Map 是 一个接口 它的实现类有 HashMap
HashTable
SortedHashMap
LinkedHashMap
HashMap
:JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。详细可以查看:HashMap 源码分析。LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:LinkedHashMap 源码分析Hashtable
:数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的。TreeMap
:红黑树(自平衡的排序二叉树)。
Map 常用方法
Java 中的 Map
接口提供了一组丰富的方法来操作键值对集合。以下是一些常用的方法:
- put(K key, V value): 将指定的值与此 Map 中的指定键关联。如果键已经存在,则会替换旧值。
- get(Object key): 返回指定键所对应的值。如果键不存在,则返回
null
。 - remove(Object key): 移除指定键的映射关系。如果键存在,返回被移除的值,否则返回
null
。 - containsKey(Object key): 如果 Map 包含键的映射关系,则返回
true
。 - containsValue(Object value): 如果 Map 包含指定的值,则返回
true
。 - size(): 返回 Map 中键值对的数量。
- isEmpty(): 如果 Map 为空,则返回
true
。 - keySet(): 返回 Map 中所有键的 Set 视图。
- values(): 返回 Map 中所有值的 Collection 视图。
- entrySet(): 返回 Map 中所有键值对的 Set 视图。
- clear(): 移除 Map 中所有的键值对。
- putAll(Map<? extends K, ? extends V> m): 将指定 Map 中的所有映射关系复制到此 Map 中。
- equals(Object o): 比较此 Map 和指定对象是否相等。
- hashCode(): 返回 Map 的哈希码值。
这些方法提供了对 Map 集合的基本操作,包括添加、获取、删除元素以及检查集合的状态等。不同的 Map 实现可能会提供额外的方法或覆盖上述方法以提供特定的功能或性能优化。
遍历键、值、键值对
遍历键(Keys)
使用 keySet()
方法可以获取一个包含所有键的 Set
集合,然后可以对这个集合进行遍历。
Map<K, V> map = ...; // 你的Map实例
for (K key : map.keySet()) {
// 对每个键执行操作
}
或者使用迭代器遍历键:
Iterator<K> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
K key = iterator.next();
// 对每个键执行操作
}
遍历值(Values)
使用 values()
方法可以获取一个包含所有值的 Collection
集合,然后可以对这个集合进行遍历。
for (V value : map.values()) {
// 对每个值执行操作
}
或者使用迭代器遍历值:
Iterator<V> iterator = map.values().iterator();
while (iterator.hasNext()) {
V value = iterator.next();
// 对每个值执行操作
}
遍历键值对(Entries)
使用 entrySet()
方法可以获取一个包含所有键值对的 Set
集合,集合中的元素是 Map.Entry
对象。然后可以对这个集合进行遍历,并访问每个键值对。
for (Map.Entry<K, V> entry : map.entrySet()) {
K key = entry.getKey();
V value = entry.getValue();
// 使用键和值执行操作
}
或者使用迭代器遍历键值对:
Iterator<Map.Entry<K, V>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<K, V> entry = iterator.next();
K key = entry.getKey();
V value = entry.getValue();
// 使用键和值执行操作
}
这些遍历方法允许你对 Map
中的元素进行各种操作,例如搜索、修改或打印。选择哪种遍历方法取决于你的需求:如果你只需要键或值,可以使用 keySet()
或 values()
;如果你需要同时访问键和值,使用 entrySet()
是更好的选择。
Java Collections 中的 Set
Set的基础原理和 Map 一样 都是采用哈希函数
HashSet
(无序,唯一): 基于 HashMap
实现的,底层采用 HashMap
来保存元素。
LinkedHashSet
: LinkedHashSet
是 HashSet
的子类,并且其内部是通过 LinkedHashMap
来实现的。
TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)。
以下是 Set
接口的一些常用方法:
- add(E e): 向 Set 添加一个元素。如果 Set 已经包含该元素,则返回
false
,否则添加元素并返回true
。 - remove(Object o): 从 Set 中移除指定元素。如果元素存在并被成功移除,则返回
true
。 - contains(Object o): 检查 Set 是否包含指定的元素。
- size(): 返回 Set 中元素的数量。
- isEmpty(): 如果 Set 为空,则返回
true
。 - clear(): 移除 Set 中的所有元素。
- iterator(): 返回一个迭代器,用于遍历 Set 中的元素。
- toArray(): 将 Set 转换为数组。
- toArray(T[] a): 将 Set 转换为指定类型的数组。
- containsAll(Collection<?> c): 检查 Set 是否包含指定 Collection 中的所有元素。
- addAll(Collection<? extends E> c): 将指定 Collection 中的所有元素添加到 Set 中。
- retainAll(Collection<?> c): 保留 Set 中在指定 Collection 中存在的元素,移除其他所有元素。
- removeAll(Collection<?> c): 从 Set 中移除在指定 Collection 中存在的所有元素。
- equals(Object o): 检查指定对象是否与此 Set 相等。
- hashCode(): 返回 Set 的哈希码。
这些方法提供了对 Set 集合的基本操作,包括添加、删除、检查元素、获取大小、清空集合以及转换为数组等。不同的 Set 实现(如 HashSet
, TreeSet
, LinkedHashSet
等)可能会提供额外的方法或覆盖上述方法以提供特定的功能或性能优化。
Map
接口在 Java 8 中引入了几个 default
方法,以下是一些例子:
V getOrDefault(Object key, V defaultValue)
:- 如果指定的键存在于映射中,则返回其对应的值;如果不存在,则返回提供的默认值。
void forEach(BiConsumer<? super K, ? super V> action)
:- 对映射中的每个键值对执行给定的操作。
void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
:- 用给定函数提供的新值替换映射中的每个键值对的值。
V putIfAbsent(K key, V value)
:- 如果映射中不存在指定键的映射,则添加键值对并返回
null
;如果存在,则返回现有的值。
- 如果映射中不存在指定键的映射,则添加键值对并返回
boolean remove(Object key, Object value)
:- 如果映射中存在指定键和值的映射,则移除它并返回
true
;否则返回false
。
- 如果映射中存在指定键和值的映射,则移除它并返回
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
:- 如果映射中不存在指定键的映射,则使用提供的映射函数计算其值并添加到映射中。
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
:- 如果映射中存在指定键的映射,则使用提供的重映射函数重新计算其值。
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
:- 如果映射中存在指定键的映射,则使用提供的合并函数重新计算其值;如果不存在,则添加键值对。
用字典记录出现的次数,再用遍历另一个,出现一次减去一个次数
class Solution {
public boolean isAnagram(String s, String t) {
Map<Character,Integer> dict = new HashMap<>();
//遍历c,记录每个字符出现次数
for(Character c : s.toCharArray()){
if(dict.containsKey(c)){
dict.put(c , dict.get(c) + 1);
}else{
dict.put(c, 1);
}
}
for(Character c : t.toCharArray()){
if(!dict.containsKey(c))return false;
dict.put(c,dict.get(c) - 1);
}
for (Map.Entry<Character, Integer> entry : dict.entrySet()) {
if(entry.getValue() != 0)return false;
}
return true;
}
}
/**
* 242. 有效的字母异位词 字典解法
* 时间复杂度O(m+n) 空间复杂度O(1)
*/
class Solution {
public boolean isAnagram(String s, String t) {
int[] record = new int[26];
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i) - 'a']++; // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
}
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i) - 'a']--;
}
for (int count: record) {
if (count != 0) { // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
return true; // record数组所有元素都为零0,说明字符串s和t是字母异位词
}
}
349. 两个数组的交集
用 HashSet
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {return new int[0];}
Set<Integer> set1 = new HashSet<>();
Set<Integer> resSet = new HashSet<>();
//遍历数组1
for (int i : nums1) {
set1.add(i);
}
//遍历数组2的过程中判断哈希表中是否存在该元素
for (int i : nums2) {
if (set1.contains(i)) {
resSet.add(i);
}
}
//将结果集合转为数组
return resSet.stream().mapToInt(x -> x).toArray();
}
}
202. 快乐数
快乐数, 不快乐的数会循环,放到set中,如果循环了就会察觉到,直接返回false
class Solution {
public boolean isHappy(int n) {
int num = n;
int sqrnum = 0;
int sum = 0;
Set<Integer> set = new HashSet<>();
while(sqrnum != 1){
while(num > 0){
int a = num%10;
sum += (a*a);
System.out.println("sum:"+sum);
num = num/10;
System.out.println("num:"+num);
}
if(set.contains(sum))return false;
else set.add(sum);
num = sum;
sqrnum = sum;
sum = 0;
}
return true;
}
}
###1. 两数之和
两数之和
- 创建字典
- 遍历数组 并查找字典中有没有
target - nums[i]
的键 , 有就返回数组 - 没有就下一个,到最后都没有 就返回空数组
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2] ;
if(nums == null || nums.length == 0){
return res;
}
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0;i < nums.length ; i++){
int temp = target - nums[i];
if(map.containsKey(temp)){
res[0] = i;
res[1] = map.get(temp);
break;
}
map.put(nums[i],i);
}
return res;
}
}