在LeetCode做简单题的第5天

前言

时隔快一周再次打开LeetCode做题,今天是一道字符串简单题——有效的字母异位词。我保留了思路过程、编码习惯上的错误纠正,接着又学习了 collections.Counter ,Python内置模块的一个工具,最后弄懂了基于 ord() 的数组最优解法,因为休息了很长时间,再做题时动力较大,我还用豆包顺着题目做了变种拓展,今日整理下来方便以后看看。

一、题目

给定两个字符串 s 和 t,编写一个函数来判断 t 是否是 s 的字母异位词。
字母异位词的含义:两个字符串的字母组成完全相同,只是字母排列顺序不一样。
题目原始限制:字符串只包含小写英文字母。
示例:
输入 s = "anagram", t = "nagaram",输出 True
输入 s = "rat", t = "car",输出 False

二、我的思路过程:字典计数

新建字典,遍历s统计每个字母出现次数;再遍历t,每遇到一个字母就把字典里对应的计数减一,如果减到0就删除这个键。如果t里出现字典不存在的字母,直接返回False。遍历结束后字典为空,说明全部抵消,返回True。

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        dict_s = {}
        for i in s:
            try: dict_s[i] += 1
            except KeyError: dict_s[i] = 1
        for j in t:
            try: 
                dict_s[j] -= 1
                if dict_s[j] == 0:
                    del dict_s[j]
            except KeyError: 
                return False
        if dict_s == {}:
            return True
        return False

代码可以通过测试用例,但必须优化的点有:

  1. 使用 try-except 处理正常的键查找逻辑。异常捕获是用来处理意外错误的,不适合做常规判断,可读性差,开销也更大;可以改用 dict.get() 优雅实现计数。
  2. 没有提前判断两个字符串长度。异位词的前提一定是长度相等,长度不一样可以直接返回False,省去后面循环的开销。
  3. 计数归零就删除键不是必要操作,会增加字典删改的额外开销。

优化后的字典版本:

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        count = {}    # 不要用dict关键字起名字,改成count表示这是一个计数的字典
        for char in s:
# 字典内置方法.get语法:.get(key, default),返回对应value或default,源自菜鸟教程
            count[char] = count.get(char, 0) + 1
        for char in t:
            if char not in count:
                return False
            count[char] -= 1
            if count[char] == 0:
                del count[char]
        return count == {}

三、一种使用内置模块的简洁写法:collections.Counter

Counter 可以直接统计字符串中每个字符的频次,两个Counter对象直接比较即可。

from collections import Counter
class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        return Counter(s) == Counter(t)

⚠️ 注意:Counter本质上还是哈希字典,底层依旧存在哈希计算、哈希冲突的开销。刷题写它很省事,但不是最优解法。

四、最优解法:利用固定大小的数组计数

这是本题在给定“仅小写字母”条件下的最优方案,也是今天我理解的重点。
ord(c) 的作用:返回字符对应的ASCII码。(扩展:相反作用的函数chr())
因为ord('a')=97、ord('z')=122,所以 ord(c)-ord('a') 可以把 a~z 映射成下标 0~25,我们开一个长度固定为26的数组,每个下标对应一个字母,用来存频次。
数组是直接下标访问,没有哈希计算的消耗,访问速度更快,空间复杂度是常数O(26),且不会随输入字符串的变大而变大。

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        arr = [0] * 26
        for c in s:
# a到z的ASCll码是连续的,对应arr连续的26的下标
            arr[ord(c) - ord('a')] += 1
        for c in t:
            arr[ord(c) - ord('a')] -= 1
        for num in arr:
            if num != 0:
                return False
        return True

拓展变种:顺着思路举一反三

下面通过修改字符集,拓展和理解这个数组思路:

  1. 变种1:包含大小写字母,不区分大小写。全部转为小写,依旧使用长度26的数组。
  2. 变种2:区分大小写,A-Z和a-z是不同字符。一共52个字符,数组长度52。A-Z映射0到25,a-z映射26到51。
  3. 变种3:区分大小写 + 数字0~9。一共62个字符,数组长度62,数字映射到52到61。

ASCll码不连续的时候,用if判断句改一改下标的计算公式即可:
if c.isupper():
(缩进)idx = ord(c) - ord('A')
elif c.islower():
(缩进)idx = 26 + ord(c) - ord('a')
elif c.isdigit():
(缩进)idx = 52 + ord(c) - ord('0')
else:

总结:当字符集合固定、种类很少的时候(如本题),优先利用ord()、固定数组大小;当字符不确定、存在大量符号/Unicode字符,再改用哈希(字典/Counter)。

五、Counter工具简单扩展

Counter是Python内置模块collections中的工具之一,可以方便快捷地计数。
主要功能:接收一个可迭代对象(字符串、列表、元组),统计里面每个元素出现次数,存成 {元素:次数} 的字典。
与dict不同,读取不存在的key时并不会报错。
此外,不仅可以用dict的方法,还自带一些dict不支持的方法:

  • .most_common(n):拿到出现次数最多的 n 个元素.
  • + - 运算符:两个 Counter 可以加减计数(负次数会被自动删掉)

方法不全,仅记住第一个。

六、时间/空间复杂度

最优解——数组方法:

  • 时间复杂度:O (n)
    设 n 是两个字符串的长度(长度相等)
    遍历 s:O (n),遍历 t:O (n),最后遍历长度固定 26 的数组:O (1)(常数,和输入规模无关)
    总和:O (n + n + 26) = O(n)
  • 空间复杂度:O (1)(常数空间)
    数组永远固定 26 个元素,不管输入字符串多长,占用空间不变,属于常数空间。

最短解——Counter工具:

  • 时间复杂度:O (n)
    Counter 内部也是遍历字符串统计,两个 Counter 比较也是逐个对比 key,整体依然是 O (n)。
    ✅ 渐进复杂度和数组版本一样
    ⚠️ 但是常数更大:哈希计算、哈希冲突处理、哈希表扩容、字典对象本身开销,每一步都有额外工作。
    也就是:同是 O (n),数组跑起来更快。
  • 空间复杂度:O (k)
    k 是字符串中不同字符的数量。本题最多 26 个小写字母,最坏是 O (26),也就是这个场景下也可以说成 O (1);
    但它的本质是随字符种类变化的哈希表:如果字符集很大(比如大量 Unicode 字符),k 会跟着输入变大,这时空间就不再是常数。
    对比数组:数组是强制固定 26,绝对常数。

七、今日总结

  1. 异常捕获不要拿来做常规判断,dict.get() 是字典计数更好的选择;
  2. 异位词第一步先判断长度,这是很容易漏掉的优化;
  3. Counter底层是哈希表,有性能开销;
  4. ord() 可以把字符映射成数组下标,固定字符集场景下数组是最优解;
  5. 当字符包含大小写、数字,还有 !@#$%^&* 这类零散符号时,需要手动维护一张字符到下标的映射表,if 判断会变得非常冗长,后续新增符号还要修改映射。这种场景下,使用 Counter 是更合适的选择。
  6. Counter 不需要关心字符的 ASCII 是否连续,任意字符都可以直接统计频次。
  7. Counter 的特点:读取不存在的键时返回 0 而不是抛出 KeyError,支持 most_common、加减计数等便捷接口。
  8. Counter 的时间复杂度是 O (n),与数组,只是相比固定数组存在哈希计算的额外常数开销。

5到8点总结出一条更精准的选型标准:
字符集合固定、ASCII 连续 → 数组(最优性能)
字符种类杂乱、ASCII 离散、不确定 → 哈希表 / Counter(优先开发效率)

posted @ 2026-10-30 14:45  绝望蘑菇头  阅读(3)  评论(0)    收藏  举报