LeetCode 451 根据字符出现频率排序:python3 题解


题目链接:451. 根据字符出现频率排序


1. 题目含义

这道题要求我们将一个字符串中的字符重新排列。排列的规则是:出现次数越多的字符,越要排在前面

关键点:

  1. 频率降序:比如 'e' 出现 2 次,'t' 出现 1 次,那么 'e' 必须在 't' 前面。
  2. 相同字符在一起:题目隐含要求相同的字符必须连续排列。例如 "cccaaa" 是合法的,但 "cacaca" 是非法的,因为虽然频率对了,但字符被打散了。
  3. 区分大小写:'A' 和 'a' 是不同的字符。
  4. 任意顺序:如果两个字符频率相同(比如 'c' 和 'a' 都出现 3 次),它们之间的先后顺序无所谓,"cccaaa" 和 "aaaccc" 都是对的。

2. 核心解题思路

无论使用哪种具体算法,这道题的逻辑都可以拆解为三个标准步骤:

  1. 统计 (Count):遍历字符串,计算每个字符出现了多少次。
  2. 排序 (Sort):根据统计出来的次数,对字符进行从大到小的排序。
  3. 构建 (Build):按照排序后的顺序,将字符重复相应的次数,拼成新的字符串。

3. 解法讨论

我们将讨论三种常见的解法,从最 Pythonic 的写法到算法优化的写法。

方法一:哈希表 + 排序

这是最直观、代码最简洁的方法,也是最 Pythonic 的写法。

  • 思路
    1. 使用哈希表(在 Python 中是字典 dictcollections.Counter)统计每个字符的频率。
    2. 将哈希表中的内容转换为列表,例如 [('e', 2), ('t', 1), ('r', 1)]
    3. 使用排序函数,根据频率(元组的第二个元素)进行降序排序。
    4. 遍历排序后的列表,将 字符 * 频率 拼接到结果中。
  • 时间复杂度\(O(N + K \log K)\)。其中 \(N\) 是字符串长度(统计耗时),\(K\) 是不同字符的个数(排序耗时)。由于字符集通常很小(例如只有字母和数字,\(K \le 62\)),这部分开销极小,整体接近 \(O(N)\)
  • 空间复杂度\(O(K)\),用于存储字符频率。

方法二:桶排序 (Bucket Sort)【⭐】

如果追求理论上的最优时间复杂度,或者字符集非常大,可以使用桶排序。

  • 思路
    1. 统计频率。
    2. 创建一个数组(桶),数组的下标代表“频率”。例如 bucket[3] 存储所有出现 3 次的字符。
    3. 因为频率最大也就是字符串长度 \(N\),所以桶的大小为 \(N+1\)
    4. 从后往前遍历桶(从频率 \(N\) 到 1),将桶里的字符拼接到结果中。
  • 时间复杂度\(O(N)\)。统计 \(O(N)\),放入桶 \(O(K)\),生成结果 \(O(N)\)。没有排序的 \(\log\) 开销。
  • 空间复杂度\(O(N)\),需要创建长度为 \(N\) 的桶数组。

方法三:最大堆 (Priority Queue)

  • 思路:统计频率后,将 (频率,字符) 放入最大堆。每次弹出频率最大的元素。
  • 评价:时间复杂度同方法一 \(O(N + K \log K)\)。在 Python 中实现起来比直接排序稍微麻烦一点(因为 Python 的 heapq 是最小堆,需要取负数模拟最大堆),通常不如方法一简洁。

4. 代码实现

解法一:哈希表 + 排序 (Pythonic 写法)

import collections

class Solution:
    def frequencySort(self, s: str) -> str:
        # 步骤 1: 统计频率
        # collections.Counter 是 Python 专门用于计数的字典子类
        # 例如 s = "tree", counter 结果为 {'e': 2, 't': 1, 'r': 1}
        counter = collections.Counter(s)
        
        # 步骤 2: 排序
        # counter.items() 返回类似 [('e', 2), ('t', 1), ('r', 1)] 的列表
        # key=lambda x: x[1] 表示按照元组的第二个元素(即频率)进行排序
        # reverse=True 表示降序排列(频率高的在前)
        sorted_chars = sorted(counter.items(), key=lambda x: x[1], reverse=True)
        
        # 步骤 3: 构建结果字符串
        # 使用列表推导式生成片段,然后用 join 连接,比直接字符串相加效率高
        # 例如 'e' * 2 得到 "ee"
        result = ''.join(char * freq for char, freq in sorted_chars)
        
        return result

代码细节解释:

  1. import collections: 这是 Python 标准库,Counter 是处理计数问题最强大的工具,能减少很多手动写字典判断的代码。
  2. sorted(..., key=...): 这是 Python 排序的核心。我们不是直接对字符串排序,而是对 (字符,频率) 这个组合进行排序,规则是看频率。
  3. char * count: Python 支持字符串乘法,'e' * 2 直接变成 'ee',这比写循环追加字符要快且易读。
  4. ''.join(...): 在 Python 中,字符串是不可变的。如果在循环里写 res += char,每次都会创建新字符串,效率是 \(O(N^2)\)。使用 join 列表是 \(O(N)\) 的最佳实践。

解法二:桶排序 (优化时间复杂度)

import collections

class SolutionBucket:
    def frequencySort(self, s: str) -> str:
        # 步骤 1: 统计频率
        counter = collections.Counter(s)
        # 或者使用 python dict 进行统计:
        '''
        counter = {}
        for cc in s:
            if cc in counter.keys():
                counter[cc] += 1
            else:
                counter[cc] = 1
        '''
        
        # 步骤 2: 创建桶
        # 桶的索引代表频率,桶的内容是该频率下的字符列表
        # 最大频率不会超过字符串长度 len(s)
        # 所以我们需要 len(s) + 1 个桶(索引 0 到 len(s))
        buckets = [[] for _ in range(len(s) + 1)]
        
        # 将字符填入对应的桶中
        # 例如 char='e', freq=2,则放入 buckets[2]
        for char, freq in counter.items():
            buckets[freq].append(char)
            
        # 步骤 3: 从高频到低频构建字符串
        result = []
        # 从最后一个桶(最高频率)向前遍历到第 1 个桶
        # 注意:range 的结束值是不包含的,所以写到 0,实际遍历到 1
        for i in range(len(buckets) - 1, 0, -1):
            # 如果当前桶不为空(说明有字符出现次数为 i)
            if buckets[i]:
                # 遍历桶里的所有字符(可能有多个字符频率相同)
                for char in buckets[i]:
                    # 将字符重复 i 次加入结果
                    result.append(char * i)
                    
        return ''.join(result)

5. 复杂度对比与总结

特性 方法一:哈希 + 排序 方法二:桶排序
时间复杂度 \(O(N + K \log K)\) \(O(N)\)
空间复杂度 \(O(K)\) \(O(N)\)
代码复杂度 低 (需要掌握 python 高阶用法) 中 (需要手动维护桶)
适用场景 绝大多数情况,字符集较小 字符集极大或对时间极度敏感
推荐度 ⭐⭐⭐ ⭐⭐⭐⭐⭐

\(N\) 为字符串长度,\(K\) 为不同字符的个数。



posted @ 2026-03-10 16:29  MoonOut  阅读(15)  评论(0)    收藏  举报