对整数列表做排序,绝大多数语言内置的排序都只支持单一比较规则,比如升序或降序。如果需求变成先按出现频率降序、频率相同再按数值升序,就需要把统计和自定义排序结合起来。例如输入列表 [4, 5, 6, 5, 4, 3],其中 4 和 5 各出现两次,3 和 6 各出现一次;频率为 2 的数值按升序是 4、5,频率为 1 的数值按升序是 3、6,所以最终结果是 [4, 4, 5, 5, 3, 6]。这个需求在日志分析、用户行为统计和排行榜中非常实用。

要正确实现这个排序,先要把两个比较维度拆开:第一优先级是出现次数,第二优先级才是整数的大小。很多实现出错的原因就是搞反了优先级,或者忽略了相同数字在结果中必须连续出现。下面从规则拆解开始,再给出 Python、Java 和 C++ 的具体方案。
排序规则拆解:优先级与稳定性
假设有一个整数列表 nums,统计每个数字出现的次数 freq。排序时,对任意两个数字 a 和 b,比较规则可以这样描述:如果 freq[a] 不等于 freq[b],出现次数多的排在前面;如果 freq[a] 等于 freq[b],数值小的排在前面。这个描述已经足够清晰,但在代码中还需要处理两个细节。
第一个细节是同一数字的元素在排序后必须保持连续。由于排序依据的是数字的值和频率,而同一个数字的频率相同、数值也相同,因此它们在排序结果中天然被归为一组。即使排序算法不稳定,也不会影响同一组元素的相邻性,但稳定的排序算法可以保证相同数字元素在原始列表中的相对顺序不变。对于结果值本身相同的情况,这个顺序差异并不影响输出,所以大部分场景下可以使用不稳定的排序。
第二个细节是负数和大整数的比较。例如 Python 的 key 函数可以利用元组 (-freq[x], x) 同时实现频率降序和数值升序,负数频率取反后自然变成降序。但在 Java 和 C++ 中,如果直接使用 a-b 来做数值升序比较,遇到整数极值时可能出现溢出,改变比较结果。因此更推荐使用包装好的 compare 或比较函数来处理整数差值。
Python 实现:Counter 加自定义排序键
Python 的标准库 collections 提供了 Counter,可以快速统计每个整数出现的次数。结合 sorted 函数的 key 参数,一行代码就能完成双重排序。key 需要返回一个元组,元组第一个元素控制频率降序,第二个元素控制数值升序。
from collections import Counter
def frequency_sort(nums):
freq = Counter(nums)
return sorted(nums, key=lambda x: (-freq[x], x))
print(frequency_sort([4, 5, 6, 5, 4, 3]))
这段代码的逻辑非常直接。对列表中的每个元素 x,先计算 -freq[x],频率越高这个负数越小,sorted 默认升序,所以频率高的会排在前面;当频率相同时,-freq[x] 相等,再比较 x 本身,数值小的排在前面。由于 Python 的 sorted 是稳定排序,相同数字的元素会保留原始顺序,但它们的值相同,顺序对结果没有影响。
如果列表很大,也可以先按频率分组再拼接,避免对每个元素调用 freq 查找。例如先用 Counter.most_common() 获得按频率降序排列的 (value, count) 列表,再在同一频率内按数值升序排序,最后展开。分组方式虽然代码稍长,但在元素数量非常大时能减少重复键计算,也更容易理解频率层的结构。
需要特别注意的是 Counter 的 most_common 方法已经按频率降序返回,但它不会处理同一频率内部的数值升序,因此不能直接拿来拼接。正确做法是先对 most_common 的结果按频率分组,频率相同再按数值升序,或者直接对原列表使用上面的一行排序,通常已经足够高效。
Java 实现:HashMap 与自定义 Comparator
Java 没有内置 Counter,需要先用 HashMap 统计频率,再把元素复制到 ArrayList 中调用 sort 方法,并传入自定义的 Comparator。Comparator 需要先比较两个数字的频率,频率不同时返回降序结果,频率相同时返回数值升序结果。
import java.util.*;
public class FrequencySort {
public static List<Integer> frequencySort(List<Integer> nums) {
Map<Integer, Integer> freq = new HashMap<>();
for (int num : nums) {
freq.put(num, freq.getOrDefault(num, 0) + 1);
}
List<Integer> result = new ArrayList<>(nums);
result.sort((a, b) -> {
int fa = freq.get(a);
int fb = freq.get(b);
if (fa != fb) {
return Integer.compare(fb, fa);
}
return Integer.compare(a, b);
});
return result;
}
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(4, 5, 6, 5, 4, 3);
System.out.println(frequencySort(nums));
}
}
这里没有使用 a-b 或 fb-fa 的减法,而是用了 Integer.compare 方法,目的就是避免整数溢出。比如 a 是非常大的正数而 b 是非常小的负数时,a-b 可能超过 int 范围,导致比较结果错误。Integer.compare 内部用大小判断,不会出现这种问题。
Java 的 List.sort 底层使用的排序算法对不同版本有所差异,但自定义比较器已经定义了全序关系,所以排序结果稳定可靠。如果希望保证相同频率且相同数值的元素的原始相对顺序完全不变,可以使用 Collections.sort 并依赖稳定排序,但 Java 的 List.sort 也是稳定的,能够放心使用。
另外,如果元素数量非常大,可以先使用 HashMap 统计频率,再用频率和数值构造一个可比较的对象,或者用 PriorityQueue 维护前 N 个高频元素。不过对于完整排序需求,直接使用 ArrayList 加 Comparator 是最简洁的方式,时间复杂度为 O(n log n)。
C++ 实现:unordered_map 与 stable_sort
C++ 的标准库同样提供了统计方便的工具。使用 unordered_map 计数,再配合 sort 或 stable_sort 自定义比较函数。与 Java 类似,C++ 的比较器需要返回布尔值,表示第一个参数是否应该排在第二个参数前面。频率不同时返回 freq[a] > freq[b],频率相同时返回 a < b。
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
vector<int> frequencySort(vector<int>& nums) {
unordered_map<int, int> freq;
for (int num : nums) {
freq[num]++;
}
stable_sort(nums.begin(), nums.end(), [&](int a, int b) {
if (freq[a] != freq[b]) {
return freq[a] > freq[b];
}
return a < b;
});
return nums;
}
这里使用 stable_sort 而不是 sort,主要考虑到 C++ 的 sort 不保证稳定性,虽然在本场景中相同数字的结果元素值相同,不会产生可见差异,但 stable_sort 在语义上更符合直觉。需要注意的是,stable_sort 的时间复杂度可能略高于 sort,但在通常 n 的规模下影响不大。
在 C++ 中也会遇到类似 Java 的溢出问题吗?由于比较函数直接使用 freq[a] > freq[b] 和 a < b,没有做差值计算,因此不会出现整数溢出。唯一要注意的是 unordered_map 的键和值都是 int,如果数字范围极大,需要改用 long 类型。此外,lambda 捕获了 freq 的引用,排序过程中 freq 保持不变,这是安全的。
如果元素数量特别大,可以考虑将频率统计后放到 vector<pair<int,int>> 中,再对 pair 进行排序,避免对原列表每个元素多次查询频率。不过这种优化在大多数业务场景中并不必要,先保持代码清晰更重要。
复杂度与边界条件
三种语言的实现都需要先遍历一次列表统计频率,时间复杂度为 O(n),再对 n 个元素进行排序,时间复杂度为 O(n log n)。空间复杂度方面,频率统计需要存储每个不同整数,最坏情况下所有整数都不相同,需要 O(n) 的额外空间。在实际工程中,如果已知整数范围较小,可以使用数组代替 HashMap 或 unordered_map,将空间复杂度降到 O(1)。
边界条件需要特别处理。空列表应直接返回空列表,不需要排序;只有一个元素时直接返回原列表;所有元素都相同时,排序后仍然是同一个数字重复出现,不会因为比较器逻辑产生意外。对于负数,Python 的 key 元组和 C++ 的比较函数都能正确处理,因为数值升序直接用 a < b 判断,与正负数无关。Java 的 Integer.compare(a, b) 也会正确处理负数。
另一个容易被忽略的问题是排序稳定性对结果的影响。如果需求要求相同频率且相同数值的元素保持它们在原始列表中的相对顺序,那么 Python 的 sorted、Java 的 List.sort 和 C++ 的 stable_sort 都是稳定排序,可以满足。如果使用不稳定的 sort,虽然结果数字分组仍然正确,但相同数字元素之间的相对顺序可能变化,由于这些元素值相同,通常不会影响业务逻辑。
总结来说,按频率降序、数值升序对整数列表排序,核心在于先统计频率,再把频率和数值组合为排序键或比较器。Python 的 Counter 加元组 key 最为简洁,Java 和 C++ 则用 HashMap 或 unordered_map 配合自定义比较器。只要注意优先级、整数溢出和稳定性,就能写出稳健的代码。