力扣解题-383. 赎金信
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 中的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
提示:
1 <= ransomNote.length, magazine.length <= 10⁵
ransomNote 和 magazine 由小写英文字母组成
Related Topics
哈希表、字符串、计数
第一次解答
解题思路
核心方法:哈希表统计 + 逐字符校验,先统计ransomNote中每个字符的需求数量,再遍历magazine逐个扣减计数,逻辑直观但嵌套遍历导致性能低效。
核心逻辑拆解
判断赎金信能否构造的核心是“字符数量匹配”:
- 边界预判:若
magazine长度小于ransomNote,直接返回false(字符数量不够,无法构造); - 统计需求:用HashMap统计
ransomNote中每个字符需要的数量(比如ransomNote="aa",则map中a:2); - 校验供给:遍历HashMap中的每个字符,再遍历
magazine统计该字符的出现次数,扣减需求数量; - 结果判断:若某字符扣减后仍有剩余需求(count>0),返回
false;全部字符满足则返回true。
性能损耗分析
- 时间复杂度:O(m×n)(m为
ransomNote的不同字符数,n为magazine长度),最坏情况需遍历magazine多次; - 空间复杂度:O(k)(k为
ransomNote的不同字符数,最多26); - 核心损耗点:
- 嵌套遍历:对每个字符都遍历一次
magazine,重复扫描导致时间开销剧增; - HashMap开销:哈希表的增删查操作有常数因子损耗,且自动装箱/拆箱(int→Integer)增加额外开销;
- 嵌套遍历:对每个字符都遍历一次
- 性能表现:耗时20ms仅击败5.81%用户,内存45.9MB击败34.36%用户,是嵌套遍历和哈希表的双重损耗导致。
public boolean canConstruct(String ransomNote, String magazine) {
if(magazine.length()<ransomNote.length()){
return false;
}
Map<Character,Integer> map=new HashMap<>();
for(int i=0;i<ransomNote.length();i++){
char a=ransomNote.charAt(i);
if(map.containsKey(a)){
map.put(a,map.get(a)+1);
}else{
map.put(a,1);
}
}
boolean result=true;
for(char key:map.keySet()){
int count=map.get(key);
for(int i=0;i<magazine.length();i++){
if(magazine.charAt(i)==key){
count--;
}
}
if(count>0){
result=false;
break;
}
}
return result;
}
第二次解答
解题思路
核心方法:数组计数法(最优解),利用小写字母仅26个的特性,用长度为26的数组替代HashMap统计字符数量,一次遍历统计供给、一次遍历校验需求,时间复杂度O(n+m)、空间复杂度O(1),是本题的最优解法。
核心逻辑拆解
由于字符仅包含小写字母(a-z),可用数组下标直接映射字符(c-'a'对应0-25),比HashMap更高效:
- 边界预判:同第一次解答,
magazine长度不足直接返回false; - 统计供给:遍历
magazine,用数组count统计每个字符的出现次数(count[c-'a']++); - 校验需求:遍历
ransomNote,对每个字符的计数减1(count[c-'a']--); - 提前终止:若某字符计数减为负数,说明
magazine中该字符数量不足,直接返回false; - 结果返回:遍历完
ransomNote未触发提前终止,说明所有字符数量满足,返回true。
具体步骤(以ransomNote="aa",magazine="ab"为例)
- 统计magazine:count['a'-'a']=1,count['b'-'a']=1,其余为0;
- 校验ransomNote第一个'a':count[0]-- → 0;
- 校验ransomNote第二个'a':count[0]-- → -1 → 提前返回false。
性能优势
- 时间复杂度:O(n+m)(仅两次线性遍历,n为magazine长度,m为ransomNote长度),耗时1ms击败100%用户;
- 空间复杂度:O(1)(数组长度固定为26,与输入规模无关,属于常量级空间);
- 核心优化点:
- 数组替代HashMap:避免哈希表的哈希计算、装箱拆箱等开销,访问速度接近原生变量;
- 单次遍历统计:仅遍历magazine一次,而非按字符多次遍历;
- 提前终止:发现字符不足时立即返回,减少无效遍历;
- 内存表现:45.5MB击败77.84%用户,数组的内存开销远低于HashMap。
public boolean canConstruct(String ransomNote, String magazine) {
if(magazine.length()<ransomNote.length()){
return false;
}
int [] count=new int[26];
for(int i=0;i<magazine.length();i++){
count[magazine.charAt(i)-'a']++;
}
for(int i=0;i<ransomNote.length();i++){
count[ransomNote.charAt(i)-'a']--;
if(count[ransomNote.charAt(i) - 'a'] < 0) {
return false;
}
}
return true;
}
示例解答
解题思路
解法1:字符流优化版(工程级最优)
核心方法:利用Java字符数组遍历优化,将字符串转为字符数组后遍历,减少charAt()的方法调用开销,进一步提升性能(对超大数据量更友好)。
核心优化点
String.charAt(i)每次调用都会做下标越界检查,将字符串转为char[]后直接通过下标访问,可减少重复的边界检查开销,在字符串长度≥10⁵时效果更明显。
代码实现
public boolean canConstruct(String ransomNote, String magazine) {
if (magazine.length() < ransomNote.length()) {
return false;
}
// 转为字符数组,减少charAt()的边界检查开销
char[] magChars = magazine.toCharArray();
char[] ranChars = ransomNote.toCharArray();
int[] count = new int[26];
// 统计magazine字符
for (char c : magChars) {
count[c - 'a']++;
}
// 校验ransomNote字符
for (char c : ranChars) {
int idx = c - 'a';
count[idx]--;
if (count[idx] < 0) {
return false;
}
}
return true;
}
优势说明
- 性能提升:遍历字符数组比
charAt()快约10%~20%(超大数据量下更明显); - 代码可读性:增强for循环遍历字符数组,代码更简洁;
- 工程价值:在高性能场景(如10⁶级别的字符串)下,该优化能显著降低耗时。
解法2:排序+双指针法(拓展思路)
核心方法:先排序再双指针匹配,将两个字符串排序后,用双指针逐个匹配字符,无需额外计数空间(但排序会增加时间复杂度)。
代码实现
public boolean canConstruct(String ransomNote, String magazine) {
if (magazine.length() < ransomNote.length()) {
return false;
}
// 转为字符数组并排序
char[] ran = ransomNote.toCharArray();
char[] mag = magazine.toCharArray();
Arrays.sort(ran);
Arrays.sort(mag);
int i = 0, j = 0;
// 双指针匹配
while (i < ran.length && j < mag.length) {
if (ran[i] == mag[j]) {
i++;
j++;
} else if (ran[i] > mag[j]) {
j++;
} else {
// ran[i] < mag[j],说明ransomNote有magazine没有的字符
return false;
}
}
// 所有ransomNote字符都匹配完成
return i == ran.length;
}
性能说明
- 时间复杂度:O(nlogn + mlogm)(排序的时间主导),比数组计数法慢,但无需额外计数空间;
- 空间复杂度:O(n+m)(排序的栈空间+字符数组);
- 适用场景:若禁止使用额外数组(仅允许常量空间),该方法是备选方案,但工程中优先选择数组计数法。
总结
- 哈希表法(第一次解答):逻辑直观但嵌套遍历+哈希表开销导致性能差,仅适合理解核心思路;
- 数组计数法(第二次解答):最优解,O(n+m)时间+O(1)空间,利用字母特性优化存储,性能拉满;
- 字符数组优化版:工程级最优,减少
charAt()开销,超大数据量下更友好; - 排序双指针法:拓展思路,无额外计数空间但时间复杂度高,适合特殊限制场景;
- 关键优化技巧:
- 字符类计数优先用固定长度数组(如26/128)替代HashMap,避免哈希开销;
- 提前预判边界条件(长度不足直接返回),减少无效计算;
- 大数据量下优先将字符串转为字符数组遍历,减少方法调用开销。