今天来拆解一道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步,逻辑非常清晰:
-
统计:用Map记录magazine中每个字符出现的次数;
-
验证:遍历ransomNote的每个字符,检查Map中是否有足够的计数(有且计数>0);
-
消耗:每使用一个字符,就将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;
};
优化点详解
-
边界条件优化(低成本高收益):
如果ransomNote的长度大于magazine,说明杂志的字符总数都不够,直接返回false,避免后续无效的遍历操作。
-
数组替代Map(核心优化):
用charCodeAt(0)获取字符的ASCII码,减去'a'的ASCII码(baseCode),得到0-25的下标,对应a-z。数组的每个元素,就是对应字符的出现次数。
-
简化判断逻辑:
原代码中"!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 优化技巧
-
边界条件优先判断:提前排除不可能的情况,减少无效操作;
-
选择合适的数据结构:根据字符范围选择Map或数组,权衡通用性和效率;
-
减少重复操作:比如缓存Map查询结果、合并判断逻辑,提升代码运行效率。
4.3 复杂度对比(清晰直观)
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 初始Map解法 | O(m + n) | O(k) | 任意字符范围 |
| 优化数组解法 | O(m + n) | O(1) | 字符范围固定(如小写字母) |
| 通用Map优化版 | O(m + n) | O(k) | 任意字符范围(效率优于初始Map版) |
五、最后想说的话
LeetCode的简单题,看似"没必要优化",但正是这些题目,能帮我们夯实基础、培养优化思维。比如这道题,从Map到数组的优化,看似微小,却能让我们理解「数据结构选择」对代码效率的影响,也能记住"边界条件优先"这种低成本高收益的解题技巧。