哈希表Hash table
根据关键码的值而直接进行访问的数据结构
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素。
一般哈希表都是用来快速判断一个元素是否出现集合里
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数
把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了
通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了
哈希碰撞
一般哈希碰撞有两种解决方法, 拉链法和线性探测法
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
数值范围小小时候用数组,数值范围大时候用set,K对应value用map
set:成员值是唯一,没有重复值
List家族有两名大将,分别是ArrayList和LinkedList。而Set家族里主要有HashSet和TreeSet两名大将
按元素存取顺序来说,List是有序的,Set是无序的。按照元素和元素之间的关系来说,List是无序的,TreeSet是有序的。而HashSet怎么说都是无序的
HashSet此无序说的是读取数据的顺序不一定和插入数据的顺序一样。但是HashSet真实的情况是有序的,只不过他是通过内部HashCode方法计算hash值后自动进行了排序,所以读取的是经过内部排序后的数据,且此数据每次结果都是一样的顺序。
HashMap和HashSet是一样的,HashMap是根据key的hash值进行了排序
顺序有两个概念,一是按照添加的顺序排列,二是按,照自然顺序a-z排列
Set无序指的是HashSet,它不能保证元素的添加顺序,更不能保证自然顺序,而Set的其他实现类是可以实现这两种顺序的
1,LinkedHashset : 保证元素添加的自然顺序
2,TreeSet : 保证元素的自然顺序
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
public class ValidAnagram { public boolean isAnagram(String s, String t){ //哈希表-数组 int[] record = new int[26]; for(int i = 0; i < s.length(); i++){ //s.charAt(i)-'0'得到s[i]中存储的数字字符对应a数组中哪一个下标,也是将字符转成数字,再记数加一。 record[s.charAt(i) - 'a']++;//记录频率。charAt(i) 就是在第i个位置的字符 } for(int i = 0; i < t.length(); i++ ){ record[t.charAt(i) -'a']--; } for (int count : record){ if(count != 0){ return false; } } return true; } }
注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。不要小瞧 这个耗时,在数据量大的情况,差距是很明显的
import java.util.HashSet; import java.util.Set; public class IntersectionofTwoArrays { //set 操作 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);//把i转变称set的存储结构 } //遍历数组2的过程中判断哈希表中是否存在该元素 for(int i : nums2){ if(set1.contains(i)){//在set1中找到对应元素 resSet.add(i);//放在resSet里 } } //将结果几何Set转为数组Array return resSet.stream().mapToInt(x -> x).toArray(); } }
/* //方法2:另外申请一个数组存放setRes中的元素,最后返回数组 int[] arr = new int[setRes.size()]; int j = 0; for(int i: setRes){ arr[j++] = i; } retrun arr;*/
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
根据我们的探索,我们猜测会有以下三种可能。
- 最终会得到 1。
- 最终会进入循环。
- 值会越来越大,最后接近无穷大。
import java.util.HashSet; import java.util.Set; public class HappyNumber { public boolean isHappy(int n){ Set<Integer> record = new HashSet<>(); while (n != 1 && !record.contains(n)){//不能有重复和不等于1 record.add(n); n = getNextNumber(n); } return n == 1; } public int getNextNumber(int n){ int res = 0; while ( n > 0){ int temp = n % 10; n = n / 10; res += temp * temp; } return res; } }
首先我在强调一下 什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题呢,我就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
那么我们就应该想到使用哈希法了。
因为本地,我们不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
public int[] twoSum(int[] nums, int target){ //哈希表-遍历的元素时候,判断target - nums这个元素之前是不是遍历过,如何判断:把遍历过的元素加到集合里 //存两个东西一个是元素,一个是下标,所以用map(因为查找是元素,所以元素key,下标value) //map存放遍历过的元素 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] = map.get(temp);//一个是元素对应的value res[1] = i;//另一个是当前遍历的元素 } //map中没有找到想要查询的元素,在遍历下一个元素时候要把这个遍历的元素加在map里 //把遍历过的元素的key和value存放在map里 map.put(nums[i], i); } return res; }