空间有限的优先级队列:寻找一个好的算法

时间:2010-05-29 04:11:51

标签: c++ algorithm queue priority-queue

这不是作业。

我正在使用一个小的“优先级队列”(目前作为数组实现)来存储具有最小值的最后N个项目。这有点慢 - O(N)项目插入时间。当前实现跟踪数组中的最大项,并丢弃任何不适合数组的项,但我仍希望进一步减少操作数。

寻找符合以下要求的优先级队列算法:

  1. 队列可以实现为数组,具有固定的大小和_cannot_ grow。严禁在任何队列操作期间进行动态内存分配。
  2. 任何不适合数组的东西都会被丢弃,但是队列会保留所有遇到过的最小元素。
  3. O(log(N))插入时间(即将元素添加到队列中应占用O(log(N)))。
  4. (可选)O(1)访问队列中最大*项目(队列存储*最小*项目,因此最大项目将首先被丢弃,我需要它们来减少操作次数)
  5. 易于实施/理解。理想情况下 - 类似于二元搜索的东西 - 一旦你理解它,你就会永远记住它。
  6. 不需要以任何方式对元素进行排序。我只需要保持N遇到的最小值。当我需要它们时,我会立刻访问它们。所以从技术上讲,它不必是一个队列,我只需要存储N个最后的最小值。

我最初考虑使用二进制堆(它们可以通过数组轻松实现),但显然当数组不能再增长时它们表现不佳。链接列表和数组将需要额外的时间来移动东西。 stl优先级队列增长并使用动态分配(但可能错误)。

那么,还有其他想法吗?

- EDIT--
我对STL实现不感兴趣。由于大量的函数调用,STL实现(由少数人建议)比当前使用的线性数组慢一点。

我对优先级队列算法感兴趣,而不是实现。

8 个答案:

答案 0 :(得分:14)

基于数组的堆似乎非常适合您的目的。我不确定你为什么拒绝他们。

您使用最大堆。

假设您有一个N元素堆(实现为数组),其中包含到目前为止看到的N个最小元素。

当一个元素进入时,你检查最大值(O(1)时间),如果它更大则拒绝。

如果输入的值较低,则将root修改为新值并对此更改值进行筛选 - 最差情况为O(log N)时间。

筛选过程很简单:从root开始,在每个步骤中将此值与其较大的子项交换,直到max-heap属性恢复为止。

因此,如果您使用std :: priority_queue,则不必执行任何您可能需要的删除。根据std :: priority_queue的实现,这可能导致内存分配/释放。

所以你可以得到如下代码:

  • 分配大小为N的数组。
  • 用你看到的前N个元素填充它。
  • heapify(您应该在标准教科书中找到它,它使用sift-down)。这是O(N)。
  • 现在你得到的任何新元素,要么在O(1)时间内拒绝它,要么在最坏情况下O(logN)时间通过筛选插入。

平均而言,您可能不必一直向下筛选新值,并且可能比O(logn)平均插入时间更好(尽管我还没有尝试过证明它)。

您只需分配大小为N的数组一次,并且通过交换数组的元素完成任何插入,因此之后没有动态内存分配。

查看包含heapify和sift-down伪代码的wiki页面:http://en.wikipedia.org/wiki/Heapsort

答案 1 :(得分:8)

std::priority_queue与头部的最大项目一起使用。对于每个新项目,如果它是头部项目>=,则丢弃它,否则弹出头部项目并插入新项目。

旁注:标准容器只有在你成长的情况下才会增长。只要在插入新项目之前删除一个项目(当然,在它达到最大大小之后),就不会发生这种情况。

答案 2 :(得分:1)

我工作的大多数优先级队列都基于链表。如果您具有预定数量的优先级,则可以通过具有链接列表数组(每个优先级一个链接列表)轻松创建具有O(1)插入的优先级队列。具有相同优先级的项目当然会退化为FIFO,但这可以被认为是可接受的。

添加和删除会变得类似(您的API可能会有所不同)......

listItemAdd (&list[priLevel], &item);      /* Add to tail */
pItem = listItemRemove (&list[priLevel]);  /* Remove from head */

获取队列中的第一项然后成为查找具有最高优先级的非空链接列表的问题。这可能是O(N),但你可以使用一些技巧加快速度。

  1. 在优先级队列结构中,将指针或索引或其他内容保存到具有当前最高优先级的链接列表中。每次在优先级队列中添加或删除项目时都需要更新。
  2. 使用位图指示哪些链接列表不为空。结合查找最重要的位,或找到最低有效位算法,您通常可以一次测试多达32个列表。同样,这需要在每次添加/删除时更新。
  3. 希望这有帮助。

答案 3 :(得分:0)

如果优先级较小且固定,则可以为每个优先级使用ring-buffer。如果对象很大,那将导致浪费空间,但是如果它们的大小与指针/索引相当,那么在对象中存储附加指针的变体可能会以相同的方式增加数组的大小。
或者你可以在数组中使用简单的单链表并存储2 * M + 1个指针/索引,一个指向第一个空闲节点,其他对指向每个优先级的头尾。在那种情况下,你将不得不平均比较。 O(M)在用O(1)取出下一个节点之前。插入将采用O(1)。

答案 4 :(得分:0)

如果以最大大小构造STL优先级队列(可能来自使用占位符初始化的向量),然后在插入之前检查大小(如果有必要,则在事先删除项目),在插入操作期间永远不会进行动态分配。 STL实现非常有效。

答案 5 :(得分:0)

Matters Computational请参阅第158页。实现本身非常好,您甚至可以稍微调整它,而不会降低其可读性。例如,当你计算左边的孩子时:

int left = i / 2;

你可以像这样计算右边的孩子:

int right = left + 1;

答案 6 :(得分:0)

找到解决方案(“差异”在代码中表示“优先级”,maxRememberedResults为255(可以是任意(2 ^ n - 1)):

template <typename T> inline void swap(T& a, T& b){
    T c = a;
    a = b;
    b = c;
}


struct MinDifferenceArray{
    enum{maxSize = maxRememberedResults};
    int size;
    DifferenceData data[maxSize];
    void add(const DifferenceData& val){
        if (size >= maxSize){
            if(data[0].difference <= val.difference)
                return;

            data[0] = val;

            for (int i = 0; (2*i+1) < maxSize; ){
                int next = 2*i + 1;
                if (data[next].difference < data[next+1].difference)
                    next++;
                if (data[i].difference < data[next].difference)
                    swap(data[i], data[next]);
                else
                    break;
                i = next;
            }
        }
        else{
            data[size++] = val;
            for (int i = size - 1; i > 0;){
                int parent = (i-1)/2;
                if (data[parent].difference < data[i].difference){
                    swap(data[parent], data[i]);
                    i = parent;
                }
                else
                    break;
            }
        }
    }

    void clear(){
        size = 0;
    }

    MinDifferenceArray()
        :size(0){
    }
};
  1. 构建基于max的队列(root是最大的)
  2. 直到它满了,正常填满
  3. 当它已满时,每个新元素
    1. 检查新元素是否小于root。
    2. 如果它大于或等于root,则拒绝。
    3. 否则,用new元素替换root并执行普通堆“sift-down”。

我们得到O(log(N))插入作为最坏的情况。

与昵称为“Moron”的用户提供的解决方案相同。 感谢大家的回复。

P.S。显然没有足够睡眠的编程并不是一个好主意。

答案 7 :(得分:0)

最好使用std :: array和堆算法来实现自己的类。

  `template<class T, int fixed_size = 5>
   class fixed_size_arr_pqueue_v2
   {
     std::array<T, fixed_size> _data;
     int _size = 0;

     int parent(int i)
     {
       return (i - 1)/2;
     }

     void heapify(int i, bool downward = false)
     {
       int l = 2*i + 1;
       int r = 2*i + 2;
       int largest = 0;
       if (l < size() && _data[l] > _data[i])
        largest = l;
       else
        largest = i;

       if (r < size() && _data[r] > _data[largest])
        largest = r;   

       if (largest != i)
       {
         std::swap(_data[largest], _data[i]);
         if (!downward)
           heapify(parent(i));
         else
           heapify(largest, true);
       }
    }

    public:

    void push(T &d)
    {
       if (_size == fixed_size)
       {
          //min elements in a max heap lies at leaves only. 
          auto minItr = std::min_element(begin(_data) + _size/2, end(_data));
          auto minPos {minItr - _data.begin()};
          auto min { *minItr};

          if (d > min)
          {
             _data.at(minPos) = d;
             if (_data[parent(minPos)] > d)
             { 
                //this is unlikely to happen in our case? as this position is  a leaf?
                heapify(minPos, true);
             }  
             else
                heapify(parent(minPos));
           }           

          return ;
       }

       _data.at(_size++) = d;
       std::push_heap(_data.begin(), _data.begin() + _size);
    }

    T pop()
    {
       T d = _data.front();
       std::pop_heap(_data.begin(), _data.begin() + _size);
       _size--;
       return d;
    }

    T top()
    {
       return _data.front();
    }

    int size() const
    {
       return _size;
    }
 };`