统计数组元素出现次数/频率

352

在 Javascript 中,我试图获取一个数字值的初始数组并计算其中的元素数量。理想情况下,结果会是两个新数组,第一个指定每个唯一元素,第二个包含每个元素出现的次数。但是,对于输出格式,我也很乐意听取建议。

例如,如果初始数组如下:

5, 5, 5, 2, 2, 2, 2, 2, 9, 4

然后将创建两个新数组。第一个数组将包含每个唯一元素的名称:

5, 2, 9, 4
第二个数组将包含该元素在初始数组中出现的次数:
3, 5, 1, 1
因为数字5出现了三次,数字2出现了五次,数字9和4各出现了一次。
我已经搜索过很多解决方案,但似乎没有一个可行的,并且我自己尝试的所有方法都变得非常复杂。任何帮助将不胜感激!
谢谢 :)

16
如果你只需要判断一个值是否仅出现一次(而不是两次或更多),你可以使用 if (arr.indexOf(value) == arr.lastIndexOf(value)) - Rodrigo
2
我们可以使用 ramda.js 来轻松实现这一点。R.countBy(r=> r)(ary)``` - Eshwar Prasad Yaddanapudi
arr.filter(x => x===5).length 会返回 3,表示数组中有 '3' 个数字 '5'。 - noobninja
假设我的响应是对象数组。 - Ajay
42个回答

344

你可以使用一个对象来保存结果:

const arr = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4];
const counts = {};

for (const num of arr) {
  counts[num] = counts[num] ? counts[num] + 1 : 1;
}

console.log(counts);
console.log(counts[5], counts[2], counts[9], counts[4]);

现在你的counts对象可以告诉你特定数字的计数:

console.log(counts[5]); // logs '3'

如果您想要获取成员数组,只需使用 keys() 函数。
keys(counts); // returns ["5", "2", "9", "4"]

3
需指出的是,Object.keys()函数只受IE9+、FF4+、SF5+、CH6+支持,但Opera不支持。我认为最大的问题在于**IE9+**。 - Robert Koritnik
34
同样地,我也喜欢counts[num] = (counts[num] || 0) + 1。这种方式只需要在那一行中写两次counts[num],而不是三次。 - robru
2
这是一个很好的答案。这可以轻松地抽象成一个接受数组并返回“计数”对象的函数。 - bitsand
为什么不这样做:var arr = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4]; var counts = {}; arr.forEach(function(num) { if (!(num in counts)) { counts[num] = 0; } counts[num]++; }); console.log(counts);这种方法满足了获取唯一值和计数的要求,而且只使用了一个对象来处理这两个需求,而不是使用两个独立的数组。 - Patrick Lewis
请注意,当数组元素用作对象键时,它们总是被转换为字符串。因此,如果您传递 [1, "1", { toString: () => "1" }],则会得到结果 { 1: 3 } - 3limin4t0r
显示剩余4条评论

194

const occurrences = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4].reduce(function (acc, curr) {
  return acc[curr] ? ++acc[curr] : acc[curr] = 1, acc
}, {});

console.log(occurrences) // => {2: 5, 4: 1, 5: 3, 9: 1}


2
谢谢,非常好的解决方案 ;) ...获取“key”和“value”数组: const keys = Object.keys(a); const values = Object.values(a); - ncenerar
1
简写:使用acc[curr] = (acc[curr] || 0) + 1代替使用if/else。您可以在下面的答案中查看。 - Nguyễn Văn Phong
干净整洁!这正是我正在寻找的 :) - jim1427
请注意,当数组元素用作对象键时,它们总是被转换为字符串。因此,如果您传递 [1, "1", { toString: () => "1" }],则会得到结果 { 1: 3 } - 3limin4t0r
@ase,我应该改变什么才能得到这样的结果:num:2,occ:5?谢谢。 - Menai Ala Eddine - Aladdin

119

一行ES6解决方案。很多答案使用对象作为映射,但我没有看到有人使用实际的Map

const map = arr.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map());

使用 map.keys() 可以获取唯一的元素。
使用 map.values() 可以获取元素出现的次数。
使用 map.entries() 可以获取 [元素,频率] 对。

var arr = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4]

const map = arr.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map());

console.info([...map.keys()])
console.info([...map.values()])
console.info([...map.entries()])


我使用了这个,它工作得很好! - SalahAdDin
我原以为它可以让我使用任意对象作为键,但我发现无法告诉它如何计算对象ID,所以没什么帮助。 - x-yuri
@x-yuri 这个问题并不是特别关注于对象,但是将 e 改为 e.id 应该就足够了。另外,我不确定 id 是否应该包含重复的属性,因为它可以帮助我们识别元素。 - corashina
是的,这个问题不涉及对象,所以你可以忽略我的评论。但我来到这里是为了寻找解决方案 我的情况Map 看起来很有前途,但最终发现它并没有什么帮助。 - x-yuri

117

const arr = [2, 2, 5, 2, 2, 2, 4, 5, 5, 9];

function foo (array) {
  let a = [],
    b = [],
    arr = [...array], // clone array so we don't change the original when using .sort()
    prev;

  arr.sort();
  for (let element of arr) {
    if (element !== prev) {
      a.push(element);
      b.push(1);
    }
    else ++b[b.length - 1];
    prev = element;
  }

  return [a, b];
}

const result = foo(arr);
console.log('[' + result[0] + ']','[' + result[1] + ']')
console.log(arr)


42
该操作会对数组进行排序(副作用是不好的),而且排序的时间复杂度为O(N log(N)),所获得的优雅程度不值得这样做。 - ninjagecko
如果没有第三方库提供的高级原语,我通常会像“reduce”答案一样实现它。在我看到它已经存在之前,我正要提交这样一个答案。尽管如此,“counts[num] = counts[num] ? counts[num]+1 : 1”答案也可以工作(相当于“if(!result[a[i]])result[a[i]]=0”答案,更优雅但不易读);这些答案可以修改为使用“更好”的版本的for循环,可能是第三方for循环,但我忽略了这一点,因为标准的基于索引的for循环可悲地成为默认值。 - ninjagecko
对于小数组,原地排序可能比创建关联数组更快。 - quant_dev
@ŠimeVidas 我为 Array.sort 添加了一个免责声明,因为在实际代码中忽略这个事实会让我犯错。 (很容易天真地将它视为制作副本,因为它返回已排序的数组。) - jpaugh
同意@ninjagecko的观点。使用“字典”会更好。这里是我对另一种方法的答案。 - Nguyễn Văn Phong

102

如果使用underscore或lodash,这是最简单的方法:

_.countBy(array);

如下:

_.countBy([5, 5, 5, 2, 2, 2, 2, 2, 9, 4])
=> Object {2: 5, 4: 1, 5: 3, 9: 1}

正如其他人指出的那样,你可以在结果上执行_.keys()_.values()函数,以获得唯一数字和它们的出现次数。但是根据我的经验,原始对象更容易处理。


值得注意的是,使用countBy时,它只包括列表中存在的项目,因此如果您想计算可能不存在于列表中的项目,则需要处理异常。或者像这样使用lodash过滤器和长度:filter([true, true, true, false], function(m){return m==true}).length。如果没有值存在,则仅返回0。 - Doug
值得一提的是您需要添加:const _ = require("lodash") - Alex

69
不要使用两个数组来存储结果,使用一个对象代替:
a      = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4];
result = { };
for(var i = 0; i < a.length; ++i) {
    if(!result[a[i]])
        result[a[i]] = 0;
    ++result[a[i]];
}

那么result将会是这样的:

{
    2: 5,
    4: 1,
    5: 3,
    9: 1
}

62

如何使用ECMAScript2015选项。

const a = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4];

const aCount = new Map([...new Set(a)].map(
    x => [x, a.filter(y => y === x).length]
));

aCount.get(5)  // 3
aCount.get(2)  // 5
aCount.get(9)  // 1
aCount.get(4)  // 1

这个例子将输入数组传递给Set构造函数,创建一个包含唯一值的集合。然后展开语法将这些值扩展到一个新数组中,以便我们可以调用map并将其转换为一个二维数组[value, count]对,即以下结构:
Array [
   [5, 3],
   [2, 5],
   [9, 1],
   [4, 1]
]

新数组随后传递给Map构造函数,从而产生一个可迭代对象:
Map {
    5 => 3,
    2 => 5,
    9 => 1,
    4 => 1
}

Map对象的优点是它保留数据类型 - 也就是说,aCount.get(5)将返回3,但aCount.get("5")将返回undefined。 它还允许任何值/类型作为键,这意味着该解决方案也适用于对象数组。

function frequencies(/* {Array} */ a){
    return new Map([...new Set(a)].map(
        x => [x, a.filter(y => y === x).length]
    ));
}

let foo = { value: 'foo' },
    bar = { value: 'bar' },
    baz = { value: 'baz' };

let aNumbers = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4],
    aObjects = [foo, bar, foo, foo, baz, bar];

frequencies(aNumbers).forEach((val, key) => console.log(key + ': ' + val));
frequencies(aObjects).forEach((val, key) => console.log(key.value + ': ' + val));


你是否有一个改进的答案,适用于对象数组?我试图修改它以适用于对象数组,但遇到了麻烦。在这种情况下,您只需创建一个新的数组/映射/集合,其中删除重复项,并为对象添加一个名为“duplicatedCount:value”的新值。我已经成功地从此答案https://dev59.com/U3E95IYBdhLWcg3wp_kg#36744732中删除了嵌套对象数组中的重复项。 - sharon gur
Set 使用对象引用来保证唯一性,并没有提供比较“相似”对象的 API。如果你想在这种任务中使用这种方法,你需要一些中间的归约函数来保证实例数组的唯一性。虽然不是最高效的方法,但我在这里快速地组合了一个示例 - Emissary
谢谢你的回答!但我实际上是用了另一种方法解决了它。如果你能看到我在这里添加的答案http://stackoverflow.com/a/43211561/4474900,我给出了我所做的示例。它很好地工作了,我的情况需要比较一个复杂的对象。不过我不知道我的解决方案的效率如何。 - sharon gur
13
这个可能使用了新的好的数据结构,但时间复杂度为 O(),而这里有很多简单的算法可以用 O(n) 的时间复杂度解决。 - raphinesse

57

我认为这是在数组中计算相同值出现次数的最简单方法。

var a = [true, false, false, false];
a.filter(function(value){
    return value === false;
}).length

13
使用新的JavaScript语法,可以将 a.filter(value => !value).length 重写为:a.filter(!).length - t3chb0t
6
未回答问题。 - Ry-

38

const data = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4]

function count(arr) {
  return arr.reduce((prev, curr) => (prev[curr] = ++prev[curr] || 1, prev), {})
}

console.log(count(data))


6
有人可以解释一下这个代码片段吗?(prev[curr] = ++prev[curr] || 1, prev)这段代码的作用是将一个对象中指定键(curr)的值加1,并将更新后的对象(prev)返回。如果该键不存在,则设置值为1。 - Souljacker
8
逗号运算符会“从左到右依次计算每个操作数并返回最后一个操作数的值”,因此它会将prev[curr]的值加1(如果它不存在则初始化为1),然后返回prev。 - ChrisV
但是输出是一个数组吗? - Francesco

36

2021版

更加优雅的方法是使用具有O(n)时间复杂度的逻辑空值赋值运算符 (x ??= y)Array#reduce()结合使用。

主要思路仍然是使用Array#reduce()进行聚合,输出为object,以实现在搜索构建大量中间数组等方面获得最高性能(时间和空间复杂性),就像其他答案一样。

const arr = [2, 2, 2, 2, 2, 4, 5, 5, 5, 9];
const result = arr.reduce((acc, curr) => {
  acc[curr] ??= {[curr]: 0};
  acc[curr][curr]++;
  
  return acc;
}, {});

console.log(Object.values(result));

清理 & 重构代码

使用 逗号运算符 (,) 语法。

逗号运算符 (,) 从左到右依次评估其每个操作数,并返回最后一个操作数的值

const arr = [2, 2, 2, 2, 2, 4, 5, 5, 5, 9];
const result = arr.reduce((acc, curr) => (acc[curr] = (acc[curr] || 0) + 1, acc), {});
console.log(result);

输出

{
  "2": 5,
  "4": 1,
  "5": 3,
  "9": 1
}

我怎样才能从这个对象中获取最高的计数? - felixo
请问您能否提供您期望的结果?@kontenurban - Nguyễn Văn Phong
2
这是一个非常好的回答。你可以通过以下方式使它更简洁: const result = arr.reduce((acc, curr) => (acc[curr] = -~(acc[curr]), acc), {}); 请参见https://dev59.com/emMl5IYBdhLWcg3wK0aA#47546846进行解释。 - Yoni Rabinovitch
谢谢@YoniRabinovitch。你的答案中的位运算看起来也很优雅简洁。 - Nguyễn Văn Phong

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接