【问题标题】:Finding the count of elements in an array fastly快速查找数组中元素的数量
【发布时间】:2012-03-22 16:53:18
【问题描述】:

我有一个数组,大小可以达到 10000。它只包含 1/2/3/4。我需要找出数组中有多少个 1、2、3 和 4。最快的方法是什么?我使用的语言是 Java。我的一段代码-

for(int i=0; i<myArray.length;i++){
            int element = myArray[i];
            if(element == 1){
                onesCount++;
            }
            else if(element == 2){
                twosCount++;
            }
            else if(element == 3){
                threesCount++;
            }
            else
                foursCount++;
}

我希望有一个好的解决方案。

【问题讨论】:

  • 您想要快速的方法,还是最快的方法? :)
  • 因为无论如何你都要解析整个数组,所以无论你怎么做,你的运行时间都必须是O(n)
  • 您可以通过线程来实现类似的操作,其中每个线程可以计算数组中特定范围元素的出现次数,线程 1 计算前 2000 个元素的出现次数,依此类推。
  • @sans481,您看到您的解决方案似乎非常好。实际上有一些微优化是可能的(当然值得一试)。既然我们都在谈论它们,我想指出,您可以使用前缀增量(或分别为 decr)来“加速”您的循环;)see this quesion

标签: java arrays performance search


【解决方案1】:
int count[5]; //initialize this to 0
for(int i = 0; i<n; i++)
{
count[array[i]]+=1;
}

【讨论】:

  • 虽然还是O(n),但是这个可以省下几个比较。
  • OTOH,它现在必须更新堆上的数组而不是堆栈上的本地 int。
  • 这可能比我的好。我认为这样做不安全会赢。
  • 呃。刚刚意识到这是Java。所以这里没有不安全,抱歉。
  • @noMAD ++count[array[i]];根据stackoverflow.com/questions/561588/… 会更快。不会吧?
【解决方案2】:

基本上没有比您的解决方案更好的解决方案了。可以更灵活,但不会更快。一切都必须至少一次通过整个数组。

性能优化的唯一领域是避免执行此操作,例如在数组更新时跟踪计数器。是否值得麻烦(可能不值得)取决于您需要执行此操作的频率、数组有多大以及您还需要用它做什么。

【讨论】:

    【解决方案3】:

    您将拥有用于数组条目的单独计数器。每一个都会在找到一个新的匹配数字时递增,因此您必须至少访问每个索引一次,也就是说您将有一个算法在 O(n) 时间内工作。可以首选 Switch 语句,而不是多个 if-else 语句:

    int[] array = new int[10000];
    // ... populate array
    int[] counters = new int[4];
    
    for (int i = 0; i < array.length; i++)
    {
        int temp = array[i];
        switch (temp) {
        case 1:
            counters[0]++;
            break;
        case 2:
            counters[1]++;
            break;
        case 3:
            counters[2]++;
            break;
        case 4:
            counters[3]++;
            break;
        default:
            // to do.
        }
    }
    

    【讨论】:

    • 糟糕!我忘记了开关盒。它可能会消除大量的比较。但是为什么是计数器数组而不是计数变量(就像我的问题一样)。有什么原因吗?
    • 没有具体原因,但它更灵活。如果要打印所有计数器,只需使用 for 循环遍历 counters 数组。那不是很好吗?
    【解决方案4】:

    如果您使用的是 Java 7,则可以使用 Fork/Join Framework。 复杂度仍然是 O(n)... 但对于大型数组可能会更快

    【讨论】:

    • +1。完全不确定这在这种情况下是否有意义,但总的来说,这是并行处理的完美任务。
    • @Thilo,是的,线程创建的开销对于这个操作来说可能太大了,但我认为值得一试
    【解决方案5】:

    您可以使用一次遍历数组来做到这一点。

    您只需拥有一个由四个元素组成的数组,每个元素代表您的一个值 (1/2/3/4),对于原始数组中的每个元素,您可以在“count”数组中的相应位置增加计数。 这将使它成为 O(n)。

    【讨论】:

      【解决方案6】:

      如果您可以改用 Map 或自定义类。如果不能,则必须遍历整个数组。如果您现在没有遇到性能问题,我建议您只进行迭代。

      【讨论】:

        【解决方案7】:

        如果 switch 版本(deporter)或 noMAD 的版本不是最好的,试试这个:

        for (int i = 0; i < myArray.length; i++) {
            int element = myArray[i];
            if (element > 2) {
                if (element == 4) {
                    foursCount++;
                } else 
                    threesCount++;
            } else {
                if (element == 2) 
                    twosCount++;
                else 
                    onesCount++; 
            }
        }
        

        它可能会节省少量比较。但这取决于真实数据。如果您对数据很幸运,那么简单的版本可能会更好。

        除此之外,对于大数据,使用并行性总是值得一试的。

        【讨论】:

          【解决方案8】:

          我认为你不会比这更快:

          final int[] counts = new int[5];
          final int length = array.length - 1;
          for (int i = 0; i < length; i++) {
             counts[array[i]]++;
          }
          

          请注意,在 for 循环中,array.length 没有被引用。它被放入一个本地 final int 中,这样可以避免在每次迭代中取消引用 array.length。

          我对 this 与 this 使用 switch..case 语句且仅使用本地堆栈变量的方法进行了基准测试:

              int count1 = 0;
              int count2 = 0;
              int count3 = 0;
              int count4 = 0;
          
              for (int i = array.length - 1; i >= 0; i--) {
                  switch (array[i]) {
                  case 1:
                      count1++;
                      break;
                  case 2:
                      count2++;
                      break;
                  case 3:
                      count3++;
                      break;
                  case 4:
                      count4++;
                      break;
                  }
              }
          

          结果是第一种方法耗时 17300 纳秒,switch..case 方法耗时 79800 纳秒。 [更新:忘记将纳秒除以 10。我将每种方法运行了 10 次。]

          注意:我确实在基准测试之前先警告了 VM。

          【讨论】:

          • 你是如何测量的?发布构建? JIT 优化?附上调试器?数据大小?重复?缓存感知?你能发布一些代码吗?充其量:两个版本的汇编?
          • 我使用“-server”运行虚拟机,并通过对这两种方法的 11000 次调用对其进行预热(服务器 JIT 热点编译阈值为 10000 次迭代)。然后在调用这两个方法之前和之后使用 System.nanoTime() 10 次。是的,用调试编译。不,没有附加调试器。 10000 个整数的数据大小。
          • 运行 javap 来转储这两种方法的字节码是留给读者的练习。可以说第一种方法是 32 行字节码,第二种方法是 105 行。第二种方法使用 'tableswitch' 字节码以及大量的 'goto' 跳转,最终效率要低得多(证明由基准)。
          • 反向访问大量内存通常比按顺序访问要慢,因为缓存可以预取您需要的数据。
          • @Peter Lawrey,确实如此。我更改了我的代码,它比平均值减少了 300ns(现在测量为 17300ns)。
          猜你喜欢
          • 2016-09-25
          • 2021-08-04
          • 2013-12-26
          • 1970-01-01
          • 2012-05-07
          • 1970-01-01
          • 2011-04-07
          • 2014-07-20
          • 2015-03-01
          相关资源
          最近更新 更多