【问题标题】:Updating Java PriorityQueue when its elements change priority当元素更改优先级时更新 Java PriorityQueue
【发布时间】:2010-12-24 16:23:18
【问题描述】:

我正在尝试使用PriorityQueue 来使用Comparator 对对象进行排序。

这很容易实现,但对象类变量(比较器计算优先级的对象)可能会在初始插入后发生变化。大多数人都提出了删除对象、更新值并重新插入它的简单解决方案,因为这是优先级队列的比较器起作用的时候。

除了围绕 PriorityQueue 创建一个包装类之外,还有更好的方法吗?

【问题讨论】:

标签: java priority-queue


【解决方案1】:

您必须删除并重新插入,因为队列的工作原理是在插入新元素时将它们放在适当的位置。这比每次退出队列时查找最高优先级元素的替代方法要快得多。缺点是元素插入后不能更改优先级。 TreeMap 也有同样的限制(HashMap 也是如此,当其元素的哈希码在插入后发生变化时也会中断)。

如果要编写包装器,可以将比较代码从入队移动到出队。您不再需要在入队时进行排序(因为如果您允许更改,它创建的顺序无论如何都不可靠)。

但这会执行得更糟,如果您更改任何优先级,您希望在队列上进行同步。由于在更新优先级的时候需要添加同步代码,所以还不如出队入队(这两种情况都需要对队列的引用)。

【讨论】:

  • 我明白了,所以移除改变它的对象并重新插入它是最好的选择吗?
  • 我相信是的。当然,只有在进行更改的代码知道对象正在等待的任何队列时才能这样做。
  • 是的,我的情况就是这样。
  • 从 PQ 中移除是 O(n),更快的解决方案是重新实现像斐波那契堆/二项式堆这样的快速堆。然后当减少一个键时,您将能够更新 PQ。为此,每个节点都必须获得指向其父节点的指针才能向上渗透。困难的方式,但更快。
【解决方案2】:

我不知道是否有 Java 实现,但如果您要大量更改键值,则可以使用 Fibonnaci 堆,它的摊销成本为 O(1),减少堆中条目的键值,而不是普通堆中的 O(log(n))。

【讨论】:

  • JDK 没有 FibonacciHeap,尽管它确实来自第三方。我知道一旦将一个对象添加到我的队列中,它只能增加优先级,所以有人知道用 O(1) 交换两个元素的实现吗?比如减少功能。或者通过交换元素而不是使用 PriorityQueue 删除和重新插入分别需要 O(1) 和 O(log(n)) 的任何实现,这可以轻松实现吗?
  • @MarcusWhybrow 斐波那契堆具有恒定的摊销成本以减少密钥,但实际上该常数似乎相当大。您的数据集必须非常大才能使 O(log n) 比该常数更差。见Is there a standard Java implementation of a Fibonacci heap?。
【解决方案3】:

这在很大程度上取决于您是否可以直接控制何时值发生变化。

如果您知道值何时更改,则可以删除并重新插入(实际上这相当昂贵,因为删除需要对堆进行线性扫描!)。 此外,在这种情况下,您可以使用 UpdatableHeap 结构(尽管 Java 中没有)。本质上,这是一个跟踪哈希图中元素位置的堆。这样,当一个元素的优先级发生变化时,它可以修复堆。第三,你可以寻找一个同样的斐波那契堆。

根据您的更新率,每次线性扫描/快速排序/快速选择也可能有效。特别是如果你有比pulls 更多的更新,这就是要走的路。如果您有批量更新和批量拉取操作,则 QuickSelect 可能是最好的。

【讨论】:

    【解决方案4】:

    要触发 reheapify 试试这个:

    if(!priorityQueue.isEmpty()) {
        priorityQueue.add(priorityQueue.remove());
    }
    

    【讨论】:

    • 为什么这不会造成无限循环?
    • @MohammadZulfikar 这不是一个循环,它只是一个 if 语句。它将执行两个操作:删除和添加。
    • @MilesHenrichs 哦,是的!对不起这是我的错。在那种情况下,它似乎真的很好。怎么说?
    • 这是错误的,因为更新后无法保证堆属性没有损坏,并且您的答案基本上试图重新堆化可能没有堆属性的数组
    • @Jim 是对的。我今天已经体验过了。
    【解决方案5】:

    我已经尝试过并且到目前为止有效的方法是查看对您正在更改的对象的引用是否与 PriorityQueue 的头部相同,如果是,那么您 poll(),然后更改重新插入;否则你可以在没有轮询的情况下进行更改,因为当轮询头部时,无论如何都会堆化堆。

    缺点:这会更改具有相同优先级的对象的优先级。

    【讨论】:

      【解决方案6】:

      您可以实施的一个简单解决方案是将该元素再次添加到优先级队列中。它不会改变你提取元素的方式,虽然它会消耗更多的空间,但也不会太多影响你的运行时间。

      为了证明这一点,让我们考虑下面的 dijkstra 算法

      public int[] dijkstra() {
      int distance[] = new int[this.vertices];
      int previous[] = new int[this.vertices];
      for (int i = 0; i < this.vertices; i++) {
          distance[i] = Integer.MAX_VALUE;
          previous[i] = -1;
      }
      distance[0] = 0;
      previous[0] = 0;
      PriorityQueue<Node> pQueue = new PriorityQueue<>(this.vertices, new NodeComparison());
      addValues(pQueue, distance);
      while (!pQueue.isEmpty()) {
          Node n = pQueue.remove();
          List<Edge> neighbours = adjacencyList.get(n.position);
          for (Edge neighbour : neighbours) {
              if (distance[neighbour.destination] > distance[n.position] + neighbour.weight) {
                  distance[neighbour.destination] = distance[n.position] + neighbour.weight;
                  previous[neighbour.destination] = n.position;
                  pQueue.add(new Node(neighbour.destination, distance[neighbour.destination]));
              }
          }
      }
      return previous;
      

      }

      我们的兴趣是一致的 pQueue.add(new Node(neighbour.destination, distance[neighbour.destination])); 我不是通过删除它并再次添加来更改特定节点的优先级,而是我只是添加具有相同值但优先级不同的新节点。 现在在提取的时候,我总是首先得到这个节点,因为我在这里实现了最小堆,并且值大于这个(优先级较低)的节点总是在之后被提取,这样所有相邻节点都将在较少先验时被放松元素将被提取。

      【讨论】:

      • Integer.MAX_VALUE 溢出
      【解决方案7】:

      无需自己重新实现优先级队列(因此​​仅使用utils.PriorityQueue),您基本上有两种主要方法:

      1) 移除并放回

      删除元素,然后以新的优先级将其放回原处。这在上面的答案中有解释。删除一个元素是 O(n),所以这种方法很慢。

      2) 使用地图并在队列中保留陈旧的项目

      保留项目的HashMap -> 优先级。地图的键是项目(没有它们的优先级),地图的值是优先级。

      使其与 PriorityQueue 保持同步(即每次从队列中添加或删除项目时,相应地更新地图)。

      现在,当您需要更改项目的优先级时,只需将相同的项目添加到具有不同优先级的队列中即可。当您从队列中轮询项目时,请检查其优先级是否与您的地图中的相同。如果没有,那就放弃它并再次投票。

      如果您不需要过于频繁地更改优先级,则第二种方法更快。您的堆会更大,您可能需要轮询更多次,但您不需要找到您的项目。 “更改优先级”操作将是 O(f(n)log n*),其中 f(n) 是每次 “更改优先级”操作的次数item 和 n* 堆的实际大小(即 n*f(n))。

      我相信如果 f(n) 为 O(n/logn)(例如 f(n) = O(sqrt(n)),这比第一种方法要快。

      注意:在上面的解释中,priority是指在你的比较器中使用的所有变量。此外,您的项目需要实现equals 和hashcode,并且这两种方法都不应使用优先级变量。

      【讨论】:

        猜你喜欢
        • 2015-05-23
        • 1970-01-01
        • 2012-06-15
        • 2020-08-09
        • 1970-01-01
        • 2019-09-18
        • 1970-01-01
        相关资源
        最近更新 更多