LeetCode 451 根据字符出现频率排序:python3 题解
题目链接:451. 根据字符出现频率排序
目录
1. 题目含义
这道题要求我们将一个字符串中的字符重新排列。排列的规则是:出现次数越多的字符,越要排在前面。
关键点:
- 频率降序:比如 'e' 出现 2 次,'t' 出现 1 次,那么 'e' 必须在 't' 前面。
- 相同字符在一起:题目隐含要求相同的字符必须连续排列。例如 "cccaaa" 是合法的,但 "cacaca" 是非法的,因为虽然频率对了,但字符被打散了。
- 区分大小写:'A' 和 'a' 是不同的字符。
- 任意顺序:如果两个字符频率相同(比如 'c' 和 'a' 都出现 3 次),它们之间的先后顺序无所谓,"cccaaa" 和 "aaaccc" 都是对的。
2. 核心解题思路
无论使用哪种具体算法,这道题的逻辑都可以拆解为三个标准步骤:
- 统计 (Count):遍历字符串,计算每个字符出现了多少次。
- 排序 (Sort):根据统计出来的次数,对字符进行从大到小的排序。
- 构建 (Build):按照排序后的顺序,将字符重复相应的次数,拼成新的字符串。
3. 解法讨论
我们将讨论三种常见的解法,从最 Pythonic 的写法到算法优化的写法。
方法一:哈希表 + 排序
这是最直观、代码最简洁的方法,也是最 Pythonic 的写法。
- 思路:
- 使用哈希表(在 Python 中是字典
dict或collections.Counter)统计每个字符的频率。 - 将哈希表中的内容转换为列表,例如
[('e', 2), ('t', 1), ('r', 1)]。 - 使用排序函数,根据频率(元组的第二个元素)进行降序排序。
- 遍历排序后的列表,将
字符 * 频率拼接到结果中。
- 使用哈希表(在 Python 中是字典
- 时间复杂度:\(O(N + K \log K)\)。其中 \(N\) 是字符串长度(统计耗时),\(K\) 是不同字符的个数(排序耗时)。由于字符集通常很小(例如只有字母和数字,\(K \le 62\)),这部分开销极小,整体接近 \(O(N)\)。
- 空间复杂度:\(O(K)\),用于存储字符频率。
方法二:桶排序 (Bucket Sort)【⭐】
如果追求理论上的最优时间复杂度,或者字符集非常大,可以使用桶排序。
- 思路:
- 统计频率。
- 创建一个数组(桶),数组的下标代表“频率”。例如
bucket[3]存储所有出现 3 次的字符。 - 因为频率最大也就是字符串长度 \(N\),所以桶的大小为 \(N+1\)。
- 从后往前遍历桶(从频率 \(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
代码细节解释:
import collections: 这是 Python 标准库,Counter是处理计数问题最强大的工具,能减少很多手动写字典判断的代码。sorted(..., key=...): 这是 Python 排序的核心。我们不是直接对字符串排序,而是对(字符,频率)这个组合进行排序,规则是看频率。char * count: Python 支持字符串乘法,'e' * 2直接变成'ee',这比写循环追加字符要快且易读。''.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\) 为不同字符的个数。

浙公网安备 33010602011771号