力扣解题-383. 赎金信

5 阅读6分钟

力扣解题-383. 赎金信

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 中的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

提示:

1 <= ransomNote.length, magazine.length <= 10⁵

ransomNote 和 magazine 由小写英文字母组成

Related Topics

哈希表、字符串、计数


第一次解答

解题思路

核心方法:哈希表统计 + 逐字符校验,先统计ransomNote中每个字符的需求数量,再遍历magazine逐个扣减计数,逻辑直观但嵌套遍历导致性能低效。

核心逻辑拆解

判断赎金信能否构造的核心是“字符数量匹配”:

  1. 边界预判:若magazine长度小于ransomNote,直接返回false(字符数量不够,无法构造);
  2. 统计需求:用HashMap统计ransomNote中每个字符需要的数量(比如ransomNote="aa",则map中a:2);
  3. 校验供给:遍历HashMap中的每个字符,再遍历magazine统计该字符的出现次数,扣减需求数量;
  4. 结果判断:若某字符扣减后仍有剩余需求(count>0),返回false;全部字符满足则返回true
性能损耗分析
  • 时间复杂度:O(m×n)(m为ransomNote的不同字符数,n为magazine长度),最坏情况需遍历magazine多次;
  • 空间复杂度:O(k)(k为ransomNote的不同字符数,最多26);
  • 核心损耗点:
    1. 嵌套遍历:对每个字符都遍历一次magazine,重复扫描导致时间开销剧增;
    2. 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更高效:

  1. 边界预判:同第一次解答,magazine长度不足直接返回false
  2. 统计供给:遍历magazine,用数组count统计每个字符的出现次数(count[c-'a']++);
  3. 校验需求:遍历ransomNote,对每个字符的计数减1(count[c-'a']--);
  4. 提前终止:若某字符计数减为负数,说明magazine中该字符数量不足,直接返回false
  5. 结果返回:遍历完ransomNote未触发提前终止,说明所有字符数量满足,返回true
具体步骤(以ransomNote="aa",magazine="ab"为例)
  1. 统计magazine:count['a'-'a']=1,count['b'-'a']=1,其余为0;
  2. 校验ransomNote第一个'a':count[0]-- → 0;
  3. 校验ransomNote第二个'a':count[0]-- → -1 → 提前返回false。
性能优势
  • 时间复杂度:O(n+m)(仅两次线性遍历,n为magazine长度,m为ransomNote长度),耗时1ms击败100%用户;
  • 空间复杂度:O(1)(数组长度固定为26,与输入规模无关,属于常量级空间);
  • 核心优化点:
    1. 数组替代HashMap:避免哈希表的哈希计算、装箱拆箱等开销,访问速度接近原生变量;
    2. 单次遍历统计:仅遍历magazine一次,而非按字符多次遍历;
    3. 提前终止:发现字符不足时立即返回,减少无效遍历;
  • 内存表现: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)(排序的栈空间+字符数组);
  • 适用场景:若禁止使用额外数组(仅允许常量空间),该方法是备选方案,但工程中优先选择数组计数法。

总结

  1. 哈希表法(第一次解答):逻辑直观但嵌套遍历+哈希表开销导致性能差,仅适合理解核心思路;
  2. 数组计数法(第二次解答):最优解,O(n+m)时间+O(1)空间,利用字母特性优化存储,性能拉满;
  3. 字符数组优化版:工程级最优,减少charAt()开销,超大数据量下更友好;
  4. 排序双指针法:拓展思路,无额外计数空间但时间复杂度高,适合特殊限制场景;
  5. 关键优化技巧:
    • 字符类计数优先用固定长度数组(如26/128)替代HashMap,避免哈希开销;
    • 提前预判边界条件(长度不足直接返回),减少无效计算;
    • 大数据量下优先将字符串转为字符数组遍历,减少方法调用开销。