【LeetCode刷题笔记】哈希查找

771. 宝石与石头

【LeetCode刷题笔记】哈希查找_第1张图片

解题思路:
  • 1.  HashSet ,把所有 宝石 加入 set , 然后遍历检查 每一块石头是否包含在set中 ,若包含就是宝石。
  • 2. 计数数组map, 把所有 宝石 进行 count 数组 计数 ,, 然后遍历检查 每一块石头是否 count[stone] > 0 ,若符合就是宝石。
  • 注意:题目字符只包含英文字母,所以可以使用一个长度 128 int 数组来当做 map 使用,因为128个ASCII码包含了所有英文大小写字母。

HashSet实现的版本:

【LeetCode刷题笔记】哈希查找_第2张图片

计数数组实现的版本:

【LeetCode刷题笔记】哈希查找_第3张图片 

上面代码是先对宝石进行计数,然后去石头中判断比较,当然我们也可以先对石头进行计数,然后去宝石中判断比较,代码如下:

【LeetCode刷题笔记】哈希查找_第4张图片 

剑指 Offer 50. 第一个只出现一次的字符

【LeetCode刷题笔记】哈希查找_第5张图片

解题思路:
  • 1. HashMap ,遍历一遍字符数组使用 map 统计 每个字符和出现的次数,再遍历一遍字符数组,返回第一个在map中次数为 1 的字符。
  • 2. 计数数组 ,由于题目字符只包含 小写字母 ,因此可以使用一个 int[ 26] 数组 来代替 HashMap 进行计数。

【LeetCode刷题笔记】哈希查找_第6张图片

387. 字符串中的第一个唯一字符

【LeetCode刷题笔记】哈希查找_第7张图片

解题思路:
  • 计数数组 ,同 剑指 Offer 50. 第一个只出现一次的字符。

【LeetCode刷题笔记】哈希查找_第8张图片 

205. 同构字符串

【LeetCode刷题笔记】哈希查找_第9张图片

解题思路:
  • 1.根据字符出现的下标判断 , 相同位置的字符上次出现的下标应该是一样的 。 
  • 初始化 2 个长度为 128 数组型的Map(默认填充-1),用来存储每个字符和其出现的下标,遍历字符数组的长度,依次更新 s[i] = i t[i] = i 到这两个Map中,但是在更新之前判断一下,如果当前的 s[i]  t[i] Map中上次出现的下标不同,二者就不是同构字符串,返回false

 【LeetCode刷题笔记】哈希查找_第10张图片

【LeetCode刷题笔记】哈希查找_第11张图片 

如上图所示,两个同构的字符串,它们相同位置的字符的下标有两种情况:

  • 1)如果此时这两个字符在各自的Map中都没有出现过,它们取的下标都是 -1,然后它们在Map中会被更新成当前相同的下标
  • 2)如果此时这两个字符在各自的Map中有出现过,那么各自取出的下标一定是在上次相同的位置时被更新的下标

例如上面的 p 和 t,第一次遇到时,各自是 -1,然后 p 和 t 会在两个Map中被各自更新为 0,下次如果在相同位置再次出现 p 和 t,从两个Map中取出各自的下标都是 0。

而对于非同构的两个字符串,如下图:

【LeetCode刷题笔记】哈希查找_第12张图片

例如 o 和 a 这一对最后一次出现的下标分别是 1,这意味着下次左边出现 o 时,右边也必须出现 a,并且右边在这之前没有再次出现过 a,这样才能同构,否则右边字符出现的下标肯定不是 1 。

【LeetCode刷题笔记】哈希查找_第13张图片 

解题思路:
  • 2.  双向map ,使用 2个 HashMap 互相存对应位置的字符作为键和值。 map1  中映射 s->t 的对应关系, map2  中映射 t->s 的对应关系。
  • 遍历每个位置上的字符进行比较:
  • ① 如果当前的 s[i]  字符在  map1  中存在对应的 字符,则取出来看其是否等于当前的  t[i] ,否则将当前的 s[i] ->  t[i] 映射关系存入 map1 中。
  • ② 如果当前的 t[i]  字符在  map2  中存在对应的  s  字符,则取出来看其是否等于当前的  s[i]  ,否则将当前的 t[i] ->  s[i]  映射关系存入 map2  中。

【LeetCode刷题笔记】哈希查找_第14张图片 

【LeetCode刷题笔记】哈希查找_第15张图片

这里使用的是数组型的Map,你也可以直接使用HashMap。 

为什么必须用 2个Map
  • 因为 Map 多个key可以对应同一个value ,而题目要求的是正反都是一一对应的。所以只用 1 Map 无法满足这种双向的一一对应关系。
  【LeetCode刷题笔记】哈希查找_第16张图片

290. 单词规律

【LeetCode刷题笔记】哈希查找_第17张图片

解题思路:
  • 双向HashMap ,同205,也可以使用 出现的下标 判断。

【LeetCode刷题笔记】哈希查找_第18张图片 

注意,String 比较要用 equals,char 比较可以直接用 = 号。 

【LeetCode刷题笔记】哈希查找_第19张图片 

389. 找不同

【LeetCode刷题笔记】哈希查找_第20张图片

解题思路:
  • 1. 计数数组 ,先使用计数数组统计  t 中的字符,然后遍历 s  中的每个字符将次数减  1 由于  t  只比  s  多添加了 1 个字符,因此这时 count 数组中就只会剩下 1 大于  的字符了。最后再遍历一遍 count 数组,返回这个大于 0 的字符即可。

 【LeetCode刷题笔记】哈希查找_第21张图片

这个代码也可以反过来写,先计数 s 中的字符,然后遍历 t 中的字符减去次数,这样count数组中最终就只会出现1小于0的字符。代码如下: 

 【LeetCode刷题笔记】哈希查找_第22张图片

解题思路:
  • 2. 求ASCII码和的差值, t 中所有字符的 ASCII 码之和减去 中所有字符的 ASCII 码之和 ,就得到了多添加的那个的字符的 ASCII 码。

【LeetCode刷题笔记】哈希查找_第23张图片 

解题思路:
  • 3. 异或 ,利用异或的三个性质: 1)a^0 = a , 2 a^a = 0 ,3 a^b^c = a^c^b ,只需将  t 中的所有字符与  s  中的所有字符进行异或操作,相同的部分会被消除,最终结果就只会剩余只出现一次的字符,即新添加的字符。

【LeetCode刷题笔记】哈希查找_第24张图片

554. 砖墙

【LeetCode刷题笔记】哈希查找_第25张图片

解题思路:
  • 哈希统计每块砖的边缘出现的次数 ,在 边缘次数出现最多 的地方画线,则穿过的 砖块数最少
  • 统计每块砖的边缘出现的次数,也就是计算 每一行 中, 每块砖的右边缘 最左侧轴的距离 ,这相当于计算 每一行 数组中的每个数的 前缀和 ,然后用 Map 记录每种 前缀和出现的次数
  • 答案就是墙的行数 - Map中记录的前缀和出现的最大次数

【LeetCode刷题笔记】哈希查找_第26张图片 

【LeetCode刷题笔记】哈希查找_第27张图片 

【LeetCode刷题笔记】哈希查找_第28张图片 

本题看似花哨,其实是转换成求前缀和的最大次数。另外题目要求不能在整个墙的两侧边缘穿过,因此在计算每一行的前缀和时,要特别留意不能计算最后一个数

【LeetCode刷题笔记】哈希查找_第29张图片

242. 有效的字母异位词

【LeetCode刷题笔记】哈希查找_第30张图片

解题思路:
  • 1. 计数数组 ,先用计数数组对  中的字符进行统计,然后遍历 t 中的每个字符,将其在计数数组中的次数 减1
  • 若  和  中每个字符出现的 次数都相同 ,则这样操作之后 count 数组会归 0 ,因此这时再遍历一遍 count 数组,如果发现出现 非0 的次数,则  和  t 不是 字母异位词,否则就是。
  • 对于进阶问题:如果字符包含unicode字符,则使用普通的HashMap代替计数数组即可。 

【LeetCode刷题笔记】哈希查找_第31张图片 

这里后面两个for循环也可以合并成一个,在遍历 t 的同时判断是否出现计数小于 0 的,因为如果 t 中出现了不存在于 s 的字符,那么在进行计数减 1 的过程中该字符会变成负数(初始值默认是0)。代码如下:

【LeetCode刷题笔记】哈希查找_第32张图片

解题思路: 
  • 2.  排序 若  和  中每个字符出现的 次数都相同 ,则对二者的字符数组分别排序之后,它们组成的两个字符串应该相等。
  • 可以转成 char[] 后, Arrays.sort ,再 Arrays.equals
【LeetCode刷题笔记】哈希查找_第33张图片

49. 字母异位词分组

【LeetCode刷题笔记】哈希查找_第34张图片

解题思路: 
  • 哈希 ,如果两个单词互为字母异位词,那么这两个单词中的字母按照字典序排序后生成的两个新字符串应该是相同的,或者说对这两个单词进行计数之后的计数数组是相同的,因此可以使用相同的排序值或计数数组来作为哈希的key进行分组。
  • 1. 可以使用 任一个异位词的排序值 作为 key 来存储 相关的一组异位词组合 ,例如 aet --> ["ate","eat","tea"]。具体的,我们可以将每个单词转成char[]形式,对其调用Arrays.sort(),然后再把char[]转成String形式作为 key
  • 2. 可以使用 计数数组的字符串 形式作为 key

【LeetCode刷题笔记】哈希查找_第35张图片

使用计数数组作为key的代码:(由于主流程代码无需变化,我们只需替换getKey方法即可) 

【LeetCode刷题笔记】哈希查找_第36张图片 

956. 最高的广告牌

【LeetCode刷题笔记】哈希查找_第37张图片

解题思路: 
  • 哈希 + 贪心 ,Map中的 Key 为:两边的差值 diffMap中的 Value 为: 产生diff的两条边中 较短的一边的最大值 ( best shorter )。
  • 由于 map  中的 key diff value shorter rod ,而 longer rod  可以通过 diff + shorter rod  计算出来,因此可以认为  map  中的一组key  value其实就是描述了广告牌左右两边的一对支架高度及其差值。所以我们的目标就是更新这个 map 使其产生最优的结果(diff 为 0 的 value 短边最大,即产生最高的 一对支架)。
  • 我们遍历每一个 rod 尝试以下两种策略来更新 map 的每一组 key  value
  • ① 如果将当前  rod  放入较长的那一边,则会产生一个新的 diff ,将这个新的 diff  及可能出现的新的最大的  shorter rod  更新进 map
  • ② 如果将当前  rod  放入较短的那一边,则会产生一个新的 diff 将这个新的 diff  及可能出现的新的最大的  shorter rod  更新进 map
  • 最后取出 差值为 0 的 value ,即为答案(差值为 0 的最高的一对支架)。

【LeetCode刷题笔记】哈希查找_第38张图片

【LeetCode刷题笔记】哈希查找_第39张图片

【LeetCode刷题笔记】哈希查找_第40张图片

上面代码中有三点需要注意:

  • 1)使用的是数组型的map,这是因为题目中给出sum(roads[i])的最大值是5000,也就是说所有的钢筋加起来最大是5000,而最小值是 0,所以我们使用一个长度为 5001 int 数组就可以表示所有这些钢筋之间的差值了。(如果题目没有这个提示,只能用原生的 HashMap 了,如果用原生的map,就不用默认填充-1了,因为key不存在的时候,默认取到的value是null)
  • 2)map初始化时,设置了 map[0] = 0,也就是默认有两个高度都是 0 的钢筋,它们的差值是 0,如果没有这个初始设置,显然下面的第二个for循环中的真正逻辑一次也不会被执行。(另外这个初始值正好可以对应题目中无法安装广告牌的情况,返回的是0)
  • 3)遍历和更新map时,其实是分开的,遍历的是原始map,更新的是当前的map,也就是说我们不能一边遍历map一边又在修改这个map,否则会出现问题(因为还没有遍历到的值可能被修改掉了),所以这里拷贝了一份原始的map做为遍历时的参考。

128. 最长连续序列

【LeetCode刷题笔记】哈希查找_第41张图片

解题思路: 
  • 1. HashSet + 贪心 先数字全部放入 HashSet 中,然后 遍历数组 ,遇到每一个元素时,以 当前元素 为起点,不停的循环判断+1后的值是否在set中,在就 计数+1 。直到 +1后的值 不在 set 中了,说明 当前元素 为起点的 连续序列到此中断了,则当前计数值 count 就是 当前元素 为起点的连续序列的长度。记录这个长度的最大值就是所求答案。
【LeetCode刷题笔记】哈希查找_第42张图片

注意:在遍历当前num时,先判断如果 set 中包含 num - 1,则应该跳过当前的num,因为较小的数会形成更长连续序列,所以可以放弃当前较大的数,只需要处理较小的数就行。(重要关键点,这里有一点贪心思想,不加这个判断会超时)

【LeetCode刷题笔记】哈希查找_第43张图片 

这个代码如果不加 if (set.contains(num - 1)) continue; 这一句最坏时间复杂度可能是 O(n^2),加了才是 O(n)

解题思路: 
  • 2. 排序 ,然后按顺序统计连续的数量,不过时间复杂度是 O(nlogn) 不是 O(n)

 【LeetCode刷题笔记】哈希查找_第44张图片

888. 公平的糖果交换

【LeetCode刷题笔记】哈希查找_第45张图片

解题思路:
  • HashSet,一句话总结题意就是: 两个数组各自交换一个数给对方后,两个数组的和相等
  • 假设两人的糖果总数分别是 sumA sumB A x B,B  A,则可以得到简单的代数公式:
  • sumA - x + y = sumB - y + x => x = y + (sumA - sumB) / 2
  • 因此可以遍历  B 数组,固定每一个  y ,按公式计算出 x ,然后判断  x 是否 A 中即可(通过 HashSet )。如果存在,那么 (x,y) 就是一个解。

 【LeetCode刷题笔记】哈希查找_第46张图片

454. 四数相加 II

【LeetCode刷题笔记】哈希查找_第47张图片

解题思路:
  • 哈希计数 先用 Map 统计  A、B 数组中和为  a + b  的个数,再遍历 C、D 数组,判断 target = 0 - (c + d) 是否在 Map 中存在,如果存在就累加其在  Map  中记录的次数

【LeetCode刷题笔记】哈希查找_第48张图片

380. O(1) 时间插入、删除和获取随机元素

【LeetCode刷题笔记】哈希查找_第49张图片 

解题思路:
  • 哈希 + 随机数 + 动态数组 ,关键点: 一维数组 用来存储元素并在 删除元素 时使用 最后元素覆盖删除位置达到O(1)时间要求, map  用来记录 元素和其在数组的下标映射关系, 随机数 用于 getRandom()生成随机下标从一维数组中取值返回。
  • 1)每次插入函数调用中, 元素存入一维数组中,并在 map 记录 元素 当前下标
  • 2)在删除函数中,获取到要删除元素在 map 中的 下标 i ,然后用 原数组的最后一个元素 last 覆盖nums[i]元素 ,并 更新 last 在 map 中的值为 i ,然后从动态数组map中删除该元素(map中删除只需remove(key),动态数组只需把记录的当前索引减1)。
  • 3) 获取随机项的函数中,只需要生成一个 [0, nums.length) 随机数 下标 返回nums在该下标的值即可。

【LeetCode刷题笔记】哈希查找_第50张图片

146. LRU 缓存

【LeetCode刷题笔记】哈希查找_第51张图片

解题思路:
  • 1. 直接使用 LinkedHashMap
  • 1)LRUCache构造函数的实现:创建  new  LinkedHashMap <>(capacity,  0.75f true )  LinkedHashMap 的构造函数 第一个参数 容量 第二个参数 加载因子 第三个参数 表示 是否按照访问顺序排序 ,默认是 false 表示 按插入顺序排序 ,只需将第三个参数改成 true 即可,这时表示按访问顺序排序。同时将 capacity 记录为成员变量。
  • 2)get方法的实现:直接返回 map.getOrDefault(key, -1)
  • 3) put 方法的实现: map 中插入数据后,判断 map的大小如果超过容量capacity,就删除最久未使用的那个 。LinkedHashMap 按 访问顺序排序 时, 表头是最久未使用的, 最近使用过的排在表尾 通过 map.keySet() 拿到第一个 key ,然后移除第一个 key 即可

【LeetCode刷题笔记】哈希查找_第52张图片

这里删除的地方也可以使用迭代器删除,不过在调用迭代器的remove方法时,先要调用一次next()方法,因为迭代器的remove方法删除的是上次迭代器遍历时返回的元素。 如下:

LinkedHashMap 按访问顺序排序时,最近使用过的排在表尾,表头是最久未使用的。remove() 方法删除的是上次 return 的 element(通过 lastRet 控制),而 Iterator 提供了由前向后 next() 及从后向前 previous() 两种遍历方式,在无法确定遍历方式的情况下,remove() 方法是无法确定要删除哪一个 element 的,因此需先调用 next() 或 previous() 记录上次 return element 的 index,即 lastRet。迭代器的索引是从 0 开始的。  

解题思路:

  •  2. 继承 LinkedHashMap,我们重写父类的 removeEldestEntry() 方法,如果当前map大小超过容量capacity就让该方法返回true,该方法返回 true 表示会 删除表头元素

  • 1)LRUCache构造函数的实现:直接调用 super(capacity, 0.75f, true); 并将 capacity 记录为成员变量。

  • 2)get() put() 方法的实现均调用父类的对应方法即可。

  • 父类调用顺序:put() afterNodeInsertion()  removeEldestEntry() 

【LeetCode刷题笔记】哈希查找_第53张图片

简单了解 LinkedHashMap(跟题目有关的知识点)

HashMap 大家都清楚,底层是 数组 + 红黑树 + 链表,同时其是 无序的,而 LinkedHashMap 刚好就比 HashMap 多这一个功能,就是其提供 有序,并且,LinkedHashMap的有序可以按两种顺序排列,一种是按照 插入的顺序,一种是按照 读取 的顺序(这个题目的示例就是告诉我们要按照 读取的顺序进行排序),而其内部是靠 建立一个双向链表 来维护这个顺序的,在每次插入、删除后,都会调用一个函数来进行 双向链表的维护 ,准确的来说,是有 三个函数来做这件事,这三个函数都统称为 回调函数 ,这三个函数分别是:
  • void afterNodeAccess(Node p) { }  其作用就是 在访问元素之后,将该元素放到双向链表的尾巴处 (所以这个函数只有在按照 读取 的顺序的时候才会执行),之所以提这个,是建议大家去看看,如何优美的实现在双向链表中将指定元素放入链尾!
  • void afterNodeRemoval(Node p) { }  其作用就是 在删除元素之后,将元素从双向链表中删除 ,还是非常建议大家去看看这个函数的,很优美的方式在双向链表中删除节点!
  • void afterNodeInsertion(boolean evict) { } 这个才是我们题目中会用到的, 在插入新元素之后,需要回调函数判断是否需要移除一直不用的某些元素
其次,我们再看一下 LinkedHashMap构造函数!其主要是两个构造方法,一个是继承 HashMap ,另一个是可以选择 accessOrder 的值来确定是按 插入顺序还是 读取顺序排序。(默认 false,代表按照 插入顺序排序)

【LeetCode刷题笔记】哈希查找_第54张图片 

【LeetCode刷题笔记】哈希查找_第55张图片

解题思路: 
  • 3. HashMap + 双向链表 ,纯手工打造一个LRUCache
  • 1)创建一个 双向链表节点类 ,它包含 key value , 以及前驱指针 prev 和后继指针 next
  • 2)在 LRUCache 类中,使用一个 map 来存储 key 双向节点类 的缓存,使用虚拟头结点 head 和虚拟尾结点 tail 指向双向链表,在构造函数中 初始化这两个节点 ,并让 头尾相连 ,建立起双向链表。
  • 3)在 get 方法中,首先获取 key 对应的节点,如果节点为空,返回 -1 ,否则将节点移动到表头,然后返回节点的值即可。
  • 4) put 方法中,同样首先获取 key 对应的节点,如果 节点存在 ,就更新节点的值,然后 节点移动到表头 即可。如果节点不存在,就创建一个包含 key-value 新节点 ,将新节点 存入map 并添加到双向链表的 表头 ,然后判断一下 当前map的大小是否超过了容量capacity ,如果超了,就 删除表尾的节点 ,并同时 从map中移除对应表尾节点的key

【LeetCode刷题笔记】哈希查找_第56张图片

这个代码看起来很长,在实现的时候,我可以先写出 get 和 put 方法的主逻辑,然后再补充实现里面具体需要用到的操作双向链表的相关方法。 

在 get 和 put 方法里其实都可以分成 2 种情况:key 存在和 key 不存在的情况。

  • 对于 key 存在的情况,这两个方法中我们都需要将其移动到表头;
  • 对于 key 不存在的情况,get 方法直接返回 -1,而 put 方法需要创建新的节点放入map,同时将新节点添加到表头,并做出容量超限判断,一旦超出就删除表尾。
关于将 节点设置为最久未使用有两种方式:
  • 一种是每次  get/put 时都将节点其移动到表头,然后表尾自然就是最久未使用的了,因此在超出容量时,只需要移除表尾的节点即可。
  • 另一种则是跟上面相反的, 每次  get/put 时都将节点其移动到表尾,那么表头则是最久未使用的。(这种跟 LinkedHashMap 按照访问顺序排序的情况是一致的)

这两种方式本质比较类似,都是将使用过的节点不停的往链表的一端去移动,这样另一端的节点自然就是最久未使用的了。我们理解这个原理就可以了,至于实际使用哪一种完全看你的喜好。 

下面是关于上面代码的一些执行流程图示,以帮助理解:

【LeetCode刷题笔记】哈希查找_第57张图片

【LeetCode刷题笔记】哈希查找_第58张图片【LeetCode刷题笔记】哈希查找_第59张图片

【LeetCode刷题笔记】哈希查找_第60张图片 

注意:本题虽然前两种直接利用 LinkedHashMap 的方法可行,但是在面试时应该直接写出第 3 种手工打造的方法,否则即便用前 2 种方法也会被追问 LinkedHashMap 的源码实现。

217. 存在重复元素

【LeetCode刷题笔记】哈希查找_第61张图片

解题思路:
  • 1. 哈希查找 ,遍历数组,将每个元素放入 HashSet 中,放入之前判断一下,如果 set 中已经包含了该元素,说明存在重复 2次 的元素。
  • 2. 先排序 ,然后遍历数组,如果出现 前后挨着的两个元素相等 ,就返回 true

【LeetCode刷题笔记】哈希查找_第62张图片

【LeetCode刷题笔记】哈希查找_第63张图片

219. 存在重复元素 II

【LeetCode刷题笔记】哈希查找_第64张图片

解题思路:
  • 1. 哈希查找 ,题目要求的就是数组中是否存在两个相同的数字,它们的下标之差 ≤ k
  • 遍历数组,将每个 元素 和其 对应的下标 放入 HashMap 中,放入之前判断一下,如果 map 中已经 包含 该元素,就从 map 取出 该元素之前出现的下标 ,记为 j ,然后计算如果满足 i - j <= k ,就返回 true ,如果循环结束也没有返回true,最后就返回false。 

【LeetCode刷题笔记】哈希查找_第65张图片

解题思路: 
  • 2. 滑动窗口 + 哈希 ,维护一个大小为  的滑动窗口,在窗口范围内用哈希查找。
  • 具体地,初始 L=0,R=0 ,遍历数组, R++ 不断扩充窗口的右边界 ,将遇到的每个元素加入 Set 如果 set 大小超过了窗口大小  k , 就 set 中移除左边界  处的元素,并收缩左边界 L++
  • 每个元素加入 Set 之前,判断一下,如果 set 中已经包含了 R 处的元素,说明存在解,返回 true 。因为窗口大小为 k 所以此时窗口右侧 R 位置的元素和 set 中的重复元素的下标之差肯定 ≤ k 。 

【LeetCode刷题笔记】哈希查找_第66张图片 

这个方法中,HashSet有两个作用:

  • 1)控制窗口的大小最多是 k,不会超过 k,一旦超过 k,就从 set 移除并收缩 L,这样当遇到 R 处的元素时,窗口 + R 最多会形成一个长度为 k + 1 的区间,所以这个区间内的元素下标差值一定 ≤ k (因为区间长度是下标之差 + 1,[L, R] 区间的长度为 R - L + 1 ≤ k + 1,则 R - L ≤ k
  • 2)保证窗口内无重复元素,且可以快速判断遇到的 R 处元素是否与窗口内的元素相同,一旦出现相同,在 1)的保证下,就存在答案
【LeetCode刷题笔记】哈希查找_第67张图片

220. 存在重复元素 III

【LeetCode刷题笔记】哈希查找_第68张图片

解题思路: 
  • 1. 滑动窗口 + 有序表(TreeSet) ,用 TreeSet 维护一个大小为 k 的滑动窗口,每当遇到一个元素时,就在 TreeSet 查找满足 numsj ≥  nums[i] - t key
  • 初始 L=0,R=0 ,遍历数组 R++ 不断扩充窗口的右边界 ,将遇到的每个元素加入 TreeSet集合中
  • 在放入之前,从 TreeSet 中查找 nums[R] - t  的值,记为 numsj ,如果 numsj不为空 ,且 abs(nums[R] - numsj) <= t ,说明存在解,返回 true
  • 在放入之后,判断一下,如果 TreeSet集合大小超过了 k ,就 从set中移除左边界处元素并收缩左边界 L++

【LeetCode刷题笔记】哈希查找_第69张图片

这里下标之差 ≤ k 同样是通过 TreeSet 控制窗口大小不超过 k 来保证的,但是本题与219相比,多了一个元素值之差 ≤ t 的条件,我们将遍历遇到的 R 看作是 nums[i],利用不等式很容易推导出来,就是要在 TreeSet 中找 ≥ nums[R] - tnums[j] 元素,因此每当遇到一个 R 时,就计算出 key = nums[R] - t,然后利用 TreeSe.ceiling(key) 方法来查找,它返回的是 ≥ key 且离 key 最接近的值。

解题思路:  
  • 2. 滑动窗口 + 桶 我们思考这样一个问题:某天老师让全班同学各自说出自己的出生日期,然后统计一下出生日期相差小于等于30天的同学。我们很容易想到,出生在同一个月的同学,一定满足上面的条件。出生在相邻月的同学,也有可能满足那个条件,这就需要计算一下来确定了。但如果月份之间相差了两个月,那就不可能满足这个条件了。
  • 本题分桶的核心思想跟这个问题类似, 将所有数字分到 k 个桶中,每个桶大小为 t + 1 ,这种分法能保证 同一个桶内的元素一定满足差值 ≤ t ,而差值  ≤ t  的元素,要么出现 同一个桶内 ,要么出现在 相邻的桶之间 ,但绝对 不可能出现在跨桶之间
  • 具体地,用一个 map 来保存 桶的编号 放入桶内的元素 ,初始 L=0,R=0 ,遍历数组, R++ 不断扩充右边界,将nums[R]元素计算出应放入的 桶编号id , 更新到 map 当中
  • 1)在更新map之前,判断一下,如果当前计算出的 桶编号 id 在 map 中已经存在 了,说明 同一个桶内有多于 1 个元素 ,一定存在解,返回 true ;否则就判断 前后相邻的桶中是否存在差值 ≤ t  的解,让 当前的桶编号 id 分别 -1 和 +1 得到前后桶的编号 id ,然后到 map 中查找,如果找到且与当前 R 元素差值  ≤ t ,就返回 true
  • 2)在更新map之后,看一下,如果map的大小超过了窗口大小 k,就从map移除最左边元素对应的桶编号 id收缩左边界 L++
  • 注意点:如何计算 桶的编号id :桶的大小是 size = t + 1,如果 nums[R] ≥ 0 ,就返回 nums[R] / size , 如果 nums[R] < 0 ,就返回 (nums[R] + 1) / size   - 1
【LeetCode刷题笔记】哈希查找_第70张图片

【LeetCode刷题笔记】哈希查找_第71张图片 

【LeetCode刷题笔记】哈希查找_第72张图片

这里下标之差 ≤ k 同样是通过 Map 的大小来控制窗口大小不超过 k 来保证的,而元素值之差 ≤ t 是通过分桶思想来实现的,Map 的大小就可以看作是桶的大小,Map key 就是桶的 idMap 的 key 对应的若干 value 值就是桶内若干元素,这些元素值之差满足 ≤ t。一旦 Map 中相同的 key 出现了 2 个 value 则存在解。如果桶内无解,则判断相邻的桶。

这个方法中比较难理解的可能是 BucketId 的生成逻辑:

1. 为什么 size 需要对 t 进行+1 操作?

  • 目的是为了确保差值小于等于 t 的数能够落到一个桶中。举个例子,假设 [0, 1, 2, 3],t=3,显然四个数都应该落在同一个桶。如果不对 t 进行 +1 操作的话,那么 [0, 1, 2] 和 [3] 会被落到不同的桶中,那么为了解决这种错误,我们需要对 t 进行+1 作为 size。 这样我们的数轴就能被分割成:0 1 2 3 | 4 5 6 7 | 8 9 10 11 | 12 13 14 15 | ...
  • 总结一下,令 size = t +1 的本质是因为差值为 t 两个数在数轴上相隔距离为 t+1,它们需要被落到同一个桶中。当明确了 size 的大小之后,对于正数部分我们则有 idx = nums[i] / size。

2. 如何理解负数部分的逻辑?

  • 由于我们处理正数的时候,处理了数值 0,因此我们负数部分是从 -1 开始的。
  • 还是我们上述例子,此时我们有 t=3 和 size=t+1=4。考虑 [-4, -3, -2, -1] 的情况,它们应该落在一个桶中。如果直接复用 idx = nums[i] / size 的话,[-4] 和 [-3, -2, -1] 会被分到不同的桶中。
  • 根本原因是我们处理整数的时候,已经分掉了数值 0。
  • 这时候我们需要先对 nums[i] 进行 +1 操作(即将负数部分在数轴上进行整体右移),即得到 (nums[i] + 1) / size。 这样一来负数部分与正数部分一样,可以被正常分割了。
  • 但由于 0 号桶已经被使用了,我们还需要在此基础上进行 -1,相当于将负数部分的桶下标(idx)往左移,即得到 ((nums[i] + 1) / size) - 1。

注意,目前 LeetCode 上这道题的提示条件已经修改了,元素值和 k、t 的取值范围都缩小了,否则前面的代码中使用 Integer 的地方全部需要换成 Long 以防止越界。

小结

通过以上几道题,我们可以总结出一个滑动窗口 + 哈希的题型逻辑套路: 

【LeetCode刷题笔记】哈希查找_第73张图片

LCP 03.机器人大冒险

【LeetCode刷题笔记】哈希查找_第74张图片

解题思路:
  • 哈希查找 ,题目给定的机器人从 (0, 0) 点出发只会按照 command 指令无限循环走,这相当于提前告诉了我们机器人走的路线,我们根据 command 指令可以把机器人预计会经过的每一个 坐标点 先保存到 哈希表 中,然后我们只需检查输入的坐标点 (x, y) 是否在 哈希表 中记录的路径上即可。同时由于存在障碍物,我们对输入的障碍物 obstacles 中的每一个坐标点,同样到 哈希表 中判断一下是否会出现在机器人经过的路径,如果出现,则不可达。
  • 由于题目给出的 command 指令数据长度最大只有 1000 ,因此我们可以用一个 int 型变量来存储机器人经过的坐标点,具体用  x<<10 | y ,即使用 高10位 存储 x坐标 低10位 存储 y坐标 ,这样我们只需要用一个 HashSet 即可存储所有点。当然你也可以使用字符串拼接的形式来存储坐标,如 "x_y" 或  "x, y" 之类的。
例如,对于指令 "RURU" 而言,机器人在第一轮会经过下面两个点:

【LeetCode刷题笔记】哈希查找_第75张图片 

由于会反复执行同样的指令,机器人在第二轮又会经过下面两个点: 

【LeetCode刷题笔记】哈希查找_第76张图片 

这样下来,我们就能得到机器人经过的所有坐标点的路径,那么对于输入的目标坐标 (x, y) 和障碍物点的集合 obstacles ,只需要判断是否会出现在该路径上即可: 

【LeetCode刷题笔记】哈希查找_第77张图片

注意:目标点(x, y)可能在第一轮中不会达到,因此,上面代码中在 canReach 方法中,先将 x 和 y 分别除以 R 和 U, 其中 R 和 U 是机器人在第一轮向右走和向上走可以达到的最远距离。举例来说,假设机器人第一轮向右走和向上走最远可以到达 [2, 3],而目标点是 [6, 8],显然机器人在第一轮中不可能到达该点,因此先用 min[6/2, 8/3] 得到 2,也就是经过 2 轮之后,距离目标点 [6, 8] 还剩 [6, 8] - [2, 3] x 2 = [2, 2],此时判断 [2, 2] 是否在路径上即可,相当于以 2 轮之后的位置作为新的坐标原点 (0, 0) 来判断,因为我们在 HashSet 中只记录了机器人从(0,0)出发在第一轮中经过的点,并没有记录无限轮经过的每一个点

【LeetCode刷题笔记】哈希查找_第78张图片 

218. 天际线问题

【LeetCode刷题笔记】哈希查找_第79张图片

解题思路:
  • TreeMap ,使用两个 TreeMap 分别用来统计  <高度,高度出现次数>  ,将这两个 TreeMap 记为 heightCountMap xHeightMap
  • building 数组的每个元素的 x坐标 高度h , 以及 高度的变化 封装成一个 Node(x, h, '+'/'-') 节点,使用 '+' '-' 表示高度增加或者减少了,因此可以 构建出一个长度为 2N Node 数组(因为 building 数组的每个元素包含 x1,x2 两个横坐标
  • 然后将 Node 数组按照 x坐标 进行升序排序 ,接下来就可以遍历 Node 数组更新上面定义的两个 TreeMap ,每当遇到增加的高度就让 heightCountMap 计数 +1 每当遇到减少的高度 heightCountMap 计数-1,减到 0 就移除。更新 heightCountMap 的同时可以更新 xHeightMap ,可以通过 heightCountMap lastKey() 获得最大的 key ,也就是此时最大的高度
  • 最后根据 xHeightMap 来构造输出答案即可。

 

【LeetCode刷题笔记】哈希查找_第80张图片【LeetCode刷题笔记】哈希查找_第81张图片

一道TreeMap应用面试题

给定数组 hard 和 money,长度都为 N,hard[i] 表示 i 号工作的难度,money[i] 表示 i 号工作的收入。

给定数组 ability,长度为 M,ability[j] 表示 j 号人的能力。每一号工作,都可以提供无数的岗位,难度和收入都一样,但是人的能力必须 ≥ 这份工作的难度,才能上班。

请返回一个长度为 M 的数组 ans,ans[j] 表示 j 号人能获得的最好收入。

解题思路:

  • 利用有序表:TreeMap按照key从小到大排序的特性,使用一个TreeMap来存储<工作难度hard, 工作报酬money>

  • 1)根据 hard 和 money 构造Job(hard,money)对象,得到Job数组,然后对Job数组排序,先按照难度由小到大排序,难度一样的按照报酬从高到底排序

  • 2)遍历Job数组,更新TreeMap,TreeMap只保留难度不同的工作,如果当前难度与之前难度不同,只有当前的报酬比之前高的才放入map

  • 3)从有序表TreeMap中查询最接近 ability[i] 的 key 值,就得到了 ≤ ability[i] 的最大难度,查询它对应的报酬即可。

 注:TreeMap.floorKey(X) 返回小于等于 X 的最大的 key。

面试题

只由小写字母(a~z)组成的一批字符串,都放在字符类型的数组 String[] arr 中,如果其中某两个字符串所含有的字符种类完全一样,就将两个字符串算作一类。

比如:baacbba 和 bac 就算作一类,返回 arr 中有多少类?

解题思路:

  • 计数数组,使用一个整型的 26 位二进制位作为计数数组,对于每个字符串进行计数,这样每个字符串可生成一个整数作为哈希的key,因此使用一个HashSet即可统计arr中的全部种类数。

【LeetCode刷题笔记】哈希查找_第82张图片

注意,上面代码中是将每个字符串中的小写字母 a 放在 0 位,b 放在 1 位,c 放在 2 位... z 放在 25 位,所以需要 key | (1 << (c - 'a')),这相当于让 1 不停的左移对应的位数,再设置到 key 中。

【LeetCode刷题笔记】哈希查找_第83张图片

当然,这个题也可以直接使用一个长度 26 的计数数组对每个字符串进行计数,然后将该计数数组转换成整数或者二进制串作为 key。 

你可能感兴趣的:(LeetCode刷题笔记,LeetCode,哈希查找,计数数组,数据结构与算法,HashMap,HashSet,LinkedHashMap)