宝石与石头 图解题解
这道题到底在问什么
- 输入
- jewels="aA", stones="aAAbbbb"
- 输出
- 3 (a、A、A 是宝石,4 个 b 不是)
- 输入
- jewels="z", stones="ZZ"
- 输出
- 0 (大写 Z 和小写 z 不是一种)
最优解:为什么这么做
一句话答案: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。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住口诀:宝石进集合,石头来查询,命中就加一。下面每一帧都在套它。
- 4先把宝石种类登记进集合。现在读到 jewels[0] = "a",下一帧把它放进去。
- 5"a" 进集合了。集合记的就是「哪些字符算宝石」,以后查一个字符在不在里面非常快。
- 6先把宝石种类登记进集合。现在读到 jewels[1] = "A",下一帧把它放进去。
- 7"A" 进集合了。集合记的就是「哪些字符算宝石」,以后查一个字符在不在里面非常快。
- 8先把宝石种类登记进集合。现在读到 jewels[2] = "b",下一帧把它放进去。
- 9"b" 进集合了。集合记的就是「哪些字符算宝石」,以后查一个字符在不在里面非常快。
- 10宝石集合全部建好了,里面是 "a"、"A"、"b" 三种。接下来从头扫 stones,每块石头都来集合里查一下。
- 11扫到 stones[0] = "A",拿它去宝石集合里问一句:我是宝石吗?
- 12"A" 确实在集合里,是宝石!这块石头标绿,计数加到 1。
- 13扫到 stones[1] = "a",拿它去宝石集合里问一句:我是宝石吗?
- 14"a" 确实在集合里,是宝石!这块石头标绿,计数加到 2。
- 15扫到 stones[2] = "b",拿它去宝石集合里问一句:我是宝石吗?
- 16"b" 确实在集合里,是宝石!这块石头标绿,计数加到 3。
- 17扫到 stones[3] = "C",拿它去宝石集合里问一句:我是宝石吗?
- 18"C" 不在集合里,这块石头不是宝石(注意大小写),标灰跳过,计数还是 3。
- 19扫到 stones[4] = "b",拿它去宝石集合里问一句:我是宝石吗?
- 20"b" 确实在集合里,是宝石!这块石头标绿,计数加到 4。
- 21扫到 stones[5] = "a",拿它去宝石集合里问一句:我是宝石吗?
- 22"a" 确实在集合里,是宝石!这块石头标绿,计数加到 5。
- 23扫到 stones[6] = "Z",拿它去宝石集合里问一句:我是宝石吗?
- 24"Z" 不在集合里,这块石头不是宝石(注意大小写),标灰跳过,计数还是 5。
- 25stones 全部扫完,绿色那 5 块是宝石,灰色的 "C" 和 "Z" 不在集合里。答案就是 5。
⚠️ 容易写错的地方
✗ 错:每块石头都去 jewels 里逐个比对
✓ 对:先把 jewels 放集合,再 O(1) 查询
逐个比对是 O(m×n) 的双重循环,集合查询把它压成 O(m+n)
✗ 错:忽略大小写,把 "a" 和 "A" 当一种
✓ 对:区分大小写,两者是不同的种类
题目明确大小写不同,"z" 和 "Z" 不能算配对
✗ 错:把 stones 放进集合再查 jewels
✓ 对:把唯一的 jewels 放集合,扫 stones 计数
jewels 字符唯一、适合做集合;要统计的是 stones 里有几块命中
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from 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)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int numJewelsInStones(string jewels, string stones) {
int s[128] = {0};
for (char c : jewels) {
s[c] = 1;
}
int ans = 0;
for (char c : stones) {
ans += s[c];
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int numJewelsInStones(String jewels, String stones) {
int[] s = new int[128];
for (char c : jewels.toCharArray()) {
s[c] = 1;
}
int ans = 0;
for (char c : stones.toCharArray()) {
ans += s[c];
}
return ans;
}
}复杂度
时间
O(m + n)
m 建集合扫 jewels,n 扫 stones,各一遍
空间
O(m)
集合存 jewels 的字符;数组版固定 128 即 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 宝石与石头 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不用哈希集合,能不能更省空间?+
能。字符的 ASCII 码不超过 128,开一个长度 128 的数组当集合:jewels 的字符对应位置标 1,查询时看那个位置是不是 1,同样是 O(1),而空间固定成 128、相当于常数。本题参考代码的 C++、Java 版正是这么写的。
为什么一直强调区分大小写?+
题目写死了「a」和「A」是两种石头。放进集合时大小写字符本就是不同的键,只要照原字符建集合、照原字符查询,就自然分开了;会出岔子的是有人图省事先把两串都转成小写再比,那「z」就会把「Z」当成同一种,凭空多数出宝石。
这套「装集合再查询」还能用在哪些题?+
凡是「判断一批元素是否属于某个固定集合」的问题都能套:统计两个字符串有多少公共字符、检查一个数组的元素是否都在白名单里,核心都是把一边做成集合、另一边逐个扫描查询。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 宝石与石头 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。