题目描述
思路解析
一句话答案:LeetCode 771 宝石与石头:把 jewels 的字符装进哈希集合,再扫一遍 stones 数有几块在集合里,集合让「是不是宝石」的判断降到常数时间,总共 O(m+n)、空间 O(m)。
石头堆里数宝石,数的是哪些石头
jewels 里每个字符代表一种宝石,且种类互不相同;stones 里每个字符是你手上的一块石头。要数的是 stones 里有多少块,其种类正好也出现在 jewels 里。大小写算两种,「a」和「A」是不同的石头。题面给 jewels="aA"、stones="aAAbbbb",输出 3——a、A、A 是宝石,四个 b 不是;再给 jewels="z"、stones="ZZ",输出 0,大写 Z 和小写 z 不算一种。
每块石头都去宝石串里翻一遍,会慢成什么样
把每块石头拿去 jewels 里逐个字符比对,是能得出答案的。可 stones 有 n 块,jewels 有 m 个字符,最坏情况每块石头都要把 m 个宝石比到底,合起来 O(m×n) 次比对,一层套一层的双重循环。两个串都上万时,这就是上亿次比较,吃亏在同一个问题反复问:这块石头到底是不是宝石。
把宝石装进集合,查询为什么变成一步
既然瓶颈是「这块石头是不是宝石」被反复追问,那就让每次追问都便宜。先把 jewels 的字符一次性放进一个哈希集合,也就是 set。它底层靠散列定位,判断一个元素在不在里面是 O(1),不随集合变大而变慢。集合建好后,每块石头只要问一句「在不在集合里」,一步就有答案,不必再逐个比。建集合扫一遍 jewels 是 O(m),之后扫 stones 每块 O(1),两段线性叠加就是 O(m+n)。
题面那串 aAAbbbb,逐块数下来是几
先把 jewels="aA" 装进集合,里面只有「a」「A」两种。再从头扫 stones="aAAbbbb":第一块 a 在集合里,计数到 1;第二块 A 在,计数到 2;第三块 A 在,计数到 3;接下来四个 b 都不在集合里,计数停在 3,输出 3。换第二个例子,jewels="z" 的集合只有小写「z」,stones="ZZ" 两块都是大写 Z,集合里查不到,一次都没命中,输出 0。
大小写混作一团,z 就把 Z 认成自己人
最容易放反的是方向:该进集合的是种类唯一的 jewels,要计数的是 stones;若反过来把 stones 塞进集合再拿 jewels 去查,数出来的就不是石头块数了。大小写是道硬约束,「z」和「Z」是两个字符,只要照原字符建集合、照原字符查询,它们天生分开,怕的是有人先把两串统一转小写,那 Z 就被当成 z 多算进去。
复杂度上,建集合扫 jewels 一遍 O(m)、逐块查 stones 一遍 O(n),合起来 O(m+n);集合存 jewels 的字符是 O(m) 空间,若改用一个长度 128 的数组当集合(字符 ASCII 不超过 128),空间就固定成常数 O(1)。还有两种边界值得先过脑子:stones 每块都是宝石时,答案等于 stones 的长度;jewels 和 stones 毫无交集时,答案是 0。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住口诀:宝石进集合,石头来查询,命中就加一。下面每一帧都在套它。
先把宝石种类登记进集合。现在读到 jewels[0] = "a",下一帧把它放进去。
"a" 进集合了。集合记的就是「哪些字符算宝石」,以后查一个字符在不在里面非常快。
先把宝石种类登记进集合。现在读到 jewels[1] = "A",下一帧把它放进去。
"A" 进集合了。集合记的就是「哪些字符算宝石」,以后查一个字符在不在里面非常快。
先把宝石种类登记进集合。现在读到 jewels[2] = "b",下一帧把它放进去。
"b" 进集合了。集合记的就是「哪些字符算宝石」,以后查一个字符在不在里面非常快。
宝石集合全部建好了,里面是 "a"、"A"、"b" 三种。接下来从头扫 stones,每块石头都来集合里查一下。
扫到 stones[0] = "A",拿它去宝石集合里问一句:我是宝石吗?
"A" 确实在集合里,是宝石!这块石头标绿,计数加到 1。
扫到 stones[1] = "a",拿它去宝石集合里问一句:我是宝石吗?
"a" 确实在集合里,是宝石!这块石头标绿,计数加到 2。
扫到 stones[2] = "b",拿它去宝石集合里问一句:我是宝石吗?
"b" 确实在集合里,是宝石!这块石头标绿,计数加到 3。
扫到 stones[3] = "C",拿它去宝石集合里问一句:我是宝石吗?
"C" 不在集合里,这块石头不是宝石(注意大小写),标灰跳过,计数还是 3。
扫到 stones[4] = "b",拿它去宝石集合里问一句:我是宝石吗?
"b" 确实在集合里,是宝石!这块石头标绿,计数加到 4。
扫到 stones[5] = "a",拿它去宝石集合里问一句:我是宝石吗?
"a" 确实在集合里,是宝石!这块石头标绿,计数加到 5。
扫到 stones[6] = "Z",拿它去宝石集合里问一句:我是宝石吗?
"Z" 不在集合里,这块石头不是宝石(注意大小写),标灰跳过,计数还是 5。
stones 全部扫完,绿色那 5 块是宝石,灰色的 "C" 和 "Z" 不在集合里。答案就是 5。
边界先想清:大小写不同、全命中、无交集三种情形。
两个高频追问:数组当集合更省空间,套路可迁移到一类「属于某集合」的题。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def numJewelsInStones(self, jewels: str, stones: str) -> int: s = set(jewels) return sum(c in s for c in stones)复杂度
- 时间:O(m + n),m 建集合扫 jewels,n 扫 stones,各一遍
- 空间:O(m),集合存 jewels 的字符;数组版固定 128 即 O(1)
易错点
面试追问把动画讲成自己的话
追问不用哈希集合,能不能更省空间?
追问这题的套路还能用在哪些题上?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
在长度 2N 的数组中找出重复 N 次的元素
LeetCode 961 · 简单 · 沿着 数组套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题