什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
基础知识
基础知识参考代码随想录:文章讲解
charAt()
Java String类中 charAt() 方法用于返回指定索引处的字符。索引范围为从 0 到 length() - 1。
for(int a:i)
这种有冒号的for循环叫做 foreach循环,foreach 语句是 java5 的新特征之一。
1 for(元素类型t 元素变量x : 遍历对象obj){ 2 引用了x的java语句; 3 }
哈希表
哈希表 / 散列表(hash table)是根据关键码的值而直接进行访问的数据结构。
直白来讲其实数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个元素是否在数组里,要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。只需要在查询的时候通过索引直接就可以知道元素在不在数组里了。
将元素映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数
哈希函数如下图所示,通过 hashCode 把列表元素转化为数值,一般 hashcode 是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把元素映射为哈希表上的索引数字了。
有两种哈希函数方法比较普遍:
直接定制法
-
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
-
优点:简单、均匀 ;
-
缺点:需要事先知道关键字的分布情况
-
使用场景:适合查找比较小且连续的情况
除留取余法
-
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作。
但如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置,即发生哈希碰撞现象。
哈希碰撞
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法
将发生冲突的元素都存储在链表中。
(数据规模是 dataSize, 哈希表的大小为 tableSize )
拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证 tableSize 大于 dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放冲突的数据了。如图所示:
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
-
数组
-
set (集合)
-
map (映射)
总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
Java 补充知识
集合
概念
集合是 java 中提供的一种容器,可以用来存储多个数据。
集合中的元素实际上是对象,一些常见的基本类型可以使用它的包装类。
基本类型对应的包装类表如下:
基本类型 | 引用类型 |
---|---|
boolean | Boolean |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
集合与数组区别
-
数组的长度是固定的。集合的长度是可变的。
-
数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
分类
集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection
和双列集合java.util.Map
。
Collection
单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是 java.util.List 和java.util.Set,在使用前记得引入。其中,List 的特点是元素有序、元素可重复。Set 的特点是元素无序,而且不可重复。List 接口的主要实现类有 java.util.ArrayList 和 java.util.LinkedList,Set 接口的主要实现类有 java.util.HashSet和java.util.TreeSet。
Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List 和 Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:
-
public boolean add(E e): 把给定的对象添加到当前集合中 。
-
public void clear() :清空集合中所有的元素。
-
public boolean remove(E e): 把给定的对象在当前集合中删除。
-
public boolean contains(E e): 判断当前集合中是否包含给定的对象。
-
public boolean isEmpty(): 判断当前集合是否为空。
-
public int size(): 返回集合中元素的个数。
-
public Object[] toArray(): 把集合中的元素,存储到数组中。
List 集合
java.util.List 接口继承自 Collection 接口,习惯性地会将实现了 List 接口的对象称为 List 集合。
这类集合具有以下特点:
-
List集合中的元素是以线性方式进行存储的,基于不同的实现类有不同的表现形式(数组/链表)
-
List集合中的元素是带索引的,可以通过索引来访问集合中的指定元素
-
List集合中的元素是有序的,即元素的存入顺序和取出顺序一致。
-
List 集合中的元素是可重复的,允许出现重复的元素,可以通过元素的 equals() 方法比较两个元素是否重复
List集合特有方法均与索引相关:
-
public void add(int index, E element):将指定的元素,添加到该集合中的指定位置上。
-
public E get(int index):返回集合中指定位置的元素。
-
public E remove(int index):移除列表中指定位置的元素, 返回的是被移除的元素。
-
public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
List 接口的实现类
-
java.util.ArrayList 底层采用数组实现,查询快,增删慢
List<String> list = new ArrayList<String>();//(多态表示法)
-
java.util.LinkedList 底层采用双向链表实现,查询慢,增删快
LinkedList<String> link = new LinkedList<String>();
set 集合
java.util.Set 接口和 java.util.List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。
与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set 集合有多个子类,这里我们介绍其中的 java.util.HashSet 、 java.util.LinkedHashSet 这两个集合。
HashSet 集合
创建:
HashSet<String> set = new HashSet<String>();
java.util.HashSet 是 Set 接口的一个实现类,HashSet 允许为null,但仍不允许有重复的值,并且元素都是无序的(即存取顺序不一致)。 java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持
HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性 的方式依赖于: hashCode 与 equals 方法。
HashSet 底层是由一个哈希表结构 -> (数组+链表+红黑树(JDK1.8 增加了红黑树部分))实现的,当出现哈希冲突时,相同哈希值的元素采用链表结构存储,当同一哈希值的冲突元素超过 8 个时,改为红黑树存储。
在HashSet下面有一个子类 java.util.LinkedHashSet ,它是链表和哈希表组合的一个数据存储结构。多出一个链表来保证元素有序。
-
创建对象
LinkedHashSet<String> linked = new LinkedHashSet<String>();
-
使用 add() 方法添加元素;
-
使用 contains() 方法来判断元素是否存在于集合当中;
-
使用 remove() 方法来删除集合中的元素;
-
使用 clear 方法删除集合中所有元素
-
使用 size() 方法计算 HashSet 中的元素数量
-
使用 for-each 来迭代 HashSet 中的元素
Map
Map集合是双列集合的顶层接口,它是用来存储键值对的,其中键具有唯一性,而值是可以重复的。即:Map集合的数据结构只针对于键有效。
因为Map是接口,不能通过new关键字直接创建它的对象,为我们可以通过多态的形式,创建其子类对象,从而实现创建Map集合对象的这个需求,常用子类主要有两个,分别是:HashMap 和 TreeMap。
创建对象示例:
Map<String,String> map = new HashMap<String,String>();
Map集合的成员方法
-
V put (K key, V value):添加元素
-
V remove(Object key):根据键删除键值对元素
-
void clear():移除所有的键值对元素
-
boolean containsKey(Object key):判断集合是否包含指定的键
-
boolean containsValue(Object value):判断集合是否包含指定的值
-
boolean isEmpty():判断集合是否为空
-
int size():集合的长度,也就是集合中键值对的个数据
Map集合的获取功能
-
V get(Object key):根据键获取值
-
Set keySet():获取所有键的集合
-
Collection values():获取所有值的集合
刷题
242.有效的字母异位词
题目:给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
说明: 你可以假设字符串只包含小写字母。
思路及实现
这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
-
定一个数组叫做 record,大小为26 ,初始化为0,因为字符 a 到字符 z 的ASCII 也是26个连续的数值。
-
遍历字符串 s 的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符 a 的ASCII,只要求出一个相对数值即可。
-
遍历字符串 t 的时候,对 t 中出现的字符映射哈希表索引上的数值再做-1的操作。
-
最后检查一下 record数组如果有的元素不为零0,说明字符串 s 和 t 一定是谁多了字符或者谁少了字符,return false;如果 record 数组所有元素都为零0,说明字符串 s 和 t 是字母异位词,return true。
代码如下:
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. 两个数组的交集
题目:给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的
思路及实现
1.使用HashSet
-
将数组1转化为 HashSet
-
遍历数组2的过程中判断哈希表中是否存在该元素
-
最后将结果集合转为数组,或另外申请一个数组存放setRes中的元素,最后返回数组
代码如下:
import java.util.HashSet;
import java.util.Set;
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);
}
}
//方法1:将结果集合转为数组
return resSet.stream().mapToInt(x -> x).toArray();
//方法2:另外申请一个数组存放setRes中的元素,最后返回数组
int[] arr = new int[resSet.size()];
int j = 0;
for(int i : resSet){
arr[j++] = i;
}
return arr;
}
}
2.使用hash数组
题目限制了数值的大小,所以可以使用数组来做哈希的题目。
题目要求:
-
1 <= nums1.length, nums2.length <= 1000
-
0 <= nums1[i], nums2[i] <= 1000
所以我们可定义两个长度大于1000的哈希数组,分别记录各数组元素,再遍历这两个哈希数组,当 hash1[i] > 0 && hash2[i] > 0 时,认为数组相交,并用 resList 记录,最后申请一个数组存放resList中的元素,最后返回数组。
代码如下:
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash1 = new int[1002];
int[] hash2 = new int[1002];
for(int i : nums1)
hash1[i]++;
for(int i : nums2)
hash2[i]++;
List<Integer> resList = new ArrayList<>();
for(int i = 0; i < 1002; i++)
if(hash1[i] > 0 && hash2[i] > 0)
resList.add(i);
int index = 0;
int res[] = new int[resList.size()];
for(int i : resList)
res[index++] = i;
return res;
}
}
拓展
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
不要小瞧这个耗时,在数据量大的情况,差距是很明显的。
202. 快乐数
题目:编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
输入:19 输出:true 解释: 1^2 + 9^2 = 82 8^2 + 2^2 = 68 6^2 + 8^2 = 100 1^2 + 0^2 + 0^2 = 1
思路及实现
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。判断sum是否重复出现就可以使用HashSet。
我们知道1肯定是快乐数,所以我们可以以sum是否为1或sum重复出现来判断是否为快乐数。
求和中,可以通过取余得到末位数,再对数 n 进行 n / 10 操作,通过循环相加就可得到它它每个位置上的数字的平方和。
代码如下:
import java.util.Set;
import java.util.HashSet;
class Solution {
public boolean isHappy(int n) {
Set<Integer> recoder = new HashSet<>();
while(n != 1 && !recoder.contains(n)){
recoder.add(n);
n = getNextNumber(n);
}
return n == 1;
}
private int getNextNumber(int n){
int sum = 0;
while(n > 0){
int temp = n % 10;
sum += temp * temp;
n = n / 10;
}
return sum;
}
}
1. 两数之和
题目:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
思路及实现
本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,使用map正合适。
map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)。
接下来是map中 key 和 value 分别表示什么。
这道题我们需要给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为 key,所以数组中的元素作为 key,有 key 对应的就是 value,value用来存下标。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
在遍历数组的时候,只需要向 map 去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
过程如下:
代码如下:
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
if(nums == null || nums.length == 0){
return result;
}
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
int temp = target - nums[i];
if(map.containsKey(temp)){
result[0] = map.get(temp);
result[1] = i;
break;
}
map.put(nums[i], i);
}
return result;
}
}
总结
今天学了好多新知识,哈希表,java中的集合,Conllection,HashSet,Map等,下来一定要好好复习呀!