如何计算数组中特定元素的数量?

304

我有一个数组:

[1, 2, 3, 5, 2, 8, 9, 2]

我想知道数组中有多少个2

在JavaScript中,如何不使用for循环以最简洁的方式实现?

27个回答

492

[这个答案有点过时:请阅读编辑,因为在JavaScript中“相等”的概念是模棱两可的]

向你的朋友们打个招呼: mapfilterreduceforEachevery等等。

(我只偶尔在JavaScript中编写for循环,因为缺少块级作用域,所以如果需要捕获或克隆迭代索引或值,则必须将函数用作循环的主体。通常情况下,for循环更有效率,但有时需要一个闭包。)

最易读的方法:

[....].filter(x => x==2).length

我们也可以写成.filter(function(x){return x==2}).length

以下方法更加节省空间(是O(1)而不是O(N)),但是我不确定在时间上会有多大的好处/惩罚(因为你只访问每个元素一次,所以不会超过一个常数因子):

[....].reduce((total,x) => (x==2 ? total+1 : total), 0)

或者像一位评论者友好地指出的那样:

[....].reduce((total,x) => total+(x==2), 0)

如果您需要优化这段代码,对于某些浏览器来说使用for循环可能更快... 您可以在jsperf.com上测试一下。


然后您可以将其改写为一个原型函数,让它更加优雅:

[1, 2, 3, 5, 2, 8, 9, 2].count(2)

像这样:

Object.defineProperties(Array.prototype, {
    count: {
        value: function(value) {
            return this.filter(x => x==value).length;
        }
    }
});

除了其他答案中提到的常规for循环技术,你还可以将其放置在上述属性定义内部(同样,这可能会更快)。


2017年修订版:

糟糕,这个答案比正确答案更受欢迎。 实际上,只需使用被接受的答案即可。虽然这个答案可能很聪明,但js编译器可能无法优化这种情况(或者不能由于规范)。所以您应该真的写一个简单的for循环:

Object.defineProperties(Array.prototype, {
    count: {
        value: function(query) {
            /* 
               Counts number of occurrences of query in array, an integer >= 0 
               Uses the javascript == notion of equality.
            */
            var count = 0;
            for(let i=0; i<this.length; i++)
                if (this[i]==query)
                    count++;
            return count;
        }
    }
});
你可以定义一个名为.countStrictEq(...)的版本,它使用===相等性的概念。相等性的概念可能对你正在做的事情很重要!(例如[1,10,3,'10'].count(10)==2,因为在Javascript中像'4'==4...因此称其为.countEq.countNonstrict会强调它使用==运算符)。
注意: 谨慎地在原型上定义常见名称。如果你控制你的代码,那么这是可以的,但是如果每个人都想声明自己的[].count函数,特别是如果它们的行为不同,那么就会有问题。你可能会问自己“但是.count(query)听起来很完美和经典”...但是考虑一下你是否可以做类似于[].count(x=> someExpr of x)的事情。在这种情况下,你可以定义像countIn(query, container)这样的函数(在myModuleName.countIn下),或者像[].myModuleName_count()这样的东西。
还要考虑使用自己的多集合数据结构(例如Python的'collections.Counter'),以避免首先进行计数。这适用于形式为[].filter(x=> x==???)。length的精确匹配(最坏情况下O(N)变成O(1)),并且修改后将加快形式为[].filter(filterFunction).length的查询速度(大约是#total/#duplicates的因子)。
class Multiset extends Map {
    constructor(...args) {
        super(...args);
    }
    add(elem) {
        if (!this.has(elem))
            this.set(elem, 1);
        else
            this.set(elem, this.get(elem)+1);
    }
    remove(elem) {
        var count = this.has(elem) ? this.get(elem) : 0;
        if (count>1) {
            this.set(elem, count-1);
        } else if (count==1) {
            this.delete(elem);
        } else if (count==0)
            throw `tried to remove element ${elem} of type ${typeof elem} from Multiset, but does not exist in Multiset (count is 0 and cannot go negative)`;
            // alternatively do nothing {}
    }
}

演示:

> counts = new Multiset([['a',1],['b',3]])
Map(2) {"a" => 1, "b" => 3}

> counts.add('c')
> counts
Map(3) {"a" => 1, "b" => 3, "c" => 1}

> counts.remove('a')
> counts
Map(2) {"b" => 3, "c" => 1}

> counts.remove('a')
Uncaught tried to remove element a of type string from Multiset, but does not exist in Multiset (count is 0 and cannot go negative)

旁注:虽然,如果你仍然希望使用函数式编程的方式(或者一个不需要覆盖Array.prototype的一行代码),你现在可以更简洁地写成 [...].filter(x => x==2).length。 如果你关心性能,请注意,虽然这与for循环具有渐进相同的性能(O(N)时间),但它可能需要额外的O(N)内存(而不是O(1)内存),因为它几乎肯定会生成一个中间数组,然后计算该中间数组的元素个数。


1
这是一个很好的FP解决方案,唯一的“问题”(对大多数情况不相关)是它创建了一个中间数组。 - tokland
1
如果这是一个问题,你可以使用 array.reduce(function(total,x){return x==value? : total+1 : total}, 0) - ninjagecko
1
@ninjagecko 三元运算符中不应该只有一个冒号吗?[...].reduce(function(total,x){return x==2 ? total+1 : total}, 0) - indigo
3
@tokland也许该过滤器不会创建中间数组。一个好的优化编译器可以轻松识别到只使用了数组的长度。也许当前的JS编译器都不够聪明,但这并不重要。正如Fowler所说,“如果某事伤害了,那就多做这件事”。通过编写糟糕的代码来避免编译器缺陷是短视的。如果编译器不好,就修复编译器。https://mlafeldt.github.io/blog/if-it-hurts-do-it-more-often/ - John Henckel
1
我认为这是一个最佳的2017年解决方案: const count = (list) => list.filter((x) => x == 2).length。然后,通过调用 count(list) 使用它,其中列表是数字数组。您还可以执行 const count = (list) => list.filter((x) => x.someProp === 'crazyValue').length 来计算包含在对象数组中的crazyValue实例数。请注意,它必须与属性完全匹配。 - agm1984
显示剩余2条评论

147

现代 JavaScript:

请注意,在JavaScript(JS)中进行比较时,应始终使用三个等号 === 。 三等号确保JS比较的行为类似于其他语言中的双等号 == (有一个例外,请参见下文)。以下解决方案展示了如何以函数式的方式解决这个问题,这将确保您永远不会出现越界错误

// Let has local scope
let array = [1, 2, 3, 5, 2, 8, 9, 2]

// Functional filter with an Arrow function
// Filter all elements equal to 2 and return the length (count)
array.filter(x => x === 2).length  // -> 3

下面是JavaScript中的匿名箭头函数(lambda函数):

(x) => {
   const k = 2
   return k * x
}

可以将其简化为以下针对单个输入的简洁形式:

x => 2 * x

在JS中,如果要进行比较,请始终使用三个等号:===,但当检查是否为空时除外:if (something == null) {},因为在这种情况下仅使用双等号包括对undefined的检查。

其中return是隐含的。


过滤函数是否比使用ES6的for of循环更高效? - Niklas
1
@Niklas,我认为它们是相同的(因为两者都必须检查所有元素,O(N)),但我猜这取决于浏览器和元素数量以及计算机适合缓存内存。所以,我想答案是:“这很复杂” :) - Sverrisson
我猜reduce比filter更有效率,因为使用filter会创建一个全新的数组,仅仅是为了获取它的长度。 - Kip
@Kip 这是关于可读性的问题,过滤器清晰地告诉每个阅读代码的人它的意图。我不会假设任何关于效率的事情,因为编译器可能会在看到结果仅用于计数时进行优化。在我看来,过早地进行优化比可读性不重要。 - Sverrisson
@Sverrisson 这正是reduce旨在解决的问题类型。即使reduce更难阅读(我并不完全同意),但将“//计算等于2的元素数量”放在前一行会使得filter或reduce更易读。 - Kip
@Kip 我同意这可以用 reduce 解决,下面的答案之一就是这样做的。但当我回答这个问题时,我考虑到了正在学习 JavaScript 的人,并因此选择了 filter 函数。 - Sverrisson

119

非常简单:

var count = 0;
for(var i = 0; i < array.length; ++i){
    if(array[i] == 2)
        count++;
}

82
没错,我的意思是不使用“for”循环来实现。 - Leem
19
为什么循环不好?总有某个时刻需要循环。显然,您会创建一个隐藏循环的函数。这些“我不想使用适合工作的正确工具”请求对我来说从未太有意义。我们可以争论什么最优雅。例如,对我来说,每个元素都要调用一次函数并将其与值进行比较并不优雅。 - Felix Kling
58
楼主可能认为循环不好,因为它需要5行代码并需要可变状态。以后来阅读的开发人员将不得不花费一些时间来检查它的功能,并从他们的任务中失去关注。抽象方法要好得多:const count = countItems(array, 2);,实现细节可以在内部进行讨论。 - joeytwiddle
4
这不是正确的答案,因为问题明确要求不使用循环。请检查我的解决方案,它不使用任何循环。https://dev59.com/h2025IYBdhLWcg3wQDhb#44743436 - Luis Orantes
3
JS .reduce() 仍然使用循环。唯一的区别是循环隐藏在 reduce 源代码中,而不是你自己的函数中。 - Kyle Alm
显示剩余2条评论

81

2017年:如果有人仍然对这个问题感兴趣,我的解决方案如下:

const arrayToCount = [1, 2, 3, 5, 2, 8, 9, 2];
const result = arrayToCount.filter(i => i === 2).length;
console.log('number of the found elements: ' + result);


19

这里是一种 ES2017+ 的方法,以 O(N) 的时间复杂度获取所有数组项的计数:

const arr = [1, 2, 3, 5, 2, 8, 9, 2];
const counts = {};

arr.forEach((el) => {
  counts[el] = counts[el] ? (counts[el] + 1) : 1;
});

您还可以选择对输出进行排序:

const countsSorted = Object.entries(counts).sort(([_, a], [__, b]) => a - b);

console.log(countsSorted) 对于你的示例数组:

[
  [ '2', 3 ],
  [ '1', 1 ],
  [ '3', 1 ],
  [ '5', 1 ],
  [ '8', 1 ],
  [ '9', 1 ]
]

counts.hasOwnProperty(el) is safer than counts[el] because, while not applicable here, zero is treated as false, so if counts[el] = 0, then counts[el] == false - Edwarric

13

如果您正在使用lodash或underscore,_.countBy方法将提供一个对象,其中包含按数组中每个值为键的聚合总数。如果您只需要计算一个值,可以将其转换为一行代码:

_.countBy(['foo', 'foo', 'bar'])['foo']; // 2

这个方法同样适用于数字数组。你的示例可以采用以下一行代码:

_.countBy([1, 2, 3, 5, 2, 8, 9, 2])[2]; // 3

8
过度杀伤。因为它会为所有独特元素创建计数器。浪费存储空间和时间。 - metalim
2
请记住,Lodash 总是会增加您的捆绑包大小,通常相当显著。对于像计算数组项这样简单的事情,我不会使用 Lodash。 - Yanick J. Steinbeck

5
我能用最奇怪的方式来完成这个操作:
(a.length-(' '+a.join(' ')+' ').split(' '+n+' ').join(' ').match(/ /g).length)+1

在这里:

  • a 是数组
  • n 是要在数组中计数的数字

我的建议是使用 while 或 for 循环 ;-)


4

不使用循环通常意味着将过程交给某个使用循环的方法。

这里有一种方式,我们讨厌循环的程序员可以满足他的厌恶,但需要付出代价:

var a=[1, 2, 3, 5, 2, 8, 9, 2];

alert(String(a).replace(/[^2]+/g,'').length);


/*  returned value: (Number)
3
*/

你还可以重复调用 indexOf 方法,如果它作为数组方法可用,则每次移动搜索指针。这不会创建新的数组,并且循环比 forEach 或 filter 快。如果你需要查找一百万个成员,这可能会有所不同。
function countItems(arr, what){
    var count= 0, i;
    while((i= arr.indexOf(what, i))!= -1){
        ++count;
        ++i;
    }
    return count
}

countItems(a,2)

/*  returned value: (Number)
3
*/

2
你可以将你的正则表达式简化为 String(a).match(/2/g).length + 1 -- 不过要注意,这种实现方式可能无法处理双位数字。 - Gary Green
3
[2, 22, 2] 怎么样? - Oduvan

3
真的,为什么你需要使用map或者filter呢? reduce 就是为了这些操作而生的: [1, 2, 3, 5, 2, 8, 9, 2].reduce( (count, val)=>count+(item==val), 0); 就是这样!(如果在每次迭代中item==val,那么1将会被添加到累加器count中,因为true将解析为1)。
作为一个函数:
function countInArray(arr, val) {
   return arr.reduce((count,item)=>count+(item==val),0)
}

或者,继续扩展您的数组:
Array.prototype.count = function(val) {
   return this.reduce((count,item)=>count+(item==val),0)
}

3

我是js数组reduce函数的粉丝。

const myArray =[1, 2, 3, 5, 2, 8, 9, 2];
const count = myArray.reduce((count, num) => num === 2 ? count + 1 : count, 0)

事实上,如果您想要更加高级的操作,您可以在数组原型上创建一个计数函数,以便于重用。

Array.prototype.count = function(filterMethod) {
  return this.reduce((count, item) => filterMethod(item)? count + 1 : count, 0);
} 

那么,请执行下列操作。
const myArray =[1, 2, 3, 5, 2, 8, 9, 2]
const count = myArray.count(x => x==2)

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