ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

30 Seconds of Code:在 JavaScript 数组中分组并统计元素频次(Group and Count Values)

30 Seconds of Code:在 JavaScript 数组中分组并统计元素频次(Group and Count Values) 教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载本篇技术指南围绕 30-seconds-of-code 仓库中的核心片段 count-grouped-elements 展开系统讲解如何用Array.prototype.reduce()等原生方法统计数组中每个值的出现次数、依据函数或属性对元素分组计数并进一步用Map及自定义FrequencyMap数据结构满足动态数据场景。读完本文你将掌握从简单频次统计到可增删改查的频率映射结构的完整实现链路并了解这些实现与仓库内 group-array-elements、most-frequent-array-element、count-occurrences 等相邻片段的关联。一、分组计数解决了什么问题在处理数组时统计每个值出现了多少次是一类高频需求例如统计一篇文章中各单词的词频、聚合用户列表中每个年龄段的分布、分析日志中各状态码的数量等。此类需求既针对字符串、数字等原始值也经常面向对象数组——需要先按某个属性或映射函数提取分组键再统计每个组的数量。好消息是借助 JavaScript 原生的Array方法这类逻辑几乎不需要引入任何第三方库。下面的四个层次将逐步递进用reduce()直接统计数组中各原始值的频次用map()reduce()基于函数或属性名分组计数countBy用Map替代普通对象获得插入顺序、任意类型键与更高的键增删效率基于Map继承实现一个可持续增删、查询与排序的自定义FrequencyMap数据结构。二、用 reduce 统计每个值的出现次数最简单也最常见的做法是使用Array.prototype.reduce()构建一个对象把数组的唯一值作为键、把对应频次作为值。初始化时利用空值合并运算符??nullish coalescing当键尚不存在时先置为0之后每次遇到相同值就自增1const frequencies arr arr.reduce((a, v) { a[v] (a[v] ?? 0) 1; return a; }, {}); frequencies([a, b, a, c, a, a, b]); // { a: 4, b: 2, c: 1 } frequencies([...ball]); // { b: 1, a: 1, l: 2 }这里有两个值得注意的细节??与||的区别??只在左侧为null或undefined时取右侧默认值因此0、、false这类合法值不会被误判为缺失。若改用||虽然大部分场景结果相同但语义上会把假值一并吞掉不够严谨。对象键的字符串化对象键会被自动转换为字符串因此当数组中混有数字和数字字符串时如1与1它们会被归并到同一个键下。若需要区分原始类型应改用下一节的Map方案。这段实现与仓库中 most-frequent-array-element 片段的统计频次部分如出一辙——后者在此基础上再用Object.entries()二次reduce()找出出现最多的元素可见频次统计是找众数等进阶算法的基础。三、基于函数或属性名分组计数countBy仅统计原始值的频次往往不够面对对象数组或者希望按向下取整字符串长度等派生结果分组时就需要先把每个元素映射成一个分组键。原片段给出的countBy实现如下const countBy (arr, fn) arr .map(typeof fn function ? fn : val val[fn]) .reduce((acc, val) { acc[val] (acc[val] || 0) 1; return acc; }, {}); countBy([6.1, 4.2, 6.3], Math.floor); // {4: 1, 6: 2} countBy([one, two, three], length); // {3: 2, 5: 1} countBy([{ count: 5 }, { count: 10 }, { count: 5 }], x x.count); // {5: 2, 10: 1}实现要点map()先行映射先用Array.prototype.map()将每个元素转换为分组键。fn参数被设计成双模式——传入函数则直接作为映射函数调用传入字符串则视为属性名用val val[fn]提取对应属性值。typeof fn function的判断让同一个函数同时兼容按函数分组和按属性分组两种调用方式。reduce()累计计数与第二节逻辑一致只是此时累加的对象键来自映射后的结果。注意这里用的是||而非??二者在此处的差异通常无感但保持认知即可。该函数或属性名的双模式设计并非孤例仓库中的 group-array-elements 片段groupBy采用了完全相同的约定——typeof fn function ? fn(val, i, arr) : val[fn]——只不过它把元素分组存放为数组{ 4: [4.2], 6: [6.1, 6.3] }而不是计数。这也解释了countBy名称的由来它本质上是groupBy的计数版。四、改用 Map保持插入顺序与任意类型键前面两个示例都用普通对象存放频次这在多数场景够用但存在三个固有限制键只能是字符串或Symbol键的顺序遵循字符串键的枚举规则而非纯插入顺序Object的原型链还可能带来constructor、toString之类的键冲突隐患。改用Map可以一次性解决这些问题——Map保留插入顺序、支持任意类型的键含对象、数字并且内置get()/set()/delete()/has()删除与存在性检查都更高效。基于Map的两个实现如下const frequenciesMap arr arr.reduce((a, v) a.set(v, (a.get(v) ?? 0) 1), new Map()); frequenciesMap([a, b, a, c, a, a, b]); // Map(3) { a 4, b 2, c 1 } const countByMap (arr, fn) arr .map(typeof fn function ? fn : val val[fn]) .reduce((acc, val) { acc.set(val, (acc.get(val) || 0) 1); return acc; }, new Map()); countByMap([6.1, 4.2, 6.3], Math.floor); // Map(2) { 6 2, 4 1 } countByMap([one, two, three], length); // Map(2) { 3 2, 5 1 } countByMap([{ count: 5 }, { count: 10 }, { count: 5 }], x x.count); // Map(2) { 5 2, 10 1 }与对象版的差异集中在累加器上初始值由{}改为new Map()读键用a.get(v)写键用a.set(...)且set()返回Map自身因此frequenciesMap可以写成单行箭头表达式直接返回reduce的结果计数由(acc.get(val) || 0) 1完成逻辑等价于对象版的(acc[val] || 0) 1。一个直观的收益是类型保真countByMap([{ count: 5 }, ...], x x.count)的结果中Map的键是数字5、10而对象版countBy的键则是字符串5、10。同样的类型保真诉求在 most-frequent-array-element 片段中也被明确提及——其Map版实现正是为了修正对象版结果恒为字符串的缺陷。五、实现一个可增删改查的 FrequencyMap 数据结构前四节的方案都是一次性计算给定一个数组得到频次统计。但当数据持续变化反复插入、删除元素时每次都要重新reduce()整个数组既繁琐又低效——尤其当你只关心频次、并不需要保留原始数组时。此时更合适的做法是封装一个自定义数据结构让它内部维护频次并支持随时查询、递增、递减。原片段给出的FrequencyMap直接继承内置Map类通过覆写关键方法来提供类 Set 的频率容器语义class FrequencyMap extends Map { constructor(iterable) { super(); iterable.forEach(value this.increment(value)); } get(value) { return super.get(value) ?? 0; } has(value) { return super.get(value) 0; } increment(value) { super.set(value, this.get(value) 1); return this; } decrement(value) { super.set(value, Math.max(this.get(value) - 1, 0)); return this; } toSortedArray(ascending true) { if (ascending) return [...this].sort((a, b) a[1] - b[1]).map(v v[0]); else return [...this].sort((a, b) b[1] - (1)[1]).map(v v[0]); } } const fMap new FrequencyMap([a, b, c, a, a, b]); fMap.decrement(c); fMap.increment(d); console.log(fMap.toSortedArray(false)); // [ a, b , d ]逐方法拆解其设计意图constructor(iterable)由于该结构更像一个Set只关心某值是否存在以及出现几次构造函数接受一个可迭代的值集合而非键值对。它调用无参的super()再通过Array.prototype.forEach()对每个值调用this.increment(value)完成初始化填充。这意味着new FrequencyMap([a, b, c, a, a, b])得到a频次 3、b频次 2、c频次 1。get(value)覆写为不存在的值返回0——super.get(value) ?? 0。这样外部调用方无需先has()判断即可安全读取任何值的频次语义上等价于频次天然为 0。has(value)从键是否存在改为频次是否大于 0。这带来一个有用的副作用某个值即使曾被decrement()到 0也不会再被has()认为存在。increment(value)/decrement(value)分别用this.get(value) 1与Math.max(this.get(value) - 1, 0)更新频次后者通过Math.max(..., 0)保证频次永远不会降到负数。两者都return this支持链式调用。toSortedArray(ascending true)把Map展开为[键, 频次]对数组按频次升序或降序排序后仅返回键数组。默认升序传false时降序。示例运行过程初始化后a频次 3、b频次 2、c频次 1fMap.decrement(c)使c变为 0fMap.increment(d)使d变为 1最终按频次降序输出[a, b, d]a频次 3、b频次 2、d频次 1c为 0 被排除。代码勘误提示原片段toSortedArray的降序分支写的是(a, b) b[1] - (1)[1]其中(1)[1]是明显的笔误对字面量1取下标[1]得undefined相减得NaN排序结果不确定。按降序排序的正确比较器应为(a, b) b[1] - a[1]。在本地运行此示例时请留意该处其余方法均可直接执行验证。从源码结构看这一设计巧妙地把频次容器的复杂度收敛在Map继承层内get/has的语义覆写让外部 API 更友好increment/decrement提供 O(1) 级别的单值更新toSortedArray则只在需要输出时排序——比起每次变更都重算整个数组这种增量维护的思路在数据频繁变动时明显更优。六、分组计数家族的完整脉络与仓库定位count-grouped-elements在 30-seconds-of-code 仓库中并非孤立片段围绕它构成了一组互补的分组与计数工具。查看 content/redirects.yaml 可以发现/js/s/frequencies、/js/s/value-frequencies、/js/s/count-by三个旧地址均以 301 状态重定向到/js/s/count-grouped-elements见 redirects.yaml 第 2053-2061 行说明该片段是这一主题的权威收敛点历史上有多个等价命名。与它直接相关的相邻片段包括group-array-elementsgroupBy(arr, fn)把元素按分组键聚合成数组{ 4: [4.2], 6: [6.1, 6.3] }与countBy使用相同的双模式fn约定。需要分组后保留元素本身时用它需要每组数量时用本文的countBy。most-frequent-array-element在频次统计基础上找出出现最多的元素并同样给出对象版键恒为字符串→ Map 版键类型保真的演进路径与本文第四节互为印证。count-occurrences只统计单个指定值在数组或字符串中出现的次数countOccurrences(arr, val)是逐值频次的轻量特例字符串场景则用indexOf() 循环实现子串计数。partition-array按条件把数组切分为多个子数组partitionBy同样以Map作为累加器按分组键收集元素——与countByMap的累加器形态几乎一致只是把计数换成了push 进分区数组。这组片段的共同模式值得总结以reduce()为骨架、以对象或 Map为累加器、以函数或属性名为分组键提取约定。理解了这一模式你就掌握了数组聚合类工具的通用设计思路无论是计数、分组、分区还是求众数都可以在此基础上快速派生。七、小结本文围绕 30-seconds-of-code 的count-grouped-elements片段完整覆盖了四个由浅入深的实现层次frequenciesreduce()??统计原始值频次适合一次性计算countBymap()reduce()按函数或属性分组计数兼容对象数组frequenciesMap/countByMapMap版实现保真键类型、保持插入顺序、支持高效增删FrequencyMap继承Map的自定义容器提供get/has/increment/decrement/toSortedArray面向高频变更的动态场景。选择哪个方案取决于你的场景一次性统计选 1 或 2需要类型保真与键操作选 3数据持续变化、只关心频次则直接封装FrequencyMap。所有实现均可直接复制运行并与仓库中的groupBy、众数查找、单值计数等片段组合使用。赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐30 Seconds of Code 二分查找实战在有序 JavaScript 数组中快速定位元素30 Seconds of Code 二分查找实战在有序 JavaScript 数组中快速定位元素 二分查找Binary Search是计算机科学中最经典教程文档在 JavaScript 中获取数组头部head与尾部tail元素30-seconds-of-code 实战指南在 JavaScript 中获取数组头部head与尾部tail元素30 seconds of code 实战指南 数组的 头部head 指其第一个教程文档30 seconds of code用 JavaScript 生成数组中所有连续元素子数组aperture的完整实现30 seconds of code用 JavaScript 生成数组中所有连续元素子数组aperture的完整实现 在 30 seconds of co教程文档上一篇用 Typora插件 3 步画出能直接贴进周报的 Typora 图表下一篇B站缓存视频合并完整指南4步把Bilibili缓存碎片变成标准MP4创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表