哈希表的基础知识
特点:哈希表的插入、删除、或查找一个元素都只需要的时间
HashMap(键值对)和HashSet(单值)
哈希表的设计
- 实例通过hashcode()计算哈希值
- 可以利用对数组长度求余数将哈希值转换为数组下标
- 利用拉链法或者线性探测法解决碰撞问题
- 扩容之后要重新分配元素位置
Q30:插入、删除和随机访问都是的容器
题目(中等):设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构:
insert(val):当元素 val 不存在时返回 true ,并向集合中插入该项,否则返回 false 。remove(val):当元素 val 存在时返回 true ,并从集合中移除该项,否则返回 false 。getRandom:随机返回现有集合中的一项。每个元素应该有 相同的概率 被返回。
示例 :
输入: inputs = ["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]
输出: [null, true, false, true, 2, true, false, 2]
解释:
RandomizedSet randomSet = new RandomizedSet(); // 初始化一个空的集合
randomSet.insert(1); // 向集合中插入 1 , 返回 true 表示 1 被成功地插入
randomSet.remove(2); // 返回 false,表示集合中不存在 2
randomSet.insert(2); // 向集合中插入 2 返回 true ,集合现在包含 [1,2]
randomSet.getRandom(); // getRandom 应随机返回 1 或 2
randomSet.remove(1); // 从集合中移除 1 返回 true 。集合现在包含 [2]
randomSet.insert(2); // 2 已在集合中,所以返回 false
randomSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2
解题思路
- 同时使用哈希表和数组
- 删除时,对于数组如果直接按下标进行删除会造成的复杂度,可以将其先和最后一位元素交换位置
class RandomizedSet {
HashMap<Integer,Integer> numToLocation;
ArrayList<Integer> nums;
/** Initialize your data structure here. */
public RandomizedSet() {
numToLocation = new HashMap<>();
nums = new ArrayList<>();
}
/** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
public boolean insert(int val) {
if(numToLocation.containsKey(val)) return false;
numToLocation.put(val,nums.size());//键是val,值是数组的下标
nums.add(val);
return true;
}
/** Removes a value from the set. Returns true if the set contained the specified element. */
public boolean remove(int val) {
if(!numToLocation.containsKey(val)) return false;
int location = numToLocation.get(val);//得到哈希表中val的值(数组中val的下标)
numToLocation.put(nums.get(nums.size()-1),location);//数组最后一位val交换到要删除的位置
numToLocation.remove(val);
nums.set(location,nums.get(nums.size()-1));
nums.remove(nums.size()-1);
return true;
}
/** Get a random element from the set. */
public int getRandom() {
Random random = new Random();
int r = random.nextInt(nums.size());
return nums.get(r);
}
}
Q31:最近最少使用缓存
题目(中等):运用所掌握的数据结构,设计和实现一个 LRU (Least Recently Used,最近最少使用) 缓存机制 。实现 LRUCache 类:
LRUCache(int capacity)以正整数作为容量 capacity 初始化 LRU 缓存int get(int key)如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, int value)如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
解题思路
创建一个链表,用来实现最近最少;利用哈希表实现复杂度的get和put
- 缓存中包含哈希表和链表,哈希表的键就是缓存的键,哈希表的值就是双向链表的节点
- 设置头尾哨兵
- put若键已经存在,将其移动到链表最后,若不存在就将它存到链表最后;若缓存已满,就删除链表的头节点。
//双向链表存储键值对
class ListNode{
public int key;
public int value;
public ListNode next;
public ListNode prev;
public ListNode(int k,int v){
key = k;
value = v;
}
}
class LRUCache {
ListNode head;
ListNode tail;
HashMap<Integer,ListNode> map;
int cap;
public LRUCache(int capacity) {
map = new HashMap<>();
//设置两个哨兵
head = new ListNode(-1,-1);
tail = new ListNode(-1,-1);
head.next = tail;
tail.prev = head;
cap = capacity;
}
public int get(int key) {
ListNode node = map.get(key);
if(node == null){
return -1;
}
//最近使用过的放到链表的尾部
moveToTail(node,node.value);
return node.value;
}
public void put(int key, int value) {
if(map.containsKey(key)){
moveToTail(map.get(key),value);
}else{
if(map.size() == cap){
ListNode toBeDeleted = head.next;//缓存已满,删除第一个元素
deleteNode(toBeDeleted);
map.remove(toBeDeleted.key);
}
ListNode node = new ListNode(key,value);
insertToTail(node);
map.put(key,node);
}
}
private void moveToTail(ListNode node,int newValue){
deleteNode(node);
node.value = newValue;
insertToTail(node);
}
private void deleteNode(ListNode node){
node.prev.next = node.next;
node.next.prev = node.prev;
}
//插入到尾部
private void insertToTail(ListNode node){
tail.prev.next = node;
node.prev = tail.prev;
node.next = tail;
tail.prev = node;
}
}
哈希表的应用
如果哈希表的键的取值范围是固定的,并且范围不是很大,则可以用数组来模拟哈希表。数组的下标和哈希表的键对应,而数组的值和哈希表的值对应
Q32:有效的变位词
题目(简单):给定两个字符串 s 和 t ,编写一个函数来判断它们是不是一组变位词(字母异位词)。注意:若 s 和 t 中每个字符出现的次数都相同且字符顺序不完全相同,则称 s 和 t 互为变位词(字母异位词)
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
示例 3:
输入: s = "a", t = "a"
输出: false
- s和t仅包括小写字母
public boolean isAnagram(String s, String t) {
if(s.length() != t.length() || s.equals(t)) return false;
int [] counts = new int[26];
for(char ch : s.toCharArray()){
counts[ch-'a']++;
}
for(char ch : t.toCharArray()){
if(counts[ch - 'a'] == 0){return false;}
counts[ch-'a']--;
}
return true;
}
- s和t包含非英文字符
public boolean isAnagram(String s, String t) {
if(s.length() != t.length() || s.equals(t)) return false;
HashMap<Character,Integer> counts = new HashMap<>();
for(char ch : s.toCharArray()){
counts.put(ch,counts.getOrDefault(ch,0) + 1);
}
for(char ch : t.toCharArray()){
if(!counts.containsKey(ch) || counts.get(ch) == 0){return false;}
counts.put(ch,counts.get(ch) - 1);;
}
return true;
}
Q33:变位词组
题目(中等):给定一个字符串数组 strs ,将 变位词 组合在一起。 可以按任意顺序返回结果列表。注意:若两个字符串中每个字符出现的次数都相同,则称它们互为变位词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
解题思路
将单词的字母排序,创建一个哈希表,以排序后的单词字符串为键,值为一组变位词
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List<String>> groups = new HashMap<>();//值为一组变位词
for(String str:strs){
char[] charArray = str.toCharArray();
Arrays.sort(charArray);
String sorted = new String(charArray);
groups.putIfAbsent(sorted,new LinkedList<String>());//不存在添加一对键值
groups.get(sorted).add(str);//存在则增加变位词
}
return new LinkedList<>(groups.values());
}
Q34:外星语言是否排序
题目(简单):某种外星语也使用英文小写字母,但可能顺序 order 不同。字母表的顺序(order)是一些小写字母的排列。给定一组用外星语书写的单词 words,以及其字母表的顺序 order,只有当给定的单词在这种外星语中按字典序排列时,返回 true;否则,返回 false。
示例 1:
输入:words = ["hello","leetcode"], order = "hlabcdefgijkmnopqrstuvwxyz"
输出:true
解释:在该语言的字母表中,'h' 位于 'l' 之前,所以单词序列是按字典序排列的。
示例 2:
输入:words = ["word","world","row"], order = "worldabcefghijkmnpqstuvxyz"
输出:false
解释:在该语言的字母表中,'d' 位于 'l' 之后,那么 words[0] > words[1],因此单词序列不是按字典序排列的。
示例 3:
输入:words = ["apple","app"], order = "abcdefghijklmnopqrstuvwxyz"
输出:false
解释:当前三个字符 "app" 匹配时,第二个字符串相对短一些,然后根据词典编纂规则 "apple" > "app",因为 'l' > '∅',其中 '∅' 是空白字符,定义为比任何其他字符都小(更多信息)。
public boolean isAlienSorted(String[] words, String order) {
int[] orderArray = new int[order.length()];
for(int i = 0;i < order.length();i++){
orderArray[order.charAt(i)-'a'] = i;
}
for(int i = 1;i < words.length;i++){
if(!isSorted(words[i-1],words[i],orderArray)) {
return false;
}
}
return true;
}
private boolean isSorted(String word1,String word2,int[] orderArray){
int i = 0;
for(;i < word1.length() && i < word2.length();i++){
int ch1 = word1.charAt(i) - 'a';
int ch2 = word2.charAt(i) - 'a';
if(orderArray[ch1] > orderArray[ch2]){
return false;
}
if(orderArray[ch1] < orderArray[ch2]){
return true;
}
}
return i == word1.length();//word1短就返回true
}
Q35:最小时间差
题目(中等):给定一个 24 小时制(小时:分钟 "HH:MM")的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。
示例 1:
输入:timePoints = ["23:59","00:00"]
输出:1
示例 2:
输入:timePoints = ["00:00","23:59","00:00"]
输出:0
解题思路
创建一个键为时间、值为boolean的哈希表,因为时间分钟数已知,所以可以用数组
public int findMinDifference(List<String> timePoints) {
//时间点个数超过1440,那么必有重复的时间点所以返回0
if(timePoints.size() > 1440) return 0;
boolean[] minuteFlags = new boolean[1440];
for(String timePoint : timePoints){
String t[] = timePoint.split(":");
int min = Integer.parseInt(t[0])*60 + Integer.parseInt(t[1]);
if(minuteFlags[min]){return 0;}
minuteFlags[min] = true;
}
return helper(minuteFlags);
}
private int helper(boolean[] minuteFlags){
int minDiff = minuteFlags.length - 1;
int prev = -1;
//first和head记录首尾时间点
int first = minuteFlags.length - 1;
int last = -1;
for(int i = 0;i < minuteFlags.length;i++){
if(minuteFlags[i]){
if(prev >= 0){
minDiff = Math.min(i-prev,minDiff);
}
prev = i;
first = Math.min(i,first);
last = Math.max(i,last);
}
}
minDiff = Math.min(first + minuteFlags.length - last,minDiff);
return minDiff;
}
小结
- 为了设计一个哈希表,首先需要一个数组,把每个键的哈希值映射到数组的一个位置。
- 如果哈希表的键的数目是固定的,并且数目不大,可以用数组来模拟哈希表,数组下标对应哈希表的键,而数组的值对应哈希表的值
- 哈希表常被用来记录字符串中字母的出现次数、字符串中字符出现的位置等信息