即时计算百分位数

时间:2010-10-19 06:57:23

标签: java algorithm statistics

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

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

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

我应该使用哪种算法?我想优化计算速度,因为它不是那些100毫秒计算的唯一东西。

8 个答案:

答案 0 :(得分:10)

出于实际原因和n的合理值,您最好使用原始int ring-buffer (以跟踪最早的条目),以及线性扫描,用于确定有多少值小于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 ms,远低于100 ms)。

答案 1 :(得分:6)

您可以保留一个包含180个数字的数组,并将索引保​​存到最旧的数字中,以便在新数字出现时覆盖最旧索引处的数字,并将索引模数增加180(它是比这更复杂,因为你需要前180个数字的特殊行为。)

至于计算有多少个数字,我会使用强力方式(迭代所有数字和计数)。


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

答案 2 :(得分:3)

将您的号码添加到列表中。如果尺寸> 180,删除第一个数字。 计数只是迭代180个元素,这可能足够快。明智的表现很难打败。

答案 3 :(得分:1)

您可以使用LinkedList实现。

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

答案 4 :(得分:1)

您可以尝试自定义链接列表数据结构,其中每个节点都维护next / prev以及排序的next / prev引用。然后插入变为两阶段过程,首先始终在尾部插入节点,插入排序和插入排序将返回小于x的数字的计数。删除只是删除头部。

这是一个例子,注意:这是非常NASTY JAVA,这是示例代码,可以自觉地展示IDEA。你明白了! ;)另外,我只是添加了几个项目,但是它应该让你知道它是如何工作的......最糟糕的情况是通过排序链表完全迭代 - 这并不比例子更糟糕以上我猜?

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();    
  }
}

答案 5 :(得分:0)

让缓存成为一个列表,这样你就可以在开始时插入并让最旧的结束并删除。

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

答案 6 :(得分:0)

答案 7 :(得分:0)

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

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