哈希集合哈希映射


哈希集合和哈希映射是经常使用的两个类。

HashMap设计键的总结  https://leetcode-cn.com/leetbook/read/hash-table/xxavl2/

当字符串 / 数组中每个元素的顺序不重要时,可以使用排序后的字符串 / 数组作为键。

如果只关心每个值的偏移量,通常是第一个值的偏移量,则可以使用偏移量作为键。

在树中,你有时可能会希望直接使用 TreeNode 作为键。 但在大多数情况下,采用子树的序列化表述可能是一个更好的主意。

在矩阵中,你可能希望使用行索引或列索引作为键。

在数独中,可以将行索引和列索引组合来标识此元素属于哪个块。( rowIndex * block行数量 / block行数量 + colIndex / block列数量)

有时,在矩阵中,您可能希望将值聚合在同一对角线中。

1. 同构字符串

给定两个字符串 s 和 t,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

示例 1:

输入:s = "egg", t = "add"
输出:true
示例 2:

输入:s = "foo", t = "bar"
输出:false
示例 3:

输入:s = "paper", t = "title"
输出:true
 

提示:

可以假设 s 和 t 长度相同。

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/hash-table/xhjvbj/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

解题思路:把源字符串中的每个字符和目的字符串的每个字符串做映射,一一对应。通过HashMap来映射源到目的,通过HashSet来判断目的字符已经存在映射。 此题可以扩展为单词pattern。

class Solution {     public boolean isIsomorphic(String s, String t) {         if(s.length() != t.length()) {             return false;         }         Map dict = new HashMap<>();         Set used = new HashSet<>();
        for(int i = 0; i < s.length(); i++){             char sch = s.charAt(i);             char tch = t.charAt(i);             if(dict.containsKey(sch)) {                 char och = dict.get(sch);                 if(och != tch) {                     return false;                 }             } else {                 if(used.contains(tch)) {                     return false;                 }                 dict.put(sch, tch);                 used.add(tch);             }         }         return true;     } }   2. 有效的数独

判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 1:

输入:
[
["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出: true
示例 2:

输入:
[
  ["8","3",".",".","7",".",".",".","."],
  ["6",".",".","1","9","5",".",".","."],
  [".","9","8",".",".",".",".","6","."],
  ["8",".",".",".","6",".",".",".","3"],
  ["4",".",".","8",".","3",".",".","1"],
  ["7",".",".",".","2",".",".",".","6"],
  [".","6",".",".",".",".","2","8","."],
  [".",".",".","4","1","9",".",".","5"],
  [".",".",".",".","8",".",".","7","9"]
]
输出: false
解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。
但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
说明:

一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
给定数独序列只包含数字 1-9 和字符 '.' 。
给定数独永远是 9x9 形式的。

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/hash-table/xxpit5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

方法1:

解题思路: 每一行,每一列, 每个3*3 box都定义一个HashMap来存储元素,如果其中有一个hashMap中出现重复数字,则返回false, 否则返回true. Box的下标通过 (rowIndex / 3 * 3 + colIndex / 3) 获得。 

class Solution {     public boolean isValidSudoku(char[][] board) {         HashMap[] rowMaps = new HashMap[9];         HashMap[] colMaps = new HashMap[9];         HashMap[] boxMaps = new HashMap[9];
        for(int i = 0 ; i < 9; i++) {             rowMaps[i] = new HashMap<>();             colMaps[i] = new HashMap<>();             boxMaps[i] = new HashMap<>();         }                  for(int i = 0; i < 9; i++) {             for(int j = 0; j < 9; j++) {                if(board[i][j] == '.') {                    continue;                }                int num = board[i][j];                                HashMap rowMap = rowMaps[i];                HashMap colMap = colMaps[j];                HashMap boxMap = boxMaps[(i/3) * 3 + j / 3];
               rowMap.put(num, rowMap.getOrDefault(num, 0) + 1);                colMap.put(num, colMap.getOrDefault(num,0) + 1);                boxMap.put(num, boxMap.getOrDefault(num, 0) + 1);                                if(rowMap.get(num) > 1 || colMap.get(num) > 1 || boxMap.get(num) > 1) {                    return false;                }                              }         }             return true;     } }    方法2: 定义9*9的行,列,box布尔数组,用来存储是否是有过该数字。如果出现两次则返回false. class Solution {     public boolean isValidSudoku(char[][] board) {         boolean[][] rowFill = new boolean[9][9];         boolean[][] colFill = new boolean[9][9];         boolean[][] boxFill = new boolean[9][9];
        for(int i = 0; i < 9; i++) {             for(int j = 0; j < 9; j++) {                 if(board[i][j] == '.') {                     continue;                 }
                int num = board[i][j] - '1';                 if(rowFill[i][num] || colFill[j][num] || boxFill[i/3 * 3 + j/3][num]) {                     return false;                 }
                rowFill[i][num] = true;                 colFill[j][num] = true;                 boxFill[i/3 * 3 + j/3][num] = true;
            }         }         return true;     } }   3. 我的日程安排

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内没有其他安排,则可以存储这个新的日程安排。

MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为,  start <= x < end。

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。

每次调用 MyCalendar.book方法时,如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。

请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

示例 1:

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(15, 25); // returns false
MyCalendar.book(20, 30); // returns true
解释:
第一个日程安排可以添加到日历中. 第二个日程安排不能添加到日历中,因为时间 15 已经被第一个日程安排预定了。
第三个日程安排可以添加到日历中,因为第一个日程安排并不包含时间 20 。
说明:

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/my-calendar-i
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路:把每个日程放入TreeMap, 开始时间作为key, 结束时间作为值。TreeNap具有天然的排序功能。每次放入日程时找到开始时间的上下日程进行比价。

  class MyCalendar {     TreeMap treeMap;
    public MyCalendar() {         treeMap = new TreeMap<>();     }
    public boolean book(int start, int end) {         Integer preStart = treeMap.floorKey(start);         Integer postStart = treeMap.ceilingKey(start);
        if((preStart == null || treeMap.get(preStart) <= start)                 && (postStart == null || end <= postStart )) {             treeMap.put(start, end);             return true;         }
        return false;     } }   4. 我的日程安排2

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个新的日程安排。

MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为,  start <= x < end。

当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订。

每次调用 MyCalendar.book方法时,如果可以将日程安排成功添加到日历中而不会导致三重预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。

请按照以下步骤调用MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

示例:

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(50, 60); // returns true
MyCalendar.book(10, 40); // returns true
MyCalendar.book(5, 15); // returns false
MyCalendar.book(5, 10); // returns true
MyCalendar.book(25, 55); // returns true
解释:
前两个日程安排可以添加至日历中。 第三个日程安排会导致双重预订,但可以添加至日历中。
第四个日程安排活动(5,15)不能添加至日历中,因为它会导致三重预订。
第五个日程安排(5,10)可以添加至日历中,因为它未使用已经双重预订的时间10。
第六个日程安排(25,55)可以添加至日历中,因为时间 [25,40] 将和第三个日程安排双重预订;
时间 [40,50] 将单独预订,时间 [50,55)将和第二个日程安排双重预订。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/my-calendar-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路:要检查是否是三重预定,先找出双重预定的时间点,再用日程安排去和双重预定时间比较。

方法1:

public class MyCalendarTwo {
private List calendarList;
private List overlapList;

public MyCalendarTwo() {
calendarList = new ArrayList<>();
overlapList = new ArrayList<>();
}

public boolean book(int start, int end) {
for(int[] cal : overlapList) {
if(start < cal[1] && end > cal[0]) {
return false;
}
}

for(int[] cal : calendarList) {
if(start < cal[1] && end > cal[0]) {
overlapList.add(new int[] {Math.max(start, cal[0]), Math.min(end, cal[1])});
}
}

int[] calendar = new int[2];
calendar[0] = start;
calendar[1] = end;
calendarList.add(calendar);

return true;
}
}

方法2:使用TreeMap存储日程的开始和结束时间的key,开始记为1,结束记为-1. TreeMap是排序的,所以日程都是按时间进行的。如果有3个在同时开的会议,则返回false,否则为true.
class MyCalendarTwo {
    TreeMap calendars;
    public MyCalendarTwo() {         calendars = new TreeMap<>();     }
    public boolean book(int start, int end) {         calendars.put(start, calendars.getOrDefault(start,0) + 1);         calendars.put(end, calendars.getOrDefault(end, 0) - 1);
        int active = 0;         for(int val : calendars.values()) {             active += val;             if(active >= 3) {       //如果超过3个则把添加的日程删掉。                 calendars.put(start, calendars.get(start) - 1);                 calendars.put(end, calendars.get(end) + 1);                 if(calendars.get(start) == 0) {                     calendars.remove(start);                 }                 return false;             }         }
        return true;     } }   4. 我的日程安排3

实现一个 MyCalendar 类来存放你的日程安排,你可以一直添加新的日程安排。

MyCalendar 有一个 book(int start, int end)方法。它意味着在start到end时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为,  start <= x < end。

当 K 个日程安排有一些时间上的交叉时(例如K个日程安排都在同一时间内),就会产生 K 次预订。

每次调用 MyCalendar.book方法时,返回一个整数 K ,表示最大的 K 次预订。

请按照以下步骤调用MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

示例 1:

MyCalendarThree();
MyCalendarThree.book(10, 20); // returns 1
MyCalendarThree.book(50, 60); // returns 1
MyCalendarThree.book(10, 40); // returns 2
MyCalendarThree.book(5, 15); // returns 3
MyCalendarThree.book(5, 10); // returns 3
MyCalendarThree.book(25, 55); // returns 3
解释:
前两个日程安排可以预订并且不相交,所以最大的K次预订是1。
第三个日程安排[10,40]与第一个日程安排相交,最高的K次预订为2。
其余的日程安排的最高K次预订仅为3。
请注意,最后一次日程安排可能会导致局部最高K次预订为2,但答案仍然是3,原因是从开始到最后,时间[10,20],[10,40]和[5,15]仍然会导致3次预订。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/my-calendar-iii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路: 使用TreeMap,记录下同时段开会的最大值。

class MyCalendarThree {     TreeMap calendars;
    public MyCalendarThree() {         calendars = new TreeMap<>();     }          public int book(int start, int end) {         calendars.put(start, calendars.getOrDefault(start, 0) + 1);         calendars.put(end, calendars.getOrDefault(end, 0) -1);         int count = 0;         int result = 0;         for(int val : calendars.values()) {             count += val;             result = Math.max(count, result);         }         return result;     } }   5. 四数相加  

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。

例如:

输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

输出:
2

解释:
两个元组如下:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/4sum-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路:把四数分为两两相加后再相加。两两相加的和作为key,并统计次数放入hashMap. 如果另外两两相加的和为-key,这找到组合。

class Solution {     public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {         HashMap abMap = new HashMap<>();
        for(int v : A) {             for(int k : B) {                 abMap.put(v + k, abMap.getOrDefault(v + k, 0) + 1);             }         }                  int answer = 0;
        for(int v : C) {             for(int k : D) {                 if(abMap.containsKey(-v-k)) {                     answer += abMap.get(-v-k);                 }             }         }         return answer;     } }   6. 常数时间插入、删除和获取随机元素

设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构。

insert(val):当元素 val 不存在时,向集合中插入该项。
remove(val):元素 val 存在时,从集合中移除该项。
getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。
示例 :

// 初始化一个空的集合。
RandomizedSet randomSet = new RandomizedSet();

// 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomSet.insert(1);

// 返回 false ,表示集合中不存在 2 。
randomSet.remove(2);

// 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomSet.insert(2);

// getRandom 应随机返回 1 或 2 。
randomSet.getRandom();

// 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomSet.remove(1);

// 2 已在集合中,所以返回 false 。
randomSet.insert(2);

// 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
randomSet.getRandom();

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/hash-table/xx0zpt/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

解题思路:此题的关键点是要求在O(1)下操作。添加删除,hashMap都能O(1),但是random没法达到目的,所以需要List来存储元素,hashmap做位置索引。

class RandomizedSet {     HashMap set;     List list;     Random random;     /** Initialize your data structure here. */     public RandomizedSet() {         set = new HashMap<>();         list = new ArrayList<>();         random = new Random();     }          /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */     public boolean insert(int val) {         if(set.containsKey(val)) {             return false;         } else {             list.add(val);             set.put(val, list.size() - 1);             return true;         }     }          /** Removes a value from the set. Returns true if the set contained the specified element. */     public boolean remove(int val) {         if(!set.containsKey(val)){             return false;         }
        int index = set.get(val);         list.set(index,list.get(list.size() - 1));         set.put(list.get(list.size() - 1),index);                  list.remove(list.size() - 1);               set.remove(val);
        return true;     }          /** Get a random element from the set. */     public int getRandom() {         return list.get(random.nextInt(list.size()));     } }
/**  * Your RandomizedSet object will be instantiated and called as such:  * RandomizedSet obj = new RandomizedSet();  * boolean param_1 = obj.insert(val);  * boolean param_2 = obj.remove(val);  * int param_3 = obj.getRandom();  */   7. 给定一个非空的整数数组,返回其中出现频率前 高的元素。

class Solution {
  public int[] topKFrequent(int[] nums, int k) {
    HashMap numCount = new HashMap<>();

    for(int num : nums) {
      numCount.put(num, numCount.getOrDefault(num, 0) + 1);
    }

    PriorityQueue preKNum = new PriorityQueue<>((num1, num2) -> {
      return num1[1] - num2[1];
    });

    for(Map.Entry entry : numCount.entrySet()) {
      int num = entry.getKey();
      int count = entry.getValue();
      if(preKNum.size() < k) {
        preKNum.offer(new int[]{num, count});
        continue;
      }
      if(preKNum.peek()[1] < count) {
        preKNum.poll();
        preKNum.add(new int[]{num, count});
      }
    }
    int size = preKNum.size();
    int[] result = new int[size];
    for(int i = size -1 ; i >=0; i--){
    result[i] = preKNum.poll()[0];
    }
    return result;
  }
}

8. 寻找重复的子树

给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。

两棵树重复是指它们具有相同的结构以及相同的结点值。

示例 1:

          1
         /    \
       2      3
      /       /  \
    4      2    4
           /
         4
下面是两个重复的子树:

      2
     /
   4

  4
因此,你需要以列表的形式返回上述重复子树的根结点。

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/hash-table/xxm0i6/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

思路:把每条路径和子路径都作为key存入HashMap。重复出现的路径进行技计数统计。空节点返回#.

class Solution {  public List findDuplicateSubtrees(TreeNode root) {         Map treeMap = new HashMap<>();         List result = new ArrayList<>();
        findDupSubTrees(root, treeMap, result);         return result;     }
    private String findDupSubTrees(TreeNode node, Map treeMap, List result)    {         if(node == null) {             return "#";         }         String serial = node.val + "," + findDupSubTrees(node.left, treeMap, result) + "," + findDupSubTrees(node.right, treeMap, result);
        treeMap.put(serial, treeMap.getOrDefault(serial, 0) + 1);         if(treeMap.get(serial) == 2) {             result.add(node);         }         return serial;     } }   9. 最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

示例 1:

输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
  请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

作者:Krahets
链接:https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5dgr0c/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution {     public int lengthOfLongestSubstring(String s) {         Map charMap = new HashMap<>();         int maxLength = 0;          int pre = -1;         for(int i = 0; i < s.length(); i++) {             if(charMap.containsKey(s.charAt(i))) {                 pre = Math.max(pre, charMap.get(s.charAt(i)));             }             charMap.put(s.charAt(i), i);             maxLength = Math.max(maxLength, i - pre);         }
        return maxLength;     } }    

763. 划分字母区间

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

输入:S = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca", "defegde", "hijhklij"。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。

提示:

  • S的长度在[1, 500]之间。
  • S只包含小写字母 'a' 到 'z' 。
class Solution {
    public List partitionLabels(String s) {
        List result = new ArrayList();
        Map charMap = new HashMap<>();
        char[] chars = s.toCharArray();

        for(int i = 0; i < chars.length; i++) {
            charMap.put(chars[i], i);
        }
        int start = 0;
        int end = -1;
        for(int i = 0; i < chars.length; i++) {   
            int lastIndex = charMap.get(chars[i]);
            if(lastIndex < end) {
                continue;
            }

            end = lastIndex;
            if(i == end) {
                result.add(end - start + 1);
                start = end + 1;
            }
        }

        return result;

    }
}

1711. 大餐计数

大餐 是指 恰好包含两道不同餐品 的一餐,其美味程度之和等于 2 的幂。

你可以搭配 任意 两道餐品做一顿大餐。

给你一个整数数组 deliciousness ,其中 deliciousness[i] 是第 i?????????????? 道餐品的美味程度,返回你可以用数组中的餐品做出的不同 大餐 的数量。结果需要对 109 + 7 取余。

注意,只要餐品下标不同,就可以认为是不同的餐品,即便它们的美味程度相同。

示例 1:

输入:deliciousness = [1,3,5,7,9]
输出:4
解释:大餐的美味程度组合为 (1,3) 、(1,7) 、(3,5) 和 (7,9) 。
它们各自的美味程度之和分别为 4 、8 、8 和 16 ,都是 2 的幂。

class Solution {
    public int countPairs(int[] deliciousness) {
        int mod = 1000000007;
        int len = deliciousness.length;
        int[] checks = new int[22];

        for(int i = 0; i < 22; i++) {
            checks[i] = (1 << i);
        }
        HashMap map = new HashMap<>();
        int pairs  = 0;
        for(int j = 0; j < len; j++) {
            int num = deliciousness[j];

            for(int k = 0; k < 22; k++) {
                int count = map.getOrDefault(checks[k] - num, 0);
                pairs = (pairs + count) % mod;
            }
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        return pairs;
    }
}