Problem: 242.有效的字母异位词

思路
所谓有效字母异位词,就是要看看两个字符串里面的字符出现次数是不是一样的,如果一样,就是有效的字母异位词,否则,就是无效的。简化一下问题,就是统计每个字符串里面字符的个数,然后看是不是相等。
统计字符串的个数就可以使用数组或哈希表,记录字符和的字符出现的次数。
统计过程中,使用让两个字符串里面的字符个数互相抵消,最后看是不是都是 0, 如果都是 0 的话,说明是有效字母异位词。
就跟打地鼠游戏一样,s 字符串中的每个字符出现的次数就是地鼠的出现次数,而 t 字符串中的字符则是锤子的击打次数。只有当锤子的击打次数和地鼠的出现次数完全匹配时,才能击中所有地鼠,即两个字符串是有效的字母异位词。
经过这样的抵消操作,如果最终所有字符的出现次数都抵消为 0,那么说明两个字符串是有效的字母异位词。
方法一:排序
流程图

图解

代码实现
var isAnagram = function(s, t) {
return s.length === t.length && [...s].sort().join('') === [...t].sort().join('')
};
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
char[] sArr = s.toCharArray();
char[] tArr = t.toCharArray();
Arrays.sort(sArr);
Arrays.sort(tArr);
return Arrays.equals(sArr, tArr);
}
}
相关方法:
length()返回计算字符串长度toCharArray()将字符串转换成一个新的字符数组Arrays.sort(char[] a)将指定数组按数字升序排序Arrays.equals(char[] a, char[] a1)如果指定的两个字符数组彼此相等,则返回true
复杂度分析
- 时间复杂度:
O(nlogn),n 为 s 的长度,排序的时间复杂度为O(nlogn),比较两个字符串是否相等的时间复杂度为O(n),因此总体时间复杂度为O(nlogn + n) - 空间复杂度:
O(logn)。排序需要O(logn)的空间复杂度。
方法二:数组
因为字符串中只包含 26 个小写字母,因此我们可以维护一个长度为 26 的频次数组 table,然后统计每个字母出现的次数,对字符串 s 中出现的字母次数 + 1, 字符串 t 中出现的字母次数 - 1。
做完统计之后,只需要再“汇总一下”,如果每个字母的次数为 0,说明字符串 s 和 字符串 t 中的字母出现次数一致,一个加,一个减,两两抵销了。如果 26 个字母都是如此,那说明是有效的字母异词,反之,不是有效的字母异词。
长度为 26 的频次数组 table 怎么与 26 个字母一一对应呢?
我们期望的是 a-z 的字母能够一一对应到数组的索引 0-25 上,比如字母 a 对应 索引 0,其他字母依次类推。这样做的好处就是,频次数组 table 中每个位置就可以存放该字母出现的次数了,这样,等最终统计频次的时候,只需要关注数组 table 中的每个值是否为 0 即可。
那么,问题又变成了,字母和数字 0 是怎么对应上的呢?每种语言对应的方法不同,这里只介绍 JavaScript 的方法。
JavaScript 中,可以使用字符码来计算字母与数字的关系,每个字符都有一个唯一的字符码,可以通过 charCodeAt() 方法获得。
String 的 charCodeAt() 方法返回一个整数,表示给定索引处的 UTF-16 码元,其值介于 0 和 65535 之间
最常见的字符编码标准有 ASCII 码、Unicode 等,以 ASCII 为例,小写字母 a-z 的编码范围是 97 到 122。
举个🌰:
const s = 'a';
s.charCodeAt(0); // 97 A
A 处得倒了 字母 a 的编码,但我们想让它映射到数组索引 0 的位置,怎么办呢?如下代码:
const s = 'a';
const indexZero = s.charCodeAt(0) - 'a'.charCodeAt(0);
console.log(indexZero); // 0
但是这个方法也有需要注意的地方:
- 题目明确说明了“s 和 t 仅包含小写字母”,所以我们可以通过上面的方法映射数组。如果包含大写字母,这种方法就不行啦!如下代码:
const s = 'A';
s.charCodeAt(0); // 65
当然,如果只包含大写字母 A-Z 的话,也可以灵活变通,将其映射到数组的索引。如下:
const s = 'A';
const indexZero = s.charCodeAt(0) - 'A'.charCodeAt(0);
console.log(indexZero); // 0
除此之外,还可以使用 codePointAt()。
String.prototype.codePointAt()的codePointAt()方法返回一个非负整数,该整数是从给定索引开始的字符的 Unicode 码位值。请注意,索引仍然基于 UTF-16 码元,而不是 Unicode 码位。
charCodeAt() vs. codePointAt()
- 使用
charCodeAt()时,如果你处理的是普通字符(如 ASCII 字符),它可以正常工作。但对于某些特殊字符或表情符号,可能需要使用codePointAt()来获取完整的 Unicode 值。 - 因此,在处理多语言文本或特殊字符时,推荐使用
codePointAt()来确保能获取到完整的字符编码。
- 题目还有个进阶问题:“如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?”这就需要用到我们下文中提到的方法三了。
流程图

图解

代码实现
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isAnagram = function(s, t) {
if (s.length !== t.length) return false;
const table = new Array(26).fill(0); // A
for (let i = 0; i < s.length; i++) {
table[s.charCodeAt(i) - 'a'.charCodeAt(0)]++; // B
table[t.charCodeAt(i) - 'a'.charCodeAt(0)]--; // C
}
for(let i = 0; i < 26; i++) {
// D
if (table[i] != 0) {
return false;
}
}
return true;
};
在上面的代码中:
-
A 处初始化了一个大小为 26 的数组,并且用 0 填充
-
B 处统计字符串
s中每个字母出现的频次,C 处统计t中每个字母出现的频次。为什么我们放在一起统计呢?有两个原因:s中每个字母增加,t中每个字母减少,一增一减,最后统计的时候,只需要判断当前索引的值是否不等于0(或 < 0)即可。s和t的长度一样,当然,也可以在后面 D 处再统计t中的字母频次。如下文的 TypeScript 实现。
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
int[] table = new int[26];
for (int i = 0; i < s.length(); i++) {
table[s.charAt(i) - 'a']++;
table[t.charAt(i) - 'a']--;
}
for (int count : table) {
if (count != 0) {
return false;
}
}
return true;
}
}
bool isAnagram(char* s, char* t) {
int s_len = strlen(s), t_len = strlen(t);
if (s_len != t_len) {
return false;
}
int table[26];
memset(table, 0, sizeof(table));
for (int i = 0; i < s_len; i++) {
table[s[i] - 'a']++;
table[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (table[i] != 0) {
return false;
}
}
return true;
}
相关方法:
charAt(int index)返回指定索引处的字符。我们通过s.charAt(i) - 'a'获取字符映射到数组的索引。例如i为a,s.charAt(a)即a的 ASCII 值是 97,97-97得到 0, 即计算a字符在table中的索引为 0。我们使用这个技巧来把字母a到z映射到数组的 0 到 25 的索引,这样我们就可以存储和跟踪每个字母在字符串中出现的次数了。
复杂度分析
- 时间复杂度:
O(n),n 为字符串 s 的长度。 - 空间复杂度:
O(1),数组的长度固定为 26,是个常数值。因此,数组所需的空间不取决于字符串的长度。
方法三:哈希表
哈希表方法是一种高效的解决方案,哈希表记录每个字符的出现次数。
流程图

图解

代码实现
var isAnagram = function(s, t) {
if (s.length !== t.length) return false;
const table = new Map();
for (let i = 0; i < s.length; i++) {
table.set(s[i], ((table.get(s[i]) || 0) + 1));
table.set(t[i], ((table.get(t[i]) || 0) - 1));
}
for(let i of table.values()) {
if (i != 0) {
return false;
}
}
return true;
};
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
Map<Character, Integer> counter = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
char sChar = s.charAt(i); // A
counter.put(sChar, counter.getOrDefault(sChar, 0) + 1); // B
char tChar = t.charAt(i);
counter.put(tChar, counter.getOrDefault(tChar, 0) - 1);
}
for (int count : counter.values()) { // C
if (count != 0) {
return false;
}
}
return true;
}
}
以上代码中,涉及到以下方法:
- A 处,
charAt(index)用于获取字符串中指定索引位置的字符 - B 处,
getOrDefault(Object key, Object defaultValue)返回指定的key的值,如果映射不包含该键的映射,则返回defaultValue - C 处,
values()用于获取哈希表中所有值的方法
复杂度分析
- 时间复杂度:
O(n),n 是字符串的长度 - 空间复杂度:
O(n),使用了一个额外的哈希表来存储字符和对应的计数,最多 n 个元素,所以空间复杂度为(n)
总结
针对这道题来说,方法二是最优解,使用数组,且固定长度为 26 个字母的长度。但是如果不是 26 个字母,方法三则更为通用。