文章目录
哈希表理论基础
首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。
哈希表是根据关键码的值而直接进行访问的数据结构。
哈希表中关键码就是数组的索引下表,然后通过下表直接访问数组中的元素,如下图所示:
哈希表能解决什么问题?
一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1) 就可以做到。
哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
哈希碰撞
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
242.有效的字母异位词 - 12.1
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
解析:
定义一个数组叫做record用来上记录字符串s里字符出现的次数。
需要把字符映射到数组也就是哈希表的索引下表上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下表0,相应的字符z映射为下表25。
再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
class Solution {
public boolean isAnagram(String s, String t) {
//定义数组存储
int []record = new int[26];
//统计s中出现的字符次数,出现一次+1
for(char c : s.toCharArray()){
record[c - 'a'] += 1;
}
//若t中出现过相同的字符,就减1
for(char c : t.toCharArray()){
record[c - 'a'] -= 1;
}
//遍历
for(int i : record){
//如果存在不为0的值,就说明不是互为异位词
if(i != 0){
return false;
}
}
return true;
}
}
349. 两个数组的交集 - 12.1
给定两个数组,编写一个函数来计算它们的交集。
解法一:暴力算法,两重循环 判断 是否相同,相同就添加进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,因为返回结果要求不能重复
HashSet<Integer> set = new HashSet<>();
for(int i=0; i<nums1.length; i++){
for(int j=0; j<nums2.length; j++){
if(nums1[i] == nums2[j]){
set.add(nums1[i]);
}
}
}
//将结果几何转为数组
int[] arr = new int[set.size()];
int index = 0;
for (int i : set) {
arr[index++] = i;
}
return arr;
}
}
解法二:定义两个set,先将数组1的元素添加进set1,然后遍历数组2看数组2元素是否存在set1中,如有,就添加进set2。最后再将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,因为返回结果要求不能重复
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new HashSet<>();
//遍历数组1,添加进set1
for(int i: nums1){
set1.add(i);
}
//遍历数组2来比较
for(int j: nums2){
if(set1.contains(j)){
set2.add(j);
}
}
//将结果几何转为数组
int[] arr = new int[set2.size()];
int index = 0;
for (int i : set2) {
arr[index++] = i;
}
return arr;
}
}
202. 快乐数 - 12.1
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 true ;不是,则返回 false 。
解析:将各个位数求和,判断是否曾经出现过,出现过就会无限循环,返回false。否则看是否会为1,为1就返回true。
class Solution {
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
// 为1 或 求和结果n重复 就会无限循环
while(n != 1 && !set.contains(n)){
set.add(n);
n = getSum(n); //求和
}
return n == 1;
}
//求各个 位数之和
private int getSum(int n){
int res = 0;
while(n > 0){
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
1. 两数之和 - 12.1
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
解析:使用HashMap存储元素和下标,然后遍历数组,//如果map集合里有 target-nums[i] 就直接返回。
class Solution {
public int[] twoSum(int[] nums, int target) {
int []res = new int[2];
if(nums == null) return res;
Map<Integer,Integer> map = new HashMap<>();
for(int i=0; i<nums.length; i++){
//如果map集合里有 target-nums[i] 就直接返回
if(map.containsKey(target-nums[i])){
return new int[]{map.get(target-nums[i]), i};
}
map.put(nums[i],i);
}
return res;
}
}
454. 四数相加 II - 12.2
解析:使用HashMap,先统计两个数组中的元素之和,同时统计出现的次数,放入map;然后统计剩余的两个元素的和,在map中找是否存在相加为0的情况,同时记录次数。
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer,Integer> map = new HashMap<>();
int temp;
int res = 0;
//统计前两个数组元素相加之和,及出现的次数
for(int i : nums1){
for(int j : nums2){
//求和
temp = i + j;
//如果结果出现过,次数就+1
if(map.containsKey(temp)){
map.put(temp,map.get(temp)+1);
}else{
//没有就直接添加
map.put(temp,1);
}
}
}
//统计后两个数组元素之和,看是否能在map中找到相加为0的情况
for(int m : nums3){
for(int n : nums4){
//求和
temp = m + n;
//如果存在相加为0的情况,就获取出现的次数并累加起来
if(map.containsKey(0-temp)){
res += map.get(0-temp);
}
}
}
return res;
}
}
383. 赎金信 - 12.2
为了不在赎金信中暴露字迹,从杂志上搜索各个需要的字母,组成单词来表达意思。
给你一个赎金信 (ransomNote) 字符串和一个杂志(magazine)字符串,判断 ransomNote 能不能由 magazines 里面的字符构成。
如果可以构成,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
(me)解法一:分别将两个字符串的字符和对应字符出现的次数存入map中,然后遍历ransomNote的字符,看magazine中是否包含ransomNote的字符并且出现次数相等或更多。
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
Map<Character,Integer> map1 = new HashMap<>();
Map<Character,Integer> map2 = new HashMap<>();
//将赎金信字符串字符和字符出现次数存入map中
for(char c : ransomNote.toCharArray()){
if(map1.containsKey(c)){
map1.put(c,map1.get(c)+1);
}else{
map1.put(c,1);
}
}
//将杂志字符串字符和字符出现次数存入map中
for(char c : magazine.toCharArray()){
if(map2.containsKey(c)){
map2.put(c,map2.get(c)+1);
}else{
map2.put(c,1);
}
}
//对比杂志中是否包含有赎金信的字符,并杂志中字符次数要大于赎金信中字符次数
for(char c : ransomNote.toCharArray()){
if(map2.containsKey(c)){
if(map2.get(c) < map1.get(c)){
return false;
}
}else{
return false;
}
}
return true;
}
}
解法二: 用一个长度为26的数组还记录magazine里字母出现的次数。然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
//记录杂志字符串出现的次数
int[] arr = new int[26];
int temp;
for (int i = 0; i < magazine.length(); i++) {
temp = magazine.charAt(i) - 'a';
arr[temp]++;
}
for (int i = 0; i < ransomNote.length(); i++) {
temp = ransomNote.charAt(i) - 'a';
//对于赎金信中的每一个字符都在数组中查找
//找到相应位减一,否则找不到返回false
if (arr[temp] > 0) {
arr[temp]--;
} else {
return false;
}
}
return true;
}
}
NC93 设计LRU缓存结构 - 3.10
解析:
import java.util.*;
public class Solution {
/**
* lru design
* @param operators int整型二维数组 the ops
* @param k int整型 the k
* @return int整型一维数组
*/
public int[] LRU (int[][] operators, int k) {
// write code here
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
List<Integer> list = new LinkedList<>();
for (int[] operator : operators) {
int key = operator[1]; //获取键key
switch(operator[0]) { //判断是set或get操作
case 1: //set操作
int value = operator[2]; //获取值
if (map.size() < k) { //如果没满就直接添加
map.put(key, value);
} else { //如果满了,移除最不经常使用的记录,再添加新记录
Iterator it = map.keySet().iterator();
map.remove(it.next()); //移除
map.put(key, value); //添加
}
break;
case 2: //get操作
if (map.containsKey(key)) { //如果key存在
int val = map.get(key);
list.add(val); //获取key然后存入到list中
map.remove(key); //移除这个键
map.put(key, val); //再重新添加,相当于刷新缓存。
} else {
list.add(-1); //不存在则添加-1
}
break;
default:
}
}
int[] res = new int[list.size()];
int i = 0;
for (int val : list) {
res[i++] = val; //list转为数组
}
return res;
}
}
146. LRU 缓存 - 3.10
解析:
class LRUCache {
int capacity; //缓存的大小
LinkedHashMap<Integer, Integer> cacheMap = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if(!cacheMap.containsKey(key)){
return -1;
}
makeRecently(key); //将key变为最近使用
return cacheMap.get(key);
}
public void put(int key, int value) {
if(cacheMap.containsKey(key)){
cacheMap.put(key, value); //如果key存在,则更新key的值
makeRecently(key); //将key变为最近使用
return;
}
if(cacheMap.size() >= this.capacity){
//链表头就是最久未使用的key
int oldestKey = cacheMap.keySet().iterator().next();
cacheMap.remove(oldestKey);
}
//将新的key添加到链表尾部
cacheMap.put(key, value);
}
//将key变为最近使用
void makeRecently(int key){
int keyVal = cacheMap.get(key); //先记录key的值
cacheMap.remove(key); //移除key,重新插入到队尾
cacheMap.put(key, keyVal); //先移除,再添加,相当于更新
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/