剑指Offer(专项突破版)刷题笔记 | 第五章 哈希表

295 阅读7分钟

哈希表的基础知识

特点:哈希表的插入、删除、或查找一个元素都只需要O(1)O(1)的时间

HashMap(键值对)和HashSet(单值)

哈希表的设计

  • 实例通过hashcode()计算哈希值
  • 可以利用对数组长度求余数将哈希值转换为数组下标
  • 利用拉链法或者线性探测法解决碰撞问题
  • 扩容之后要重新分配元素位置

Q30:插入、删除和随机访问都是O(n)O(n)的容器

题目(中等):设计一个支持在平均 时间复杂度 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 

解题思路

  • 同时使用哈希表和数组
  • 删除时,对于数组如果直接按下标进行删除会造成O(n)O(n)的复杂度,可以将其先和最后一位元素交换位置
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

解题思路

创建一个链表,用来实现最近最少;利用哈希表实现O(1)O(1)复杂度的getput

  • 缓存中包含哈希表和链表,哈希表的键就是缓存的键,哈希表的值就是双向链表的节点
  • 设置头尾哨兵
  • 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;
}

小结

  • 为了设计一个哈希表,首先需要一个数组,把每个键的哈希值映射到数组的一个位置。
  • 如果哈希表的键的数目是固定的,并且数目不大,可以用数组来模拟哈希表,数组下标对应哈希表的键,而数组的值对应哈希表的值
  • 哈希表常被用来记录字符串中字母的出现次数、字符串中字符出现的位置等信息