232. 有效的字母异位词

84 阅读7分钟

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

思路

所谓有效字母异位词,就是要看看两个字符串里面的字符出现次数是不是一样的,如果一样,就是有效的字母异位词,否则,就是无效的。简化一下问题,就是统计每个字符串里面字符的个数,然后看是不是相等。

统计字符串的个数就可以使用数组或哈希表,记录字符和的字符出现的次数。

统计过程中,使用让两个字符串里面的字符个数互相抵消,最后看是不是都是 0, 如果都是 0 的话,说明是有效字母异位词。

就跟打地鼠游戏一样,s 字符串中的每个字符出现的次数就是地鼠的出现次数,而 t 字符串中的字符则是锤子的击打次数。只有当锤子的击打次数和地鼠的出现次数完全匹配时,才能击中所有地鼠,即两个字符串是有效的字母异位词。

经过这样的抵消操作,如果最终所有字符的出现次数都抵消为 0,那么说明两个字符串是有效的字母异位词。

方法一:排序

流程图

图解

242. 有效的字母异位词(方法一).gif

代码实现

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

但是这个方法也有需要注意的地方:

  1. 题目明确说明了“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() 来确保能获取到完整的字符编码。
  1. 题目还有个进阶问题:“如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?”这就需要用到我们下文中提到的方法三了。

流程图

图解

242. 有效的字母异位词(方法二).gif

代码实现

/**
 * @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)即可。
    • st 的长度一样,当然,也可以在后面 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' 获取字符映射到数组的索引。例如 ias.charAt(a)a的 ASCII 值是 97,97-97 得到 0, 即计算 a 字符在 table 中的索引为 0。我们使用这个技巧来把字母 az 映射到数组的 0 到 25 的索引,这样我们就可以存储和跟踪每个字母在字符串中出现的次数了。

复杂度分析

  • 时间复杂度:O(n),n 为字符串 s 的长度。
  • 空间复杂度:O(1),数组的长度固定为 26,是个常数值。因此,数组所需的空间不取决于字符串的长度。

方法三:哈希表

哈希表方法是一种高效的解决方案,哈希表记录每个字符的出现次数。

流程图

图解

242. 有效的字母异位词(方法三).gif

代码实现

var isAnagram = function(s, t) {
    if (s.length !== t.lengthreturn 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 个字母,方法三则更为通用。