【问题标题】:Calculating Percentiles on the fly即时计算百分位数
【发布时间】:2011-04-27 07:55:36
【问题描述】:

我正在用 Java 编程。每 100 毫秒,我的程序就会获得一个新数字。

它有一个缓存,其中包含最后一个n = 180 号码的历史记录。 当我得到一个新数字x 时,我想计算缓存中有多少小于x 的数字。 之后我想删除缓存中最旧的号码。

每 100 毫秒我想重复计算有多少较小数字的过程并删除最旧的数字。

我应该使用哪种算法?我想优化以加快计算速度,因为这不是唯一在这 100 毫秒上计算的东西。

【问题讨论】:

    标签: java algorithm statistics


    【解决方案1】:

    180 个值并不多,而且是一个简单的数组,暴力搜索和 System.arraycopy() 应该快于 1 微秒(1/1000 毫秒)并且不会导致 GC。玩更复杂的集合可能会更快。

    我建议你保持简单,并在假设你需要优化之前测量 ti 需要多长时间。

    【讨论】:

      【解决方案2】:

      【讨论】:

      • 据我所知,这个类没有忘记最旧值的功能。
      • 在 DescriptiveStatistics 类中,您可以设置“窗口大小”。 addValue() 方法的 Javadoc:将值添加到数据集。如果数据集处于最大大小(即存储元素的数量等于当前配置的 windowSize),则丢弃数据集中的第一个(最旧的)元素以为新值腾出空间。 commons.apache.org/math/apidocs/src-html/org/apache/commons/…
      【解决方案3】:

      您可以尝试自定义链表数据结构,其中每个节点都维护下一个/上一个以及排序的下一个/上一个引用。那么插入就变成了一个两阶段的过程,首先总是在尾部插入节点,然后是插入排序,插入排序会返回小于x的数字个数。删除只是简单地删除头部。

      这是一个示例,注意:这是 非常讨厌 JAVA,它是纯粹展示这个想法的示例代码。你明白了! ;) 另外,我只添加了一些项目,但它应该让您了解它是如何工作的......最糟糕的情况是通过排序链表进行完整迭代 - 这并不比示例差上面我猜?

      import java.util.*;
      
      class SortedLinkedList {
      
        public static class SortedLL<T>
        {
          public class SortedNode<T>
          {
            public SortedNode(T value)
            {
              _value = value;
            }
      
            T _value;
      
            SortedNode<T> prev;
            SortedNode<T> next;
      
            SortedNode<T> sortedPrev;
            SortedNode<T> sortedNext;
          }
      
          public SortedLL(Comparator comp)
          {
            _comp = comp;
            _head = new SortedNode<T>(null);
            _tail = new SortedNode<T>(null);
            // Setup the pointers
            _head.next = _tail;
            _tail.prev = _head;
            _head.sortedNext = _tail;
            _tail.sortedPrev = _head;
            _sortedHead = _head;
            _sortedTail = _tail;      
          }
      
          int insert(T value)
          {
            SortedNode<T> nn = new SortedNode<T>(value);
      
            // always add node at end
            nn.prev = _tail.prev;
            nn.prev.next = nn;
            nn.next = _tail;
            _tail.prev = nn;
      
            // now second insert sort through..
            int count = 0;
            SortedNode<T> ptr = _sortedHead.sortedNext;
            while(ptr.sortedNext != null)
            {
              if (_comp.compare(ptr._value, nn._value) >= 0)
              {
                break;
              }
              ++count;
              ptr = ptr.sortedNext;
            }  
      
            // update the sorted pointers..
            nn.sortedNext = ptr;
            nn.sortedPrev = ptr.sortedPrev;
            if (nn.sortedPrev != null)
              nn.sortedPrev.sortedNext = nn;
            ptr.sortedPrev = nn;
      
            return count;            
          }
      
          void trim()
          {
            // Remove from the head...
            if (_head.next != _tail)
            {
              // trim.
              SortedNode<T> tmp = _head.next;
              _head.next = tmp.next;
              _head.next.prev = _head;
      
              // Now updated the sorted list
              if (tmp.sortedPrev != null)
              {
                tmp.sortedPrev.sortedNext = tmp.sortedNext;
              }
              if (tmp.sortedNext != null)
              {
                tmp.sortedNext.sortedPrev = tmp.sortedPrev;
              }
            }
          }
      
          void printList()
          {
            SortedNode<T> ptr = _head.next;
            while (ptr != _tail)
            {
              System.out.println("node: v: " + ptr._value);
              ptr = ptr.next;
            }      
          }
      
          void printSorted()
          {
            SortedNode<T> ptr = _sortedHead.sortedNext;
            while (ptr != _sortedTail)
            {
              System.out.println("sorted: v: " + ptr._value);
              ptr = ptr.sortedNext;
            }      
          }
      
          Comparator _comp;
      
          SortedNode<T> _head;
          SortedNode<T> _tail;    
      
          SortedNode<T> _sortedHead;
          SortedNode<T> _sortedTail;    
      
        }
      
        public static class IntComparator implements Comparator
        {
          public int compare(Object v1, Object v2){
            Integer iv1 = (Integer)v1;
            Integer iv2 = (Integer)v2;
            return iv1.compareTo(iv2);
          }
        }
      
      
        public static void main(String[] args){
      
          SortedLL<Integer> ll = new SortedLL<Integer>(new IntComparator());
          System.out.println("inserting: " + ll.insert(1));
          System.out.println("inserting: " + ll.insert(3));
          System.out.println("inserting: " + ll.insert(2));
          System.out.println("inserting: " + ll.insert(5));
          System.out.println("inserting: " + ll.insert(4));
          ll.printList();
          ll.printSorted();    
      
          System.out.println("inserting new value");
          System.out.println("inserting: " + ll.insert(3));
          ll.trim();
          ll.printList();
          ll.printSorted();    
        }
      }
      

      【讨论】:

        【解决方案4】:

        出于实际原因和n 的合理值,您最好使用原始ints 的环形缓冲区(以跟踪最旧的条目)和线性扫描,用于确定有多少值小于x

        为了让它出现在O(log n) 中,您必须使用Guavas TreeMultiset 之类的东西。以下是它的外观轮廓。

        class Statistics {
        
            private final static int N = 180;
            Queue<Integer> queue = new LinkedList<Integer>();
            SortedMap<Integer, Integer> counts = new TreeMap<Integer, Integer>();
        
            public int insertAndGetSmallerCount(int x) {
        
                queue.add(x);                                // O(1)
                counts.put(x, getCount(x) + 1);              // O(log N)
        
                int lessCount = 0;                           // O(N), unfortunately
                for (int i : counts.headMap(x).values())     // use Guavas TreeMultiset
                    lessCount += i;                          // for O(log n)
        
                if (queue.size() > N) {                      // O(1)
                    int oldest = queue.remove();             // O(1)
                    int newCount = getCount(oldest) - 1;     // O(log N)
                    if (newCount == 0)
                        counts.remove(oldest);               // O(log N)
                    else
                        counts.put(oldest, newCount);        // O(log N)
                }
        
                return lessCount;
            }
        
            private int getCount(int x) {
                return counts.containsKey(x) ? counts.get(x) : 0;
            }
        
        }
        

        在我的 1.8 GHz 笔记本电脑上,此解决方案在大约 13 秒内执行 1,000,000 次迭代(即一次迭代大约需要 0.013 毫秒,远低于 100 毫秒)。

        【讨论】:

        • 由于只有 180 个数字并且每 100 毫秒才会重新计算一次,所以我肯定会优化可读性而不是速度。
        • @CodeInChaos,我认为使用列表不会更具可读性。再说了,谁说180是一成不变的? ;)
        • @Eiko,是的,尤其是图表形式,n 沿 x 轴增加 ;-)
        • 我对 180 个案例进行了一些性能测试 - 插入了 1 百万个条目:队列+树 1380 毫秒,仅队列:1300 毫秒,普通 int[] 环形缓冲区:210 毫秒。
        • 除了性能上的困难外,TreeSet 还会出现重复数字的问题。 (虽然,您当然可以使用 STL 的 multiset,但会牺牲更多性能。但不确定 multiset 是否提供排序)
        【解决方案5】:

        您可以保留一个由 180 个数字组成的数组并将索引保​​存到最旧的数字,这样当有新数字出现时,您会覆盖 最旧 索引处的数字并将索引以 180 为模递增(它是比这更复杂一些,因为您需要前 180 个数字的特殊行为)。

        至于计算有多少数字更小,我会使用蛮力方法(迭代所有数字并计数)。


        编辑:看到"optimized" version 的运行速度比这个简单的实现慢五倍,我觉得很有趣(感谢@Eiko 的分析)。我认为这是因为当你使用树和地图时,你会失去数据的局部性并且有更多的内存错误(更不用说内存分配和垃圾回收了)。

        【讨论】:

        • +1。环形缓冲区胜过 ArrayList 和 LinkedList。获得百分位数的完整迭代似乎也不错。
        • 但无论如何他的缓存应该只保存 180 (+1) 个号码。
        • @Eiko,我不明白你的意思缓存包含问题中描述的 180 个元素,+1 是参数。
        • 啊,抱歉...没有捕获环形缓冲区。我以为你想保留每个元素的年龄。
        • @Motti,关于您的编辑,我发现问题 可以O(log n) 中解决很有趣。然而不幸的是,我已经变成了无可救药的理论。我想这是在正式方法研究生学习几年后的职业伤害:-\
        【解决方案6】:

        您可以使用 LinkedList 实现。

        使用这种结构,您可以轻松地操作 List 的第一个和最后一个元素。 (addFirst, removeFirst, ...) 对于算法(找出有多少数字更小/更大),列表上的一个简单循环就足够了,并且会在 180 的元素列表上在不到 100 毫秒的时间内为您提供结果。

        【讨论】:

          【解决方案7】:

          让缓存成为一个列表,这样你就可以在开头插入,让最旧的在最后并被删除。

          然后在每次插入后扫描整个列表并计算您需要的数字。

          【讨论】:

            【解决方案8】:

            将您的号码添加到列表中。如果大小 > 180,则删除第一个数字。 计数只是迭代 180 个元素,这可能已经足够快了。在性能方面很难超越。

            【讨论】:

            • 又好又简单 :) 对于如此小的数组,O(n) 并不重要。
            猜你喜欢
            • 2011-12-29
            • 2013-06-20
            • 2016-07-28
            • 2017-08-29
            • 2012-10-28
            • 2021-02-26
            • 2013-08-31
            • 2019-02-16
            • 1970-01-01
            相关资源
            最近更新 更多