哈希表理论基础
哈希表是根据关键码的值而直接进行访问的数据结构。
一般哈希表都是用来快速判断一个元素是否出现集合里。
如果使用哈希表的话, 只需要O(1)就可以做到查询一个元素。
哈希函数
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
哈希碰撞
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法
相当于数组+链表的结合。
需要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。
冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。
java中常见的哈希结构
- 数组
- set(集合)
- map(映射)
HashMap
在 HashMap 中的加载因子为 0.75。
- HashMap是基于哈希表的Map接口的非同步实现,
- 允许使用null值和null键(HashMap最多只允许一条记录的键为null,允许多条记录的值为null。)。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
- HashMap中不允许出现重复的键(Key)
- Hashmap是非线程安全的,
- 其迭代器是fail-fast的
- HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体,(JDK1.8增加了红黑树部分,会将时间复杂度从O(n)降为O(logn))。
- 数据存储:先根据key的hashCode(使用key的hashCode()方法获取)重新计算hash值,根据hash值算出这个元素在数组中的位置(即下标), 如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
- 数据读取:首先根据key的hashCode,找到其数组中对应位置的数据(可能只有一个数据,也可能是多个数据,其表现形式是一个链表),然后通过key的equals方法在对应位置的链表中找到需要的元素。
- hashMap的默认初始容量是16个,其会有一个负载因子,用于当hashMap中的数据量等于容量*负载因子时,hashMap会进行扩容,扩大的容量是原本的2倍。负载因子的默认初始值为0.75
HashSet
- 它是基于HashMap实现的,底层采用HashMap来保存元素,而且只使用了HashMap的key来实现各种特性。HashSet实现了Set接口
- HashSet较HashMap来说比较慢
- HashSet中的数据不是key-value键值对,其只是单值,虽然其借助与HashMap来实现,但是其只是将值作为key来存入HashMap中,因为HashMap中的值是key-value键值对的,所以每个HashSet存储到HashMap的数据对应的value值只是一个new Object()对象
- 当添加数据时,如果set中尚未包含指定元素,则添加指定元素。更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2))的元素e2,则向此set 添加指定的元素e。如果此set已包含该元素,则该调用不更改set并返回false。但底层实际将将该元素作为key放入HashMap。
Hashtable
- Hashtable也是一个散列表,它存储的内容是键值对。基于Dictionary类
- 存储数据: 首先判断value是否为空,为空则抛出异常;计算key的hash值,并根据hash值获得key在table数组中的位置index,如果table[index]元素不为空,则进行迭代,如果遇到相同的key,则直接替换,并返回旧value;否则,我们可以将其插入到table[index]位置。
- key和value都不允许为null,Hashtable遇到null,直接返回NullPointerException。
- 线程安全,几乎所有的public的方法都是synchronized的
- 较HashMap速度慢
LinkedHashMap
- LinkedHashMap是HashMap的一个子类,它保留插入顺序,帮助我们实现了有序的HashMap。
- 其维护一个双向链表,并不是说其除了维护存入的数据,另外维护了一个双向链表对象,而是说其根据重写HashMap的实体类Entry,来实现能够将HashMap的数据组成一个双向列表,其存储的结构还是数组+链表的形式,
- LinkedHashMap能够做到按照插入顺序或者访问顺序进行迭代顺序。
- 修改Entry对象,Entry新增了其上一个元素before和下一个元素after的引用
- 根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表
- LinkedHashMap并未重写父类HashMap的put方法,只是重写了put方法里面的recordAccess、addEntry、createEntry等方法,添加了特有的双向链接列表
- LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。
- 这个虽然是访问元素,但是当设置以访问排序时,其仍然会先将元素从原本位置remove掉,然后在将该元素以新元素插入到链头,哪怕其新插入的位置还在原位置(所以如果以访问排序,其过程会涉及到删除数据和增添数据)。
- 读取速度与容量无关
TreeMap
- TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。 该映射根据其键的自然顺序(字母排序)进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
- TreeMap是非线程安全的。 它的iterator 方法返回的迭代器是fail-fast的。
ConcurrentHashMap
- ConcurrentHashMap是弱一致性,也就是说遍历过程中其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据
- ConcurrentHashMap是基于分段锁设计来实现线程安全性,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。
- 并发度就是ConcurrentHashMap中的分段锁个数,默认的并发度为16,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)
- 通过将key的高n位(n = 32 – segmentShift)和并发度减1(segmentMask)做位与运算定位到所在的Segment(分段)
HashMap相关方法
1.1 初始化
HashMap<object1, object2> hashmap = new HashMap<object1, object2>();
1.2 添加新键值对
hashmap.put(object1, object2);
1.3 在原有的基础上增加
hashmap.put(object1, hashmap.getOrDefault((object1, 0) + 1));
1.4 访问元素的value
object2 obj2 = hashmap.get(object1);
1.5 判断存在
boolean contains(Object value) ——-确定哈希表内是否包含了给定的对象,若有返回true,否则false
Boolean bool = hashmap.containsKey( Object key );
Boolean bool = hashmap.containsValue( Object value );
1.6 删除元素
hashmap.remove(object1);
1.7 清空元素
hashmap.clear();
1.8 计算大小
int size = hashmap.size();
1.9 遍历
for(object1 obj1 : hashmap.keySet()){
System.out.println(obj1 + hashmap.get(obj1));
}
for(object2 obj2 : hashmap.values()){
System.out.println(obj2);
}
1.10 判断为空
Boolean bool = hashmap.isEmpty();
HashSet相关方法
HashSet继承自Set接口
- 初始化
Set<Integer> set1 = new HashSet<>();
- 添加元素
set1.add(i);
- 将set集合转为数组
resSet.stream().mapToInt(x -> x).toArray();
- 判断set中是否存在该元素
set1.contains(i)
- set集合元素的个数
set1.size()
242.有效的字母异位词
题目链接:https://leetcode.cn/problems/valid-anagram/
视频链接:https://www.bilibili.com/video/BV1YG411p7BA/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689
讲解链接:https://programmercarl.com/0242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
暴力
两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。
思路
因为都是小写字母,考虑用一个长度为26的数组,a~z
对应hash[0,25]
第一遍for循环先统计s中每个字符出现的次数,第二次for遍历t,对应字符出现一次 就让hash[]--
。最后看hash数组是否全为0,是就说明是有效的字母异位词。
hash[s[i] - 'a']++; //遍历s的操作
hash[t[i] - 'a']--; //遍历t的操作
- 完整java代码:
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']++;
}
for(int i = 0; i < t.length(); i++) {
record[t.charAt(i) - 'a']--;
}
for(int count : record) {
if(count != 0) {
return false;
}
}
return true;
}
}
349.两个数组的交集
题目链接:https://leetcode.cn/problems/intersection-of-two-arrays/description/
视频链接:https://www.bilibili.com/video/BV1ba411S7wu/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689
讲解链接:https://programmercarl.com/0349.%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E7%9A%84%E4%BA%A4%E9%9B%86.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
思路
题目说length<=1000,其实用数组也可以。但是要求去重,所以用HashSet
先对nums1进行处理,存放到哈希表里,然后遍历nums2,如果哈希表中也出现过,就放到result集合里,注意是去重的。
需要注意 如果有一个数组为null,就返回一个空数组!
- 数组解法:
hash[nums1.charAt(i)] = 1;
- java代码(用set)
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
if(nums1 == null || nums2 == null) {
return new int[0];
}
Set<Integer> retSet = new HashSet<>();
Set<Integer> set = new HashSet<>();
for(int i : nums1) {
set.add(i);
}
for(int i : nums2) {
if(set.contains(i)) {
retSet.add(i);
}
}
//新申请一个数组用来返回
int[] result = new int[retSet.size()];
int j = 0;
for(int i : retSet) {
result[j++] = i;
}
return result;
}
}
- 版本二:用数组
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash1 = new int[1000];
int[] hash2 = new int[1000];
for(int i : nums1) {
hash1[i]++;
}
for(int i : nums2) {
hash2[i]++;
}
//动态添加元素 用ArrayList
List<Integer> result = new ArrayList<>();
for(int i = 0; i < 1000; i++) {
if(hash1[i] > 0 && hash2[i] > 0) {
result.add(i); //注意i代表的是重复了的元素 hash1[i]是出现的次数
}
}
//最后还要转成数组返回
int[] res = new int[result.size()];
int j = 0;
for(int i : result) {
res[j++] = i;
}
return res;
}
}
202.快乐数
题目链接:https://leetcode.cn/problems/happy-number/
讲解链接:https://programmercarl.com/0202.%E5%BF%AB%E4%B9%90%E6%95%B0.html
思路
题目中说平方和可能会无限循环
,所以只要一旦开始循环了就一定不是快乐数。
可以把每一次得到的平方和sum放入set集合里,每次放入前判断一次set.contains(sum)!=true
,如果set中已经有了就return false
,一旦出现1就return true
。
-
注意先判断是否重复再判断1!
-
怎么取每个位上的数?
while(n > 0) {
sum += (n % 10) * (n % 10);
n = n / 10;
}
把计算sum单独写成一个方法,简化代码。
- java代码
class Solution {
public int getSum(int n) {
int sum = 0;
while(n > 0) {
sum += (n % 10) * (n % 10);
n = n / 10;
}
return sum;
}
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
while(true) {
int sum = getSum(n);
if(set.contains(sum)) {
return false;
}
if(sum == 1) {
return true;
}
set.add(sum);
n = sum; //记得更新sum
}
}
}
1.两数之和
题目链接:https://leetcode.cn/problems/two-sum/description/
视频链接:https://www.bilibili.com/video/BV1aT41177mK/?spm_id_from=333.788
讲解链接:https://programmercarl.com/0001.%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C.html#%E6%80%9D%E8%B7%AF
暴力法
两层for,遍历每一个数和其他的数相加。
思路
四个重点:
- 为什么会想到用哈希表
- 哈希表为什么用map
- 本题map是用来存什么的
- map中的key和value用来存什么的
首先什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。在这题里就是我们遍历数组时要找target-nums[i]
是否在集合里出现过。
本题需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
- 选用什么形式的哈希表?
存放这个元素的同时,还要存放对应的下标,因此选用Map。key
对应数组元素,value
对应下标。
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++) {
if(map.containsKey(target - nums[i])) {
result[0] = i;
result[1] = map.get(target - nums[i]); //获取对应的下标
break;
}
map.put(nums[i],i);
}
return result;
}
}
day6总结
1.java中 对字符串操作:
str.length()
str.charAt()
2.选择数组,set,map的原则
- 元素比较少,范围可控,元素较为连续 —选数组
- 数值比较大,元素很分散 —选set
- 有key value对应 —选map
3.数组如果要动态添加元素,就要用ArrayList。
4.如何取每个位置上的单数?
-
while(n > 0) {
sum += (n % 10) * (n % 10);
n = n / 10;
}
5.如果有一个重复性高的需求,考虑另外单独写成一个方法。
- 如:getSum()方法 求每个位置上数的平方和
6.HashSet对应的方法
//初始化
Set<Integer> set = new HashSet<>();
//添加元素
set.add(...);
//返回set的大小
set.size();
//查询是否含有某元素
set.contains();
7.HashMap对应的方法
//初始化
Map<Integer,Integer> map = new HashMap<>();
//添加键值对
map.put(key,value);
//判断key是否存在
map.containskey(key);
//根据key获取对应value
map.getValue(key);