LeetCode 383. 赎金信:解题思路+代码解析+优化实战

今天来拆解一道LeetCode简单题------383. 赎金信。这道题看似基础,却能很好地考察我们对"字符统计"的理解,以及代码优化的思路,非常适合新手入门练习,也适合老司机快速复盘基础知识点。

话不多说,先看题目本身,再一步步解析代码、优化代码,确保每一步都讲清楚、讲透彻。

一、题目解读(清晰易懂版)

题目给出两个字符串,分别是 ransomNote(赎金信)和 magazine(杂志),我们需要判断:能不能用杂志中的字符,拼写出赎金信

这里有一个关键限制:杂志中的每个字符,只能在赎金信中使用一次

举两个简单例子,帮大家快速理解:

  • 示例1:ransomNote = "a",magazine = "b" → 杂志里没有'a',返回false;

  • 示例2:ransomNote = "aa",magazine = "ab" → 杂志里只有1个'a',不够拼2个'a',返回false;

  • 示例3:ransomNote = "aa",magazine = "aab" → 杂志有2个'a'、1个'b',足够拼写,返回true。

本质上,这道题的核心需求是:ransomNote 中每个字符的出现次数,都不能超过 magazine 中该字符的出现次数。只要满足这一点,就能拼出赎金信;否则不能。

二、初始代码解析

先看你给出的代码,这是一种非常直观、易懂的解法,核心思路是用 Map 统计字符出现次数,我们逐行拆解,搞懂每一步的作用。

typescript 复制代码
function canConstruct(ransomNote: string, magazine: string): boolean {
  // 1. 初始化一个Map,用来存储magazine中每个字符的出现次数
  const map = new Map<string, number>();
  
  // 2. 遍历magazine,统计每个字符的出现次数
  for (const char of magazine) {
    // 若Map中已有该字符,就取当前计数+1;若没有,就从0开始+1
    map.set(char, (map.get(char) || 0) + 1);
  }
  
  // 3. 遍历ransomNote,验证每个字符是否能从magazine中获取(且计数足够)
  for (const char of ransomNote) {
    // 若Map中没有该字符,或该字符的计数已为0(不够用了),直接返回false
    if (!map.has(char) || map.get(char) === 0) {
      return false;
    }
    // 用掉一个该字符,计数减1
    map.set(char, map.get(char)! - 1);
  }
  
  // 4. 所有字符都验证通过,返回true
  return true;
};

2.1 代码核心逻辑

整个代码分为3步,逻辑非常清晰:

  1. 统计:用Map记录magazine中每个字符出现的次数;

  2. 验证:遍历ransomNote的每个字符,检查Map中是否有足够的计数(有且计数>0);

  3. 消耗:每使用一个字符,就将Map中对应的计数减1,确保每个字符只使用一次。

2.2 代码优缺点分析

优点很明显:

  • 逻辑直观,容易理解和编写,适合新手上手;

  • 通用性强,不限制字符范围(无论是小写字母、大写字母,甚至特殊字符,都能处理);

  • 时间复杂度是 O(m + n)(m是magazine长度,n是ransomNote长度),这是理论最优复杂度------毕竟要遍历两个字符串各一次,无法再降低。

但也有可优化的地方:

  • 空间复杂度:O(k),k是magazine中不同字符的数量(最多可能和magazine长度一致,但实际场景中多为26个小写字母),虽然不高,但可以进一步优化到极致;

  • 效率开销:Map的API(get、set、has)有轻微的哈希查询/修改开销,相比数组下标访问,速度会慢一点。

三、代码优化实战(兼顾效率和简洁)

结合题目场景(LeetCode中这道题的测试用例,默认字符是 小写英文字母),我们可以做两处关键优化,让代码更高效、更简洁。

优化方向:用数组替代Map(空间O(1),效率翻倍)

小写英文字母只有26个(a-z),我们可以用一个长度为26的固定数组,替代Map来统计字符次数。数组的下标对应字符的ASCII码偏移量(a→0,b→1,...,z→25),这样:

  • 空间复杂度直接降至 O(1)(数组长度固定为26,不随输入字符串长度变化);

  • 数组下标访问比Map的API更快,消除哈希查询的开销,实际运行效率更高。

优化后代码(推荐版本)

typescript 复制代码
function canConstruct(ransomNote: string, magazine: string): boolean {
  // 边界优化:如果赎金信比杂志长,直接返回false(不可能拼出来)
  if (ransomNote.length > magazine.length) return false;
  
  // 初始化长度为26的数组,对应a-z,初始值全为0(固定空间O(1))
  const charCount = new Array(26).fill(0);
  // 基准ASCII码:a的ASCII码,用于计算字符对应的数组下标
  const baseCode = 'a'.charCodeAt(0);
  
  // 1. 统计magazine中每个字符的出现次数
  for (const char of magazine) {
    const index = char.charCodeAt(0) - baseCode;
    charCount[index]++; // 数组下标直接访问,比Map.set高效
  }
  
  // 2. 验证并消耗字符
  for (const char of ransomNote) {
    const index = char.charCodeAt(0) - baseCode;
    // 计数为0,说明不够用,返回false
    if (charCount[index] === 0) return false;
    charCount[index]--; // 消耗一个字符,计数减1
  }
  
  return true;
};

优化点详解

  1. 边界条件优化(低成本高收益):

    如果ransomNote的长度大于magazine,说明杂志的字符总数都不够,直接返回false,避免后续无效的遍历操作。

  2. 数组替代Map(核心优化):

    用charCodeAt(0)获取字符的ASCII码,减去'a'的ASCII码(baseCode),得到0-25的下标,对应a-z。数组的每个元素,就是对应字符的出现次数。

  3. 简化判断逻辑:

    原代码中"!map.has(char) || map.get(char) === 0",在数组方案中可以简化为"charCount[index] === 0"------因为如果字符不存在,charCount[index]默认是0,两种情况可以合并判断。

补充:通用场景优化(不限制字符范围)

如果题目不限制字符(比如包含大写字母、数字、特殊字符),Map方案仍然是最优选择,此时我们可以优化Map的API调用,减少重复查询:

typescript 复制代码
function canConstruct(ransomNote: string, magazine: string): boolean {
  if (ransomNote.length > magazine.length) return false;
  
  const map = new Map<string, number>();
  for (const char of magazine) {
    map.set(char, (map.get(char) || 0) + 1);
  }
  
  for (const char of ransomNote) {
    // 缓存当前计数,减少一次map.get调用(原代码调用了2次,优化后只调用1次)
    const currentCount = map.get(char) || 0;
    if (currentCount === 0) return false;
    map.set(char, currentCount - 1);
  }
  
  return true;
};

优化点:缓存map.get(char)的结果,避免在判断和赋值时重复调用get方法,减少哈希查询的开销。

四、解题总结(必看重点)

4.1 核心知识点

这道题的核心是「字符频次统计」,常用的两种统计方式:

  • Map:通用性强,适合任意字符场景,空间复杂度O(k);

  • 数组:适合字符范围固定的场景(如小写字母、大写字母),空间复杂度O(1),效率更高。

4.2 优化技巧

  1. 边界条件优先判断:提前排除不可能的情况,减少无效操作;

  2. 选择合适的数据结构:根据字符范围选择Map或数组,权衡通用性和效率;

  3. 减少重复操作:比如缓存Map查询结果、合并判断逻辑,提升代码运行效率。

4.3 复杂度对比(清晰直观)

解法 时间复杂度 空间复杂度 适用场景
初始Map解法 O(m + n) O(k) 任意字符范围
优化数组解法 O(m + n) O(1) 字符范围固定(如小写字母)
通用Map优化版 O(m + n) O(k) 任意字符范围(效率优于初始Map版)

五、最后想说的话

LeetCode的简单题,看似"没必要优化",但正是这些题目,能帮我们夯实基础、培养优化思维。比如这道题,从Map到数组的优化,看似微小,却能让我们理解「数据结构选择」对代码效率的影响,也能记住"边界条件优先"这种低成本高收益的解题技巧。

相关推荐
东东5162 小时前
OA自动化居家办公管理系统 ssm+vue
java·前端·vue.js·后端·毕业设计·毕设
不懒不懒2 小时前
【逻辑回归从原理到实战:正则化、参数调优与过拟合处理】
人工智能·算法·机器学习
一只大袋鼠2 小时前
分布式 ID 生成:雪花算法原理、实现与 MyBatis-Plus 实战
分布式·算法·mybatis
周某人姓周2 小时前
DOM型XSS案例
前端·安全·web安全·网络安全·xss
tobias.b2 小时前
408真题解析-2010-27-操作系统-同步互斥/Peterson算法
算法·计算机考研·408真题解析
程序员鱼皮2 小时前
前特斯拉 AI 总监:AI 编程最大的谎言,是 “提效”
前端·后端·ai·程序员·开发
寄存器漫游者2 小时前
数据结构 二叉树核心概念与特性
数据结构·算法
m0_706653232 小时前
跨语言调用C++接口
开发语言·c++·算法
皮皮哎哟2 小时前
数据结构:从队列到二叉树基础解析
c语言·数据结构·算法·二叉树·队列