news 2026/10/1 3:17:07

数组元素按出现次数筛选并升序输出:四种语言实现与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数组元素按出现次数筛选并升序输出:四种语言实现与工程实践

这道题看起来简单,但我在实际处理业务数据时经常碰到它的变体:比如从订单记录里找出恰好被下单3次的商品编号、从访问日志里筛选出访问了指定次数的用户IP,又或者从传感器数据中挑出异常频次的设备ID。核心无外乎四个动作——数组遍历计数、按指定次数筛选、对筛选结果升序输出,最后把元素本身打印出来。难点从来不在思路,而在不同语言里"怎么写得干净、跑得快、不出边界毛病"。

这篇文章我把JavaScript、Python、Java、C++四套写法和底层原理一起拆开讲,再顺带聊聊同样的逻辑怎么迁移到Excel、Shell这类工作场景里。适合刚刷算法题的新手,也适合需要在日常脚本里快速落地这段统计逻辑的开发者。

1. 拆解题目本质:一次频率统计要过三关

很多初学者一上来就写双重循环,对每个元素再从头到尾数一遍它出现了几次。这在小数组上勉强能跑,但一旦数据量上千,O(n²)的时间复杂度马上让你卡死。正确姿势是把它拆成三个独立环节,挨个击破。

1.1 第一关:建立频率映射表

所谓统计次数,本质上就是用哈希表(字典)记录"每个元素 -> 它出现了几次"。遍历一遍数组,对每个元素做一次查表加一。这一步的时间复杂度是O(n),空间复杂度是O(m),m表示数组里不同元素的个数。

选数据结构时有个关键判断:如果数组元素是非负整数且范围不大(比如成绩0~100分),直接用数组下标当元素、值当次数,连哈希都不用,速度最快。如果元素是负数、浮点数、字符串或对象,或者取值范围极大,就必须上哈希表。千万不要试图用普通对象同时统计1和"1",不同语言对键的隐式转换规则不同,很容易掉坑。

1.2 第二关:按指定次数过滤

频率表建好后,遍历它的键值对,把"值等于指定次数"的键挑出来。这里有个很容易忽略的边界:如果指定次数是0,结果永远为空数组;如果指定次数大于数组长度,结果也是空。这两种情况不需要特判,过滤条件自然满足不了,但逻辑上要清楚。

1.3 第三关:升序输出

筛选出的元素集必须排序。这里也有讲究:元素类型决定排序规则。纯数字可以直接按数值升序;字符串要按字典序;混合类型(比如数字和字符串混在一个数组里)就需要你先定义好"谁在前"的规则。不同语言对升序的默认实现不一样,后面每一节都会有对应提醒。

这三关串联起来的整体复杂度是O(n + m + k log k),其中k是符合条件的元素个数,通常远小于n。这也是为什么这个三段式解法能扛住百万级数据的原因。

2. JavaScript实现:Map计数和sort排序里的两个暗坑

JS是处理这类问题最常用的语言之一,因为数组方法全家桶实在方便。但用不好,两个隐坑会直接导致输出错误。

2.1 最干净的写法:Map + entries + filter + sort

function filterByCount(arr, count) { const freq = new Map(); for (const item of arr) { freq.set(item, (freq.get(item) || 0) + 1); } return [...freq.entries()] .filter(([key, value]) => value === count) .map(([key]) => key) .sort((a, b) => a - b); } // 使用示例 const arr = [5, 3, 5, 1, 3, 3, 8, 5, 2, 8, 8]; console.log(filterByCount(arr, 3)); // 出现3次的元素是 3 和 8,升序输出 [3, 8]

这段代码用Map而不是普通对象,原因在于:普通对象的键会被强制转成字符串,1和"1"会混在一起计数;而Map严格区分数字和字符串键。(freq.get(item) || 0) + 1是经典写法,第一次碰到的元素get返回undefined,undefined || 0取到0,加1后变成1;之后再碰到就先取旧值再加1。

2.2 坑一:sort()不传比较函数时是字典序

这是JS里出现频率最高的排序错误。[1, 2, 10].sort()的结果是[1, 10, 2],因为默认会把元素转成字符串,按Unicode码点排序。"10"排在"2"前面。所以升序输出数字时,必须显式传(a, b) => a - b。如果是字符串数组,直接sort()反而符合字典序预期;但如果你要排序的是包含数字和字符串的混合数组,a - b会算出NaN,这时你得自己定义完整比较逻辑。

// 字符串元素的升序比较 .sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)); // 等价写法 .sort((a, b) => String(a).localeCompare(String(b)));

2.3 坑二:浮点数次数统计的精度问题

如果数组里是浮点数(比如0.1 + 0.2这样的计算结果),直接当Map的键没问题,Map比较用的是严格等于(SameValueZero),0.1和0.2是不同的键。但如果你用对象当键的替代方案、或者用数组下标法,就麻烦了。这里我的建议是:如果浮点数的精度对你没有意义(比如传感器读数的整数部分),先Math.round归一化再加进Map;如果精度有意义,就接受"值完全相等才算同一元素"这个语义。

2.4 边缘情况与性能测试

我实际测试过几个边界数据,发现最容易出错的不是统计逻辑,而是入口参数的类型:

// 空数组 console.log(filterByCount([], 2)); // [] // 指定次数大于任何元素频率 console.log(filterByCount([1, 2, 2], 5)); // [] // 全部是同一个元素 console.log(filterByCount([7, 7, 7, 7], 4)); // [7] // 包含负数 console.log(filterByCount([-3, -1, -3, 0, -1], 2)); // [-3, -1]

负数排序时a - b依然正确,因为减法比较不依赖正负号,只看差值。另外,如果count参数传了字符串"3"而不是数字3,严格相等value === count会直接返回空数组。这是我在团队代码评审时最常抓到的类型隐患——建议入口处加一行count = Number(count)做防御。

2.5 大数据量时的内存优化

当数组达到几十万量级,[...freq.entries()]会一次性把所有键值对展开成数组,内存峰值可能翻倍。这时可以用for...of循环遍历Map,只收集符合条件的元素,省掉中间展开的花销:

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

实际压测里,这个版本在百万级随机整数数组上比展开式快15%左右,内存占用也更平滑。追求极致性能可以把排序省掉,改成Math.min和Math.max先找出范围再桶排,但多数业务场景用不上。

3. Python版本:一行流写法和海量数据下的效率账

Python写这道题几乎是最舒服的,collections.Counter直接给频率表,列表推导式做过滤,sorted完成升序。但"写得快"不等于"跑得快",里面有几笔账要算清楚。

3.1 Counter一行流与手写dict对比

from collections import Counter def filter_by_count(arr, count: int): freq = Counter(arr) return sorted(k for k, v in freq.items() if v == count) # 示例 numbers = [5, 3, 5, 1, 3, 3, 8, 5, 2, 8, 8] print(filter_by_count(numbers, 3)) # [3, 8]

Counter(arr)的底层是C实现的循环计数,比你在Python层手写for item in arr: freq[item] = freq.get(item, 0) + 1快不少。在一百万个元素的数组上,Counter大概能省30%左右的时间。原因是Python层每执行一次循环体都有解释器开销,而C循环没有。

但要注意一个细节:Counter是dict的子类,Python从3.7起dict保持插入顺序,所以freq.items()出来的顺序是元素第一次出现的顺序,不是你想要的升序。所以sorted()这一步必须保留,不能依赖字典顺序。

3.2 sorted对混合类型与中文字符串的排序规则

当数组中混着数字和字符串时,sorted会直接抛TypeError,它不知道该怎么比较1和"a"。这其实是个保护机制,逼你先把类型统一。如果元素全是中文字符串,sorted默认按Unicode码点排序,跟中文拼音、笔画都没关系。比如['李', '张', '陈']升序结果是['张', '李', '陈'],因为"张"(U+5F20)的码点比"李"(U+674E)大,这跟直觉里的拼音顺序完全不同。

如果业务上真要按拼音排,就得用locale模块:

import locale from functools import cmp_to_key locale.setlocale(locale.LC_COLLATE, 'zh_CN.UTF-8') sorted(names, key=cmp_to_key(locale.strcoll))

3.3 大数据量下的numpy加速方案

热搜词里有"python 数据分析之 numpy 统计",确实,如果数组本身是numpy.ndarray且元素是统一数值类型,用np.unique(return_counts=True)比Python原生Counter快一个数量级:

import numpy as np def filter_by_count_np(arr: np.ndarray, count: int): values, counts = np.unique(arr, return_counts=True) return values[counts == count] # 示例 arr_np = np.array([5, 3, 5, 1, 3, 3, 8, 5, 2, 8, 8]) print(filter_by_count_np(arr_np, 3)) # [3 8]

np.unique内部先排序再分组,一次性拿到唯一值和频次,连后续升序都省了,因为values本身就是升序的。我在处理GIS轨迹点统计(热搜里那个"qgis统计kml路线公里数"的类似需求)时,百万级点坐标用numpy这套方案毫秒级出结果。

3.4 惰性生成器与内存友好写法

如果数组是文件流一行行读进来的,就不要先全量载入内存再Counter。可以边读边更新Counter,或者用生成器表达式传给Counter:

def read_numbers(file_path): with open(file_path, 'r') as f: for line in f: yield int(line.strip()) freq = Counter(read_numbers('data.txt')) result = sorted(k for k, v in freq.items() if v == 10)

这种写法下,read_numbers是生成器,Counter消费它时逐条计数,内存里始终只存频率表,不存全量数组。对几十GB日志文件做词频统计时,这是唯一可行方案。

4. Java与C++实现:传统语言里那些容易翻车的细节

相比脚本语言,Java和C++没有内置的"按次数筛选"一步到位API,但控制力更强。这里把常见的几个翻车点一次说清。

4.1 Java版:HashMap + stream管道

import java.util.*; import java.util.stream.Collectors; public class FilterByCount { public static List<Integer> filterByCount(int[] arr, int count) { Map<Integer, Integer> freq = new HashMap<>(); for (int n : arr) { freq.merge(n, 1, Integer::sum); } return freq.entrySet().stream() .filter(e -> e.getValue() == count) .map(Map.Entry::getKey) .sorted() .collect(Collectors.toList()); } public static void main(String[] args) { int[] arr = {5, 3, 5, 1, 3, 3, 8, 5, 2, 8, 8}; System.out.println(filterByCount(arr, 3)); // [3, 8] } }

merge(n, 1, Integer::sum)是Java 8之后最优雅的计数写法:键不存在时放入初值1,键已存在时用Integer::sum把旧值和新值相加。这段代码里最隐蔽的坑是Integer拆箱比较——e.getValue() == count中,getValue()返回Integer,count是int,Java会自动拆箱成int再比较,所以这里用==没问题。但如果两个都是Integer,比如Integer.valueOf(200) == Integer.valueOf(200),结果就是false,因为==比较的是引用,只有-128到127在缓存池内才相等。教训就是:判断包装类型相等一律用equals或intValue(),不要凭直觉用==。

4.2 Java版:数组下标桶排序替代HashMap

如果确认数组元素是非负整数且最大值不大,用桶排序思路能省掉哈希的开销:

public static List<Integer> filterByCountBucket(int[] arr, int count) { int max = 0; for (int n : arr) max = Math.max(max, n); int[] freq = new int[max + 1]; for (int n : arr) freq[n]++; List<Integer> result = new ArrayList<>(); for (int i = 0; i <= max; i++) { if (freq[i] == count) result.add(i); } return result; // 天然升序,无需再排序 }

这个版本的时间复杂度是O(n + max),如果max远小于n,比HashMap方案更快。但max一旦达到千万级,new int[max + 1]会直接OutOfMemoryError。所以用之前先估算max的合理范围。

4.3 C++版:unordered_map + sort的经典组合

#include <iostream> #include <vector> #include <unordered_map> #include <algorithm> std::vector<int> filterByCount(const std::vector<int>& arr, int count) { std::unordered_map<int, int> freq; for (int n : arr) freq[n]++; std::vector<int> result; for (const auto& [key, value] : freq) { if (value == count) result.push_back(key); } sort(result.begin(), result.end()); return result; } int main() { std::vector<int> arr = {5, 3, 5, 1, 3, 3, 8, 5, 2, 8, 8}; auto res = filterByCount(arr, 3); for (int x : res) std::cout << x << " "; // 3 8 return 0; }

C++里最容易被忽略的是unordered_map迭代结果无序,所以最后一步sort是必须的。如果你用map(红黑树),键本身就是有序的,过滤完直接push_back就是升序,省一次排序——但插入和查询都变成O(log m)。数据量小用map更简洁,数据量大用unordered_map + sort更快,这是个经典的工程权衡。

4.4 C++进阶:先排序再统计的空间优化方案

一个常被忽视的替代思路是:先把原数组排序,再一遍扫描统计相邻相同元素的出现次数。这样不需要哈希表,额外空间是O(1)。

std::vector<int> filterByCountNoHash(std::vector<int> arr, int count) { sort(arr.begin(), arr.end()); std::vector<int> result; int i = 0; while (i < arr.size()) { int j = i; while (j < arr.size() && arr[j] == arr[i]) j++; if (j - i == count) result.push_back(arr[i]); i = j; } return result; }

时间复杂度从O(n + m + k log k)变成O(n log n),因为排序成了主操作。当n在百万级别以下、而你特别在意内存占用时(比如嵌入式环境),这个方案比哈希表更稳。哈希表在极端碰撞情况下还有被DoS攻击的理论风险,排序方案完全不受影响。

5. 从算法题到业务场景:Excel报表、Shell命令和日志分析里的同款逻辑

写算法题是一回事,但我在真实业务里发现,这种"按指定次数筛选 + 排序"的统计逻辑根本不止出现在代码里。热搜词里一大半跟Excel、日志、词频统计相关,这里集中讲讲怎么迁移。

5.1 Excel里用COUNTIF辅助列实现

当数据躺在Excel里,你不想写代码时,可以用辅助列完成同样的统计。目标:从A1:A1000这列数据中,找出出现次数等于指定次数(比如3次)的所有值,并升序排列。

  • 在B1输入=COUNTIF($A$1:$A$1000, A1),下拉填充。B列就是每个元素在整列中出现的次数。
  • 在旁边区域输入=IF(COUNTIF($B$1:$B$1000, 3) >= ROW() - 某行基准, 指定索引, "")这类数组公式,或者更直观的做法:用"筛选"功能,对B列筛选等于3,再对A列升序排序。

最省事的还是透视表:把A列拖到行区域,再拖到值区域并改成"计数",筛选计数为3的行,最后按行标签排序。这个方法处理十多万行数据也不会卡。热搜里"excel同一列中统计含关键词对应数据求和""电子表格根据某项合计"其实都是同一套透视表思路。

5.2 VBA里用Dictionary对象

如果你需要在Excel里做自动化重复操作,可以录一个宏或者直接写VBA。VBA里的Dictionary对应哈希表:

Sub FilterByCount() Dim arr As Variant arr = Range("A1:A11").Value ' 读入数据列 Dim freq As Object Set freq = CreateObject("Scripting.Dictionary") Dim i As Long For i = LBound(arr, 1) To UBound(arr, 1) Dim key As Variant key = arr(i, 1) If freq.Exists(key) Then freq(key) = freq(key) + 1 Else freq.Add key, 1 End If Next i Dim result As Collection Set result = New Collection Dim k As Variant For Each k In freq.Keys If freq(k) = 3 Then result.Add k Next k ' 排序并写入新列(VBA没有内置的集合排序,需要自己写冒泡/快排) End Sub

VBA没有原生的低复杂度排序API,这是最疼的地方。小数据量直接写个双层循环冒泡排序没问题;数据量上万再考虑用ArrayList或者调用Excel的WorksheetFunction.Sort来排序。热搜里"vba数组""vba数组对比最快"说明很多人卡在VBA数组操作和排序上,我的建议是:VBA里能用Excel工作表函数就用工作表函数,自己写循环能少则少。

5.3 Shell命令一行搞定日志词频统计

如果你处理的是日志、文本行这类数据,Shell其实是最快的方案。热搜里"统计行数""统计单词个数"对应到命令就是wc -l、wc -w,而"统计出现指定次数的元素并升序输出"对应的是这套管道:

# 从access.log提取IP列(假设第一列),统计每个IP出现次数,筛出恰好出现3次的IP,按数值升序 awk '{print $1}' access.log | sort | uniq -c | awk '$1 == 3 {print $2}' | sort -n

拆解一下:awk '{print $1}'取第一列,sort把相同IP排到相邻位置,uniq -c统计相邻重复次数(输出格式是"次数 值"),第二个awk '$1 == 3 {print $2}'筛出次数为3的项并把值提出来,最后的sort -n做数值升序。这里有个容易踩的坑:第一个sort和最后一个sort -n缺一不可。没有第一个sort,uniq -c统计的是"相邻相同元素"的数量而非全局数量;没有最后一个sort -n,IP地址按字典序排出来就是10.0.0.1在2.2.2.2前面这种反直觉结果。

5.4 同一个算法套路在不同岗位的变体

我见过运营同学用Excel干这件事,后端用Shell干这件事,数据工程师用Flink实时计算做"词频统计初体验",甚至有人在电子表格里用数据透视表统计"2012-2021年全球地震发震情况"这类公开数据集。它们底层的逻辑完全一样:频率表 + 条件过滤 + 排序。所以这道算法题不是刷完就扔的,它是很多实际报表、监控、风控功能的最简抽象。

5.5 关于"先排序再统计"的取舍

最后再聊一个工程上的经验:如果元素种类是无限大的(例如订单号),哈希表方案永远是首选,因为排序的O(n log n)是硬成本;如果元素种类是有限的且已经天然有序(比如日期、序号),那排序方案因为省了哈希碰撞的开销反而更稳。我个人的判断标准很简单:拿不准的时候就看"需要排序的元素集"和"全集"的大小关系,当符合次数条件的元素数量远超总元素数量的一半时,说明这道题在数据结构设计时就应该用有序结构存频率表。

我在实际项目里发现,只要摸清了这一套"哈希计数 + 条件过滤 + 排序"的心法,不管语言怎么换、场景怎么变,都能快速落地。最后再分享一个小技巧:如果你拿到的数组里元素类型不统一(比如既有数字又有字符串),先在做频率表之前把所有元素统一转成字符串或统一转成数字,这能省掉后面所有比较运算的麻烦。试着拿自己手头的数据跑一遍,你会回来感谢现在这个思路的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 3:17:07

亚马逊Climate Pledge Friendly绿标认证实操:流量红利与申请全攻略

1. 为什么一夜之间&#xff0c;跨境卖家都在追这抹绿亚马逊前台那个墨绿色的小叶子标识&#xff0c;这两年出现的频率越来越高。做跨境电商的朋友应该都有印象&#xff0c;搜索页面里部分产品标题下方会多一行“Climate Pledge Friendly”的小字&#xff0c;配一片叶子图标。点…

作者头像 李华
网站建设 2026/10/1 3:17:01

ShardingSphere实战:Spring Boot订单系统分库分表与读写分离全攻略

1. 先搞清楚ShardingSphere到底帮你做了什么很多人一听到“分库分表”就头皮发麻&#xff0c;觉得自己业务还没到那个量级&#xff0c;没必要折腾。其实ShardingSphere并不是大厂专属的“重型武器”&#xff0c;它更像是一个数据层的“路由管家”——你告诉他哪条数据去哪张表、…

作者头像 李华
网站建设 2026/10/1 3:14:56

SpringBoot + CompletableFuture + 线程池:高并发异步编排实战指南

1. 不只是“异步”那么简单&#xff1a;为什么需要编排后端接口性能优化这件事&#xff0c;做久了你会发现一个非常现实的问题&#xff1a;单靠“异步”两个字解决不了真正的性能瓶颈。举个例子&#xff0c;一个聚合查询接口要调用户服务、订单服务、营销服务、库存服务四个下游…

作者头像 李华
网站建设 2026/10/1 3:14:38

光伏板积灰四分类识别:光照鲁棒性与监督对比学习实战

简介&#xff1a;本资源是一个面向计算机专业本科生毕业设计与深度学习实战训练的太阳能光伏板积灰识别项目&#xff0c;聚焦真实工业场景中的灰尘污染检测难题&#xff0c;支持图像四分类任务。项目采用自制高质量灰尘图像数据集&#xff0c;集成普通数据增广、AutoAugment增强…

作者头像 李华
网站建设 2026/10/1 3:13:40

CSS border 实践指南:从样式、宽度、颜色到方向与盒模型避坑

CSS border 是前端写样式时几乎躲不开的属性&#xff0c;边框的样式、宽度、颜色、方向四个维度&#xff0c;看着就四件事&#xff0c;但实际项目里因为简写规则、默认值、盒模型影响&#xff0c;经常能见到一堆莫名其妙的问题。这篇文章我会把 border 从基础规则讲到实战细节&…

作者头像 李华
网站建设 2026/10/1 3:13:27

Unity A*寻路算法实战:从原理到代码实现与优化

1. A*寻路算法&#xff1a;Unity游戏开发绕不开的必修课做游戏开发这些年&#xff0c;我面试过不少Unity候选人&#xff0c;几乎每次都会聊到寻路。有人张口就是NavMesh Agent&#xff0c;但一追问A的原理就含糊其辞。这其实很可惜——Unity内置的NavMesh确实好用&#xff0c;但…

作者头像 李华