【问题标题】:How to group by count in array without using loop如何在不使用循环的情况下按数组中的计数分组
【发布时间】:2011-07-25 03:38:25
【问题描述】:
arr = [1,2,1,3,5,2,4]

如何通过排序按组值对数组进行计数?我需要以下输出:

x[1] = 2  
x[2] = 2  
x[3] = 1  
x[4] = 1  
x[5] = 1

【问题讨论】:

标签: ruby


【解决方案1】:
x = arr.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }

【讨论】:

  • 非常感谢 michael 和 Terw。我喜欢这个很短的。但是,能否请您简要解释一下上述短线。 :)。
  • inject 将一个累加器“注入”到一个 Enumerable 中,在我们的例子中它是一个默认值为 0 的 Hash。在每次迭代中,我们使用当前元素的键 (e) 将值加一。最后我们返回累加器。 ruby-doc.org/core/classes/Enumerable.html#M001494
  • “inject”操作在函数式编程语言中常被称为“fold”,我觉得这个名字更直观。
  • 但是该代码不会对哈希进行排序。所以最后它需要更多:Hash[#code here#.sort] 甚至 sort_by
  • 在构建哈希而不是算术时,更喜欢 .each_with_object 而不是 inject。请参阅下面@sawa 的答案。
【解决方案2】:

我相信有更好的方法,

>> arr.sort.group_by {|x|x}.each{|x,y| print "#{x} #{y.size}\n"}
1 2
2 2
3 1
4 1
5 1

根据需要将 x 和 y 值分配给哈希。

【讨论】:

  • sort 不必在group_by 之前。 arr.group_by {...} 会做同样的事情
  • @user102008 OP暗示结果将按顺序呈现。不是[2,1].group_by {|x|x} #=> {2=>[2], 1=>[1]}kurumi,有什么更好的方法?
  • @CarySwoveland: group_by 返回一个没有订单的HashHash 中的条目的迭代顺序是不可预测的。
  • @user102008, group_by preserves order,至少在 MRI 1.9+ 中。 AFAIK,它没有记录,但应该是,因为它是规范的一部分。
  • @CarySwoveland:每个键对应的值都有顺序;如果你关心的话,排序可能是相关的。但是键之间没有顺序,因为返回值是Hash。所以它与[2,1].group_by {|x|x} #=> {2=>[2], 1=>[1]} 没有任何关系
【解决方案3】:
x = Hash[arr.uniq.map{ |i| [i, arr.count(i)] }]

最新的 Ruby 有 to_h 方法:

x = arr.uniq.map{ |i| [i, arr.count(i)] }.to_h

【讨论】:

  • Michael Kohl 打败了我,但他的代码应该更快。这段代码大约需要两倍的时间
  • @fl00r..这很有趣..我认为这会更慢,因为它循环然后再次在数组上使用count 方法。也许使用内置方法有其优势。 :)
  • @fl00r:真的吗?我最初有一个使用count 的版本,但认为它不能很好地适应数组长度,所以用我当前的答案替换它。你能用更大的数组运行你的基准测试并再次比较吗?
  • 并非如此。我错了。就O(n2) 而言,它在小型阵列的基准测试中更快,但在大型阵列中速度会非常慢。我的错是我在百万循环台上测试现有阵列 - 所以它快了 20%。
  • @fl00r..yeah..这对于较大的数组来说肯定会很慢。
【解决方案4】:

应该这样做

arr = [1,2,1,3,5,2,4]

puts arr.inject(Hash.new(0)) {|h, v| h[v] += 1; h}
#=> {1=>2, 2=>2, 3=>1, 5=>1, 4=>1}

【讨论】:

    【解决方案5】:

    仅在 ruby​​ 1.9 下可用

    Michael's answer基本相同,只是方式略短:

    x = arr.each_with_object(Hash.new(0)) {|e, h| h[e] += 1}
    

    在类似的情况下,

    • 当起始元素是一个可变对象,例如ArrayHashString,您可以使用each_with_object,如上例所示。
    • 当起始元素是Numeric不可变对象时,您必须使用inject,如下所示。

      sum = (1..10).inject(0) {|sum, n| sum + n} # => 55

    【讨论】:

    • 就字符而言,它更长。就令牌而言,它更短。感谢您的评论。
    • 谢谢@sawa。绝对它非常短和更快。因为,我的实际数组是可变格式的,它包含大量数据。再次感谢。
    • 虽然我注意到这种方法的值不像答案所说的那样按排序顺序。
    • 这是最干净的答案。已添加each_with_object 以避免h[e] += 1 ; h
    【解决方案6】:
    arr = [1,2,1,3,5,2,4]
    r = {}
    arr.each { |e| r[e] = arr.count(e) if r[e].nil?}
    

    输出

    p r
    #==> {1=>2, 2=>2, 3=>1, 5=>1, 4=>1}
    

    【讨论】:

      【解决方案7】:

      每当你发现有人断言某事在这种类型的原始程序中是最快的,我总是觉得确认这一点很有趣,因为没有确认我们大多数人实际上只是在猜测。所以我把这里所有的方法都拿来做基准测试。

      我从需要按计数分组的网页中提取了一组 120 个链接,并使用 seconds = Benchmark.realtime do 循环实现了所有这些链接,并且得到了所有时间。

      假设 links 是我需要计算的数组的名称:

      #0.00077
      seconds = Benchmark.realtime do
        counted_links = {}
        links.each { |e| counted_links[e] = links.count(e) if counted_links[e].nil?}
      end
      seconds
      
      #0.000232
      seconds = Benchmark.realtime do
        counted_links = {}
        links.sort.group_by {|x|x}.each{|x,y| counted_links[x] = y.size}
      end
      
      #0.00076
      seconds = Benchmark.realtime do 
        Hash[links.uniq.map{ |i| [i, links.count(i)] }]
      end
      
      #0.000107 
      seconds = Benchmark.realtime do 
        links.inject(Hash.new(0)) {|h, v| h[v] += 1; h}
      end
      
      #0.000109
      seconds = Benchmark.realtime do 
        links.each_with_object(Hash.new(0)) {|e, h| h[e] += 1}
      end
      
      #0.000143
      seconds = Benchmark.realtime do 
        links.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }
      end
      

      然后一点点红宝石找出答案:

      times = [0.00077, 0.000232, 0.00076, 0.000107, 0.000109, 0.000143].min
      ==> 0.000107
      

      所以实际最快的方法当然是 ymmv:

      links.inject(Hash.new(0)) {|h, v| h[v] += 1; h}
      

      【讨论】:

      • 我认为“逻辑上的飞跃”符合您的结论。 :-)
      • 感谢基准测试,这帮助我选择了最快的选项,这正是我感兴趣的。
      【解决方案8】:

      为了记录,我最近读到了Object#taphere。我的解决方案是:

      Hash.new(0).tap{|h| arr.each{|i| h[i] += 1}}

      #tap 方法将调用者传递给块,然后返回它。当您必须增量构建数组/哈希时,这非常方便。

      【讨论】:

        【解决方案9】:

        另一种 - 与其他方法相似 - 方法:

        result=Hash[arr.group_by{|x|x}.map{|k,v| [k,v.size]}]
        
        1. 按每个元素的值分组。
        2. 将分组映射到 [value, counter] 对数组。
        3. 将 paris 数组转换为 Hash 中的键值,即可通过 result[1]=2 ... 访问。

        【讨论】:

        • 比公认的答案更简洁易懂。
        【解决方案10】:

        ruby 2.7 => Enumerable#tally 中有一个简短的版本。

        [1,2,1,3,5,2,4].tally  #=> { 1=>2, 2=>2, 3=>1, 5=>1, 4=>1 }
        
        # Other possible usage
        
        (1..6).tally { |i| i%3 }   #=> { 0=>2, 1=>2, 2=>2 }
        
        

        【讨论】:

        【解决方案11】:
        arr.group_by(&:itself).transform_values(&:size)
        #=> {1=>2, 2=>2, 3=>1, 5=>1, 4=>1}
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-04-26
          • 2023-02-05
          • 2018-06-02
          • 2020-11-07
          • 2021-01-29
          • 1970-01-01
          • 1970-01-01
          • 2015-03-06
          相关资源
          最近更新 更多