目录
当我们遇到了要快速判断一个元素是否出现在集合里的时候,就要考虑哈希法
哈希表理论基础
哈希表
Hash table,也叫散列表。是一种通过键来访问值的数据结构),数组也是一种哈希表(通过下标访问数组中的元素)。可以用来快速判断一个元素是否在集合里(时间复杂度O(1))。
内部实现原理
哈希函数
将其他数据格式hashcode转换映射为哈希表上的索引数字的函数,若hashcode得到的数值大于哈希表的tablesize,会再次对数值进行一个取模操作,使得映射出来的索引数值都落在哈希表上。
哈希碰撞
当有两个或多个数值的hashcode映射到哈希表同一索引下标的位置时,我们称之为哈希碰撞。
解决方法
- 拉链法
-
- 发生冲突的元素被存储在链表中,通过索引找到冲突元素
- 要选择适当的哈希表大小,避免因为数组空值而浪费大量内存,也避免链表太长导致查找费时
- 线性探测法
-
- 发生冲突的元素一个放在冲突的位置,其余的元素向下找一个空位存放
- tablesize一定要大于datasize(哈希冲突的数据个数)
常见哈希结构
数组,set(集合),map(映射)。
数组
用数组下标来做映射
set 和map
set是一个集合,里面放的元素只能是一个key
哈希法是空间换时间
哈希值比较小,且数值范围可控时,选用数组
如果哈希值比较少、特别分散、跨度非常大,用数组就造成空间的极大浪费,使用set会比较合适
242. 有效的字母异位词
经典的数组在哈希表的应用
将字符串中出现的所有字母频次统计在哈希数组中,再对哈希数组根据另一字符串字母出现频次做减减操作,最终数组出现非零元素表明两字符串非有效的字母异位词
public class ValidAnagram {
/**
* 数组在哈希表的应用
* 利用26位字母在ascii码中是连续的特性,将字符串映射到数组上来统计字母出现的次数
* 再利用减减方式,判断最终数组的所有元素是否为0,出现非0元素,则表明s和t不是字母异位词
*
* @param s
* @param t
* @return
*/
public boolean isAnagram(String s, String t) {
// 定义一个record数组来统计字符串s中字符出现的次数
int[] record = new int[26];
// 因为小写a到小写z的ASCII码是26个连续的数值,所以字符a映射到数组下标为0的位置,其他依次推出
for (char sChar : s.toCharArray()) {
record[sChar - 'a']++;
}
// 字符串t出现的字符次数做减减操作
for (char tChar : t.toCharArray()) {
record[tChar - 'a']--;
}
// 如果数组最终存在元素为非0,说明有字符串少了/多了字符
for (int count : record) {
if (count != 0) {
return false;
}
}
return true;
}
}
349. 两个数组的交集
使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
public class IntersectionOfTwoArrays {
/**
* 使用set这种哈希结构查找元素
*
* @param nums1
* @param nums2
* @return 交集
*/
public int[] hashsetMethod(int[] nums1, int[] nums2) {
// 判断数组是否为空
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0];
}
// 返回的交集是去重后的结果
HashSet<Integer> resultSet = new HashSet<>();
HashSet<Integer> numSet = new HashSet<>();
// 将num1数组中出现的字母在哈希set中做记录
for (int i : nums1) {
numSet.add(i);
}
// 在set中查找num2的元素,并添加到结果集中
for (int j : nums2) {
if (numSet.contains(j)) {
resultSet.add(j);
}
}
// 返回int数组
int[] resultArr = new int[resultSet.size()];
int n = 0;
for (Integer r : resultSet) {
resultArr[n++] = r;
}
return resultArr;
}
/**
* 使用哈希数组的方式查找元素(仅适用于数值范围可控/较小),查询会比set快,并且占用空间少
* 题目后修改了数值范围:
* 1 <= nums1.length, nums2.length <= 1000
* 0 <= nums1[i], nums2[i] <= 1000
*
* @param nums1
* @param nums2
* @return
*/
public int[] hashArrayMethod(int[] nums1, int[] nums2) {
// 判断数组是否非空
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0];
}
// 结果集
HashSet<Integer> resultSet = new HashSet<>();
// 定义记录nums1的数组,大小比最大值大一点即可
int[] hash = new int[1002];
// 出现的字母在哈希数组做记录,其对应元素值为1,表示有该数值
for (int nums : nums1) {
hash[nums]++;
}
// 查找num2的元素,并添加到结果集
for (int nums : nums2) {
if (hash[nums] != 0) {
resultSet.add(nums);
}
}
int[] resultArr = resultSet.stream().mapToInt(x -> x).toArray();
return resultArr;
}
}
202. 快乐数
此题有两个关键:
- 快速判断一个元素是否出现在集合里,选用哈希法
- 对取数值各个位上的单数操作
public class HappyNumber {
/**
* 判断一个元素是否已出现在集合里 -> 哈希法
* @param n
* @return
*/
public boolean isHappy(int n) {
HashSet<Integer> record = new HashSet<>();
// 不为1且过程中还未出现该数值
while (n != 1 && !record.contains(n)) {
record.add(n);
n = getNextNumber(n);
}
// 为1,则是快乐数;重复出现数值了,则不是快乐数
return n == 1;
}
/**
* 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和
* @param n
* @return
*/
public int getNextNumber(int n) {
int result = 0;
while (n > 0) {
int tmp = n % 10;
result += tmp * tmp;
n /= 10;
}
return result;
}
}
1. 两数之和
此题关键:
- 需要一个集合去记录已遍历过的元素
- 由于需要查找已遍历过的元素,我们选择用哈希表
- 我们既需要存储元素,也需要记录元素对应的下标,所以选择map这种数据结构
- map的key来存遍历过的元素,方便快速查找,map的value则存元素在数组中的下标
public class TwoSum {
/**
* 哈希map记录遍历过的元素(key)和元素对应下标(value)
* @param nums
* @param target
* @return
*/
public int[] twoSum(int[] nums, int target) {
// 判断入参是否非空
if (nums == null || nums.length == 0) {
return null;
}
// 定义一个记录遍历过元素的数据结构
Map<Integer, Integer> recordMap = new HashMap<>();
int tmp;
for (int i = 0; i < nums.length; i++) {
tmp = target - nums[i];
// 查询是否已经遍历过该元素的余数
if (recordMap.containsKey(tmp)) {
return new int[]{i, recordMap.get(tmp)};
} else {
// 没有,则将元素添加至map继续遍历
recordMap.put(nums[i], i);
}
}
return null;
}
}