☰
数组元素按出现次数筛选并升序排序:哈希计数与多语言实现
2026/9/30 3:11:50 网站建设 项目流程

"统计数组中出现指定次数的元素并且升序输出"——这个需求光看标题,像是初学者练习题,但真落到代码里,牵扯出来的东西其实不少:怎么确定"指定次数"这个阈值、重复元素要不要保留、排序时数字和字母混在一起怎么办、空数组输入怎么处理,更别说不同语言里实现方式天差地别。

我这些年写 JavaScript、Python、VBA 都遇到过类似需求,统计词频、筛选重复点击的用户 ID、找埋点日志里异常上报的接口名、整理问卷多选题选项,全是这个套路:先数次数,再按次数筛,最后排个序。今天把这块掰开揉碎讲清楚,JS、Python、Java、C++、VBA 各给一套能直接抄的写法,再聊聊排序和统计里的坑。不管你是刚学数组的新手,还是写了几年业务代码的老兵,这篇都能给你省点时间。

1. 核心思路拆解:为什么"先统计、再筛选、最后排序"是唯一正确顺序

做这个需求,第一反应可能是双重循环:拿数组每个元素去跟全数组比一遍,数出有几个一样的。数组短还好,长度一上 1000,这就是 100 万次比较,页面直接卡死。正确姿势是引入一个中间容器——哈希表、字典、Map、对象都行——用一趟遍历完成计数,时间复杂度 O(n)。

1.1 "哈希计数"背后的空间换时间逻辑,新手最容易忽略

所谓哈希计数,就是建立一个"元素值 → 出现次数"的映射关系。拿 JavaScript 举例,用普通对象{}做容器,遍历数组,每遇到一个元素,就在对象里找对应的键,有就加 1,没有就初始化为 1。

const arr = [3, 1, 2, 3, 5, 1, 3]; const freq = {}; for (const item of arr) { freq[item] = (freq[item] || 0) + 1; } // freq = { 1: 2, 2: 1, 3: 3, 5: 1 }

每次循环只做一次"字典查找 + 一次加法",几十万条数据也就几十万次操作,秒级完成。代价是多了一块和数组元素数量成正比的内存。这属于典型的空间换时间,在绝大多数业务场景下都划算。

这里有个新手极容易踩的坑:JS 普通对象的键会被强制转成字符串,所以数字1和字符串"1"会被当成同一个键。真要区分类型,得换Map来存。同样的问题在 Python 里也有,字典的键必须是可哈希的,列表就不能当键。

1.2 筛选"出现指定次数"的边界条件,先想清楚再写循环

"出现指定次数"这句话其实有多个理解层次。拿题目来说,我倾向理解为:找出所有出现次数恰好等于某个数 N 的元素。但也有人要的是"大于等于 N",这直接决定了过滤条件怎么写。

我一般第一件事就是把需求翻译成逻辑表达式:

  • 恰好等于 N 次:freq[item] === N
  • 至少出现 N 次:freq[item] >= N
  • 找出所有不重复元素:freq[item] === 1
  • 找出重复元素:freq[item] > 1

还有一个更阴间的细节:如果数组里同一个值出现了 N 次,但它其实是一个重复项,最终输出时是输出一个元素还是 N 个相同元素?正常业务逻辑都是"每个唯一值输出一次",但你要是拿双重循环硬数,很容易把重复元素重复塞进结果数组。所以淘汰双重循环还有一个原因:它天然会让输出结果里带重复项,你还得二次去重。

1.3 升序排序放最后,是因为它根本不关心你的计数逻辑

为什么排序不放到前面?因为排序是一个纯"排列"操作,它不改变元素集合,只改变顺序。你先排序再统计,确实能通过相邻元素比较来计数,但排序本身比哈希计数慢(最快 O(n log n),而计数是 O(n))。而且先排序会破坏数组原始顺序,万一后面还有别的需求依赖原始顺序,那就麻烦了。

正确的流水线是:原始数组 → 哈希计数 → 按条件筛选 → 得到结果数组 → 对结果数组做升序排序 → 输出。每步只管一件事,清晰、好调试、可复用。这个思路不只在编程题里成立,在 Excel 的数据透视、SQL 的 GROUP BY 和 ORDER BY 里也是完全一样的逻辑。

2. 各语言实现盘点:JS、Python、Java、C++、VBA 一次给全

同一个需求,不同语言因为内置容器和 API 的差异,写出来的代码风格完全不同。下面这五种我都实测过,直接复制就能用。

2.1 JavaScript:对象 + filter + sort,三行搞定但要注意类型陷阱

function filterAndSort(arr, targetCount) { const freq = {}; for (const item of arr) { freq[item] = (freq[item] || 0) + 1; } return Object.keys(freq) .filter((key) => freq[key] === targetCount) .map(Number) .sort((a, b) => a - b); } const data = [5, 3, 8, 3, 1, 5, 3, 8, 9]; console.log(filterAndSort(data, 2)); // [5, 8]

这段代码里Object.keys(freq)拿到的键一律是字符串,所以.map(Number)这步不能省——除非你想让"10"排在"9"前面。排序回调(a, b) => a - b是升序的固定写法,千万别写成sort()不带参数,那样是按 Unicode 字典序排的,数字10会排在2前面,这是 JS 新手最常踩的排序坑。

如果数组里可能存在对象、NaN、undefined 这类复杂元素,freq[item] = (freq[item] || 0) + 1这套以普通对象做字典的方案就会出问题。稳妥做法换成Map:

function filterAndSortSafe(arr, targetCount) { const freq = new Map(); for (const item of arr) { freq.set(item, (freq.get(item) || 0) + 1); } const result = []; for (const [item, count] of freq) { if (count === targetCount) result.push(item); } return result.sort((a, b) => a - b); }

Map的键可以是任何类型,for (const [item, count] of freq)这种解构遍历也清晰明白。日常业务代码里,只要有"统计出现次数"需求,我建议一律用Map而不是{},省得后面数据形态变了还要回头改。

2.2 Python:Counter 计数器 + 列表推导式,目前最优雅的方案

Python 的collections.Counter就是为这种需求量身定做的。它本身是一个字典子类,直接统计好所有元素的出现次数,然后一行过滤、一行排序:

from collections import Counter def filter_and_sort(arr, target_count): freq = Counter(arr) return sorted([item for item, count in freq.items() if count == target_count]) data = [5, 3, 8, 3, 1, 5, 3, 8, 9] print(filter_and_sort(data, 2)) # [5, 8]

Counter(arr)内部用 C 语言优化过的哈希表实现,性能比手写for循环快不少。统计 100 万元素大概只要零点几秒。sorted()默认就是升序,对纯数字序列直接生效。

如果你不想引入collections,自己写也不难,本质跟 JS 一样,用 dict 做哈希:

def filter_and_sort_manual(arr, target_count): freq = {} for item in arr: freq[item] = freq.get(item, 0) + 1 result = [item for item, count in freq.items() if count == target_count] result.sort() return result

注意一个细节:Python 的元组、字符串、数字都是可哈希的,可以放进 dict 做键,但列表不行。如果你统计的对象是列表,得先转成元组再计数。

2.3 Java:HashMap + Stream 流处理,适合工程项目的写法

import java.util.*; import java.util.stream.Collectors; public class FrequencySort { public static List<Integer> filterAndSort(int[] arr, int targetCount) { Map<Integer, Integer> freq = new HashMap<>(); for (int num : arr) { freq.put(num, freq.getOrDefault(num, 0) + 1); } return freq.entrySet().stream() .filter(e -> e.getValue() == targetCount) .map(Map.Entry::getKey) .sorted() .collect(Collectors.toList()); } public static void main(String[] args) { int[] data = {5, 3, 8, 3, 1, 5, 3, 8, 9}; System.out.println(filterAndSort(data, 2)); // [5, 8] } }

Java 的HashMap跟 Python 的 dict、JS 的 Map 一个原理。getOrDefault是处理"键不存在"的惯用法。Stream写法在工程代码里很常见,链式调用清晰,但如果你是写 Android 或者老项目,也可以用传统循环 +ArrayList,效果一样。

传统写法长这样,容易读懂,适合面试时手写:

public static List<Integer> filterAndSortTraditional(int[] arr, int targetCount) { Map<Integer, Integer> freq = new HashMap<>(); for (int num : arr) { Integer count = freq.get(num); freq.put(num, count == null ? 1 : count + 1); } List<Integer> result = new ArrayList<>(); for (Map.Entry<Integer, Integer> e : freq.entrySet()) { if (e.getValue() == targetCount) { result.add(e.getKey()); } } Collections.sort(result); return result; }

2.4 C++:unordered_map + vector 排序,性能敏感场景的标杆

C++ 里能用std::unordered_map做到 O(1) 平均复杂度哈希查找,注意它不是有序的,所以最后必须sort一次。这个方案适合大数组、性能敏感的场景。

#include <iostream> #include <vector> #include <unordered_map> #include <algorithm> std::vector<int> filterAndSort(const std::vector<int>& arr, int targetCount) { std::unordered_map<int, int> freq; for (int num : arr) { freq[num]++; } std::vector<int> result; for (const auto& pair : freq) { if (pair.second == targetCount) { result.push_back(pair.first); } } std::sort(result.begin(), result.end()); return result; } int main() { std::vector<int> data = {5, 3, 8, 3, 1, 5, 3, 8, 9}; auto result = filterAndSort(data, 2); for (int num : result) { std::cout << num << " "; // 输出 5 8 } return 0; }

freq[num]++这里有个小陷阱:如果 key 不存在,operator[]会先默认初始化一个 0,然后自增成 1。这在统计场景下反而是我们想要的行为,所以可以直接写。

如果数组本身是有序的,你还可以省掉 unordered_map,直接扫描相邻元素计数,这样空间复杂度降到 O(1)。但请记住:有序是前置条件,不是所有场景都满足。

2.5 VBA:字典对象 + 集合排序,Excel 场景的另类解法

VBA 处理数组排序比较反直觉——它没有内置的数组排序函数,常规做法是把数据丢到 Excel 工作表里用 Range.Sort,或者借助ArrayList这类 .NET 对象。真正统计次数,用Scripting.Dictionary准没错。

Function FilterAndSort(arr, targetCount As Long) As Variant Dim dict As Object Set dict = CreateObject("Scripting.Dictionary") Dim i As Long For i = LBound(arr) To UBound(arr) If dict.Exists(arr(i)) Then dict(arr(i)) = dict(arr(i)) + 1 Else dict.Add arr(i), 1 End If Next i ' 筛选出现次数等于 targetCount 的键 Dim tempArr() As String Dim count As Long count = 0 For Each key In dict.Keys If dict(key) = targetCount Then ReDim Preserve tempArr(count) tempArr(count) = key count = count + 1 End If Next key ' VBA 没有原生数组排序,扔到工作表排序 If count = 0 Then FilterAndSort = Array() Exit Function End If Dim ws As Worksheet Set ws = ThisWorkbook.Sheets("排序辅助") ws.Cells.Clear ws.Range("A1").Resize(count, 1).Value = Application.Transpose(tempArr) ws.Range("A1").Resize(count, 1).Sort ws.Range("A1"), xlAscending FilterAndSort = Application.Transpose(ws.Range("A1").Resize(count, 1).Value) End Function

这个方案里我用了辅助工作表存中间结果,属于"脏但能用"的典型 VBA 风格。更彻底的办法是引入System.Collections.ArrayList,代码更简洁但要开启对应引用。VBA 里用字典要记住CreateObject("Scripting.Dictionary")这个创建方式,直接New Dictionary在多数环境里是不行的。

2.6 五种语言方案横向对比,选择困难的直接看表

语言核心容器计数复杂度排序方式适合场景
JavaScriptObject / MapO(n)sort((a,b)=>a-b)Web 前端、Node 后端
Pythoncollections.CounterO(n)sorted()数据分析、脚本、算法题
JavaHashMapO(n)Stream.sorted()工程后端、Android
C++unordered_mapO(n)std::sort高性能计算、算法竞赛
VBAScripting.DictionaryO(n)工作表排序Excel 自动化处理

3. 排序细节与边界情况:升序输出不是 sort 一下那么简单

这个题目的后半段"升序输出"看着简单,但不同类型、不同数据形态下都有隐藏问题。

3.1 数字排序的字典序陷阱,JavaScript 用户最常见

JS 的sort()默认行为是对元素做字符串转换后按 Unicode 码点排序。也就是说[10, 9, 100].sort()得到的是[10, 100, 9]。对这个题目,结果数组如果是数字,必须显式传比较器:

result.sort((a, b) => a - b);

Python 的sorted()对纯数字列表没有这个问题,它直接比较数值大小。但如果你的列表里混了字符串和数字,比如[1, '2', 3],会直接抛TypeError。所以实际操作中第一件事是确认元素的类型一致性。Java 的Collections.sort()要求元素实现Comparable接口,Integer 原生支持,但混合类型也无法编译。

C++ 的std::sort默认用<运算符,数字没问题,但如果是自定义结构体,要自己重载operator<或传 lambda。

3.2 空数组、全重复数组、边界值数组,三个极端用例必须测

我写这类函数时有个习惯:先把极端输入测一遍,再跑正常用例。这个题目至少有三种边界:

  • 空数组输入:freq是空字典,filter结果也是空,排序空数组不报错,直接返回[]。这个流程对 JS、Python、Java、C++ 都自然成立。
  • 目标次数为 0 或负数:正常逻辑里"出现 0 次"意味着元素根本不在数组里,不应该出现在结果中。所以严格来说,targetCount应该在函数入口做校验,小于 1 就直接返回空数组,避免后续无意义计算。
  • 数组长度恰等于目标次数:比如[7, 7]里找出现 2 次的元素,结果是[7];找出现 3 次的元素,结果是[]。这类用例最能验证筛选条件写没写对。

3.3 稳定性与逆序输入的考量,以及"出现次数相同元素"的二次排序

如果需求变成"先按出现次数排序,次数相同再按值升序",代码就要在排序键上做文章。Python 里可以:

# 按出现次数降序,次数相同按元素升序 sorted_items = sorted(freq.items(), key=lambda x: (-x[1], x[0]))

JS 里对应写法:

const sortedKeys = Object.keys(freq).sort((a, b) => { if (freq[b] !== freq[a]) return freq[b] - freq[a]; // 次数降序 return Number(a) - Number(b); // 值升序 });

排序稳定性在现代引擎里都是稳定的,但你要是手写了一个不稳定的快排(比如某些教科书简化版),相同次数的元素顺序可能随机变化。我建议不要过分依赖稳定性,显式把二级排序键写清楚最稳妥。

3.4 使用"排序后相邻比较"统计的替代方案,什么时候它更优

前面我主推哈希统计,但存在一个哈希不擅长的场景:元素本身不可哈希。比如数组里装的是坐标点[x, y]或嵌套对象。这时候哈希容器无能为力,替代方案是先排序再扫描:

def count_sorted(arr): arr = sorted(arr) # 先排序,让相同元素相邻 result = [] i = 0 n = len(arr) while i < n: j = i while j < n and arr[j] == arr[i]: j += 1 if j - i == target_count: # 此处 target_count 在外层定义 result.append(arr[i]) i = j return result

这个方案的时间复杂度是 O(n log n),比哈希慢一点,但好处是空间 O(1),而且对元素类型要求低,只要能比较大小就行。大数组内存紧张或者是复杂对象数组时,值得考虑。

4. 常见问题与排查技巧实录

每次写这种统计+排序的需求,我都会在生产环境里遇到一些教科书不会讲的坑,集中整理如下。

4.1 "统计词频结果不对"多半是类型或隐式转换问题

场景:用户上传 Excel 里的工号,要求统计每个人出现的次数,然后筛选出现 3 次的工号。结果发现有的工号明明出现了 3 次却不在结果里。排查后发现 Excel 读取的数字和字符串混在一起——"00123"和123数值相同,但哈希容器把它们当成两个不同的键。

解决办法:统计前统一做类型归一化,要么全部转字符串,要么全部转数字,并且注意前导零的保留。这个坑在 VBA 和 JS 里尤其常见。

4.2 "排序结果顺序诡异"检查到底是数字比较还是字典序比较

经典案例:数组[1, 2, 10, 20],期望升序[1, 2, 10, 20],实际输出[1, 10, 2, 20]。十有八九用了默认字符串排序。排查方法很简单:在排序前打印每个元素的typeof或type(),确认类型;再看排序回调有没有写。JS 里sort()不带参数就是这个结果,加(a, b) => a - b立刻正常。

4.3 "VBA 数组明明有值,字典统计却是空的"多半是数组维度问题

VBA 数组有两种:定长数组Dim arr(1 To 10)和动态数组Dim arr()配合ReDim。LBound和UBound的使用是基础中的基础,但很多人不知道Application.Transpose有长度限制——超过 65537 个元素会报错。应对大数据,别用Transpose往工作表搬,改成分段写入或者直接用ArrayList处理。

4.4 大数据量性能优化的三个方向

百万级数组的场景,我建议关注这三个方向,按性价比排序:

  • 哈希容器预分配容量:Java 的HashMap构造时传入预估大小,new HashMap<>(arr.length / 2)能减少扩容次数。Python 的 Counter 没法预分配,但可以用defaultdict(int)略快一点。
  • 避免不必要的复制:JS 里filter会生成新数组,再sort又生成一次。如果内存敏感,可以先把结果 push 进数组再原位 sort。
  • 并行化:如果数组分布在多个分片,可以先对每个分片做统计,再把各分片的统计结果合并。这正好是 MapReduce 思想的雏形,Map 阶段各算各的,Reduce 阶段汇总频次。

4.5 常见错误速查表,建议贴显示器旁边

症状可能原因解决方案
统计次数少算普通对象键类型转换导致数字和字符串串键改用 Map / 提前归一化类型
统计次数多算数组本身包含重复项,双重循环未去重改用哈希容器做唯一键统计
排序结果乱字符串字典序排序JS 传比较器,其他语言用对应数值排序 API
结果里有重复元素筛选时遍历原始数组而非唯一键集合遍历freq.keys()而不是遍历arr
大数据 VBA 报错Array 维度或 Transpose 长度限制用 ArrayList 或循环写入单元格
负数排序错位未考虑负数绝对值排序规则升序直接用数值比较器即可

4.6 面试现场如何拆解这道题,5 分钟讲出高分答案

这题经常伪装成"统计数组中出现次数最多的元素""找出出现次数超过一半的数字""数组中出现次数不低于两次的元素"等面目出现,出现在社招和校招面试里。我要是面试官,我会这么考察候选人:

第一步,问思路。候选人如果能说出"先哈希计数,再筛选,最后排序"这个三步流水线,基本分拿到。

第二步,问复杂度。要能答出时间 O(n)、空间 O(n) 以及为什么不能更优——因为至少要看一遍所有元素,O(n) 是下限。

第三步,追边界。空数组、全相同数组、目标次数大于数组长度、数组元素类型混杂,这些都是加分项。

第四步,发散的隐藏考点:如果要统计次数并保留原数组顺序输出怎么办?答案是不排序,遍历原数组,用freq[item] === N && !seen.has(item)去重输出。这个变体考察的是对"排序是否必要"的判断力。

5. 实操扩展:从"统计出现次数"到真实业务场景的迁移

学会了这道题的解法,你会发现它像一块积木,能拼进各种真实系统里。

5.1 日志分析场景:统计每个接口的调用频次并升序输出 Top N

假设后端收到一批访问日志,每行一个接口名,要找出调用次数最多的 5 个接口。核心代码就是"哈希计数 + 排序取前 N":

from collections import Counter logs = ["/api/login", "/api/order", "/api/login", "/api/user", "/api/order", "/api/login"] counter = Counter(logs) top5 = counter.most_common(5) print(top5)

most_common内部就是先计数后按次数排序取出前 N,底子还是Counter+ 排序。业务侧唯一的额外考虑是日志量级——一天几亿条时,单机哈希可能扛不住,得往消息队列 + 流计算框架上迁移。流计算的词频统计本质上是分片统计 + 汇总合并,跟前面的并行优化思路一脉相承。

5.2 Excel 场景:数据清洗后按出现次数筛选工号并升序输出

VBA 那段代码可以直接封装成一个宏:选中一个包含工号的列,运行宏,筛选出出现次数等于 N 的工号,按升序输出到另一列。 这比用 Excel 自带的数据透视表更适合重复执行。 数据透视表步骤多,而且每次数据更新都要手动刷新。 宏一次写好,按钮一点就完事。

5.3 数据可视化前奏:先统计再排序,直接喂给图表

做柱状图、词云之前,通常要把原始数据转成"标签 + 频次"的表结构。

import matplotlib.pyplot as plt word_freq = Counter(text.split()).most_common(10) labels = [w for w, _ in word_freq] values = [c for _, c in word_freq] plt.bar(labels, values) plt.show()

这里的most_common(10)已经帮你按频次排序了,要是想升序输出,就sorted(word_freq.items(), key=lambda x: x[1])。同样一个统计逻辑,换个排序方向,就能服务完全不同的可视化需求。

5.4 从数组统计到其他数据结构的迁移思路

这个解法稍微变形,还能处理:

  • 统计字符串中每个字符出现次数:Counter("hello world")
  • 统计二维数组中满足条件的行数:逐行套用哈希计数
  • 两个数组的交集/差集:先各自计数,再比较频次
  • 找出数组中出现次数超过一半(即大于 n/2)的元素:排序后取中位数验证

5.5 用树状数组解决"连续区间频次统计"的进阶题目

如果题目升级为"统计一个动态数组中各个区间段内元素的出现次数,并支持单点修改",哈希表就不够用了,需要树状数组(Fenwick Tree)或线段树这种支持区间求和与单点修改的数据结构。我见过的最经典例子是:维护一个长度为 n 的序列,支持两个操作——查询前缀和sum(11)、单点修改add(3, x)。树状数组在这类场景下能做到 O(log n) 的查询与更新,比每次重新统计全数组快得多。

这个进阶方向不展开讲了,但想提醒你:基础题目的解法是一块跳板,能把"哈希计数"和"树状数组"这两套思路放在一起理解,以后遇到数据流场景就不会抓瞎。

6. 我的实操心得与避坑清单

这个需求我写了不下二十次,闭着眼都能背出解法。最后分享几个只有踩过坑才知道的细节。

第一,永远先用测试用例验证筛选条件。写循环前,先把预期结果写出来。数组[1, 1, 2]查出现 2 次的元素,预期就是[1]。如果代码输出[],别急着改排序,先测计数对不对。

第二,哈希容器选错,排查半天也找不出原因。JS 普通对象会把数字键转字符串、__proto__这类特殊键还会出幺蛾子。直接上Map,省心省力。Map还有一个好处:插入顺序就是遍历顺序,这在某些场景下能做"按首次出现顺序输出"。

第三,排序永远要显式声明"按什么排"。你以为的升序,在不同语言、不同 API 里可能是字典序、可能是内存地址序、可能是随机序。显式写清楚数字比较器,既防止自己脑子短路,也方便同事 review 时一眼看懂意图。

第四,业务数据里元素类型比你以为的复杂。Excel 读出来的单元格有字符串、数字、日期、布尔值,日志里的接口名偶尔带空格或换行。统计前先做一层清洗,把空白 trim 掉,把类型统一掉,能省下大量排查时间。这步脏活不做,后面再巧妙的数据结构都救不了你。

最后分享一个真正好用的小技巧:如果你只需要"出现次数达标"的元素,不需要关心具体次数值,可以把次数直接当成淘汰条件来遍历——先给每个元素标记"出现过一次",第二次遇到直接从候选集合里移除,专门给"找出只出现一次的元素"这类题目用,空间占用还能再矮一截。这算是哈希统计的一个变种,写出来跟常规方案完全不同,面试时提一嘴,通常会有意外收获。

希望这篇把统计、筛选、排序的全链路讲透了。记住核心那七个字:先计数,再筛选,后排序。剩下的都是各语言 API 的细节,查文档翻翻就能解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询