意外行为 java 优先级队列。对象添加了一次但轮询了两次。怎么可能?

Posted

技术标签:

【中文标题】意外行为 java 优先级队列。对象添加了一次但轮询了两次。怎么可能?【英文标题】:Unexpected behaviour java priority queue. Object added once but polled out twice. How could it be possible? 【发布时间】:2021-05-30 05:41:54 【问题描述】:
import java.util.*;

public class Main 
    public static void main (String[] args) 
        Solution solution = new Solution();
        int[] res = solution.assignTasks(new int[]3,3,2, new int[]1,2,3,2,1,2);
    

class Solution 
    public int[] assignTasks(int[] servers, int[] tasks) 
        PriorityQueue<Pair> free = new PriorityQueue(); // (wt, id, time)
        PriorityQueue<Pair> busy = new PriorityQueue(); // (time, wt, id)
        
        for (int i = 0; i < servers.length; i++) 
            free.add(new Pair(servers[i], i, 0));
            System.out.println("Free Added " + i + " " + servers[i] + " " + i + " " + 0 + " " + free.size());
        
        
        int[] ans = new int[tasks.length];
        for (int i = 0; i < tasks.length; i++) 
            Pair b = busy.peek();
            while (b != null && b.a <= i) 
                busy.poll();
                System.out.println("Busy Polled " + i + " " + b.a + " " + b.b + " " + b.c + " " + busy.size());
                free.add(new Pair(b.b, b.c, b.a));
                System.out.println("Free Added " + i + " " + b.b + " " + b.c + " " + b.a + " " + free.size());
                b = busy.peek();
            
            Pair p = free.poll();
            
            int wt = p.a;
            int id = p.b;
            
            // int time = p.c;
            System.out.println("Free Polled " + i + " " + p.a + " " + p.b + " " + p.c + " " + free.size());
            
            ans[i] = id;
            busy.add(new Pair(i + tasks[i], wt, id));
            System.out.println("Added to Busy " + i + " " + (i + tasks[i]) + " " + p.a + " " + p.b + " " + busy.size());
        
        
        return ans;
    

class Pair implements Comparable<Pair> 
    int a, b, c;
    Pair(int x, int y, int z) 
        a = x;
        b = y;
        c = z;
    
    public int compareTo(Pair p) 
         if (this.a == p.a) 
             if (this.b == p.b) return this.c - p.c;
             return this.b = p.b;
         
         return this.a - p.a;
     
    

我在优先级队列中使用 Pair 类插入了三元组,一对从队列中轮询了两次,并且只插入了一次。怎么可能? 我还为添加到调试的打印语句附加了控制台输出。突出显示异常行为。

代码的输出:(注意输出中的行为。突出显示它(3,0,0)轮询两次?)

Free Added 0 3 0 0 1       (Added to the queue)
Free Added 1 3 1 0 2
Free Added 2 2 2 0 3
Free Polled 0 2 2 0 2
Added to Busy 0 1 2 2 1
Busy Polled 1 1 2 2 0
Free Added 1 2 2 1 3
Free Polled 1 2 2 1 2
Added to Busy 1 3 2 2 1
Free Polled 2 3 0 0 1     (Polled 1st time)
Added to Busy 2 5 3 0 2
Busy Polled 3 3 2 2 1
Free Added 3 2 2 3 2
Free Polled 3 2 2 3 1
Added to Busy 3 5 2 2 2
Free Polled 4 3 0 0 0     (Polled 2nd time)
Added to Busy 4 5 3 0 3
Busy Polled 5 5 3 0 2
Free Added 5 3 0 5 1
Busy Polled 5 5 3 0 1
Free Added 5 3 0 5 2
Busy Polled 5 5 3 2 0
Free Added 5 3 2 5 3
Free Polled 5 3 0 5 2
Added to Busy 5 7 3 0 1

这个问题来自最近结束的 Leetcode 竞赛。链接到问题的discussion 部分。链接到question IDE链接run

【问题讨论】:

return this.b = p.b。您不应该分配给 compareTo() 方法中的对象之一。此时您可能应该返回this.b - p.b 【参考方案1】:

不完全清楚,但我认为您的问题是由于compareTo 的错误实现引起的。

public int compareTo(Pair p) 
     if (this.a == p.a) 
         if (this.b == p.b) return this.c - p.c;
         return this.b = p.b;
     
     return this.a - p.a;
 

问题 #1

return this.b = p.b;

这将返回p.b,并作为副作用将p.b 分配给this.b。这是完全错误的。

你的意思可能是return this.b - p.b;

请注意,由于您的 compareTo 方法正在更改 Pair 对象之一,因此结果将会混乱。这可能就是为什么某些Pair 对象似乎 会被多次返回。 (事实上​​,它们是不同的 Pair 对象......但是您已经对它们进行了变异,以便它们在 c 字段中具有相同的值。)

问题 #2

表达式this.c - p.c 在极端情况下给出不正确的答案。例如,this.c 非常大,而p.c 为负数,那么减法可能会溢出到一个负数,这表示this.cp.c“小”……但事实并非如此。

请参阅Java Integer compareTo() - why use comparison vs. subtraction? 了解更多信息。

以下是比较两个int 值以给出与compareTo 合约兼容的结果的几种正确方法:

    速写:

    if (this.c == p.c) 
        return 0;
     else if (this.c < p.c) 
        return -1;
     else 
        return +1;
    
    

    使用条件表达式:

    return (this.c == p.c) ? 0 : ((this.c < p.c) ? -1 : +1);
    

    使用Integer.compare

    return Integer.compare(this.c, p.c);
    

正确的实现

如果我们假设Pair 对象应该是不可变的,那么这是一个正确的1 实现:

class Pair implements Comparable<Pair> 
    final int a, b, c;

    Pair(int x, int y, int z) 
        a = x;
        b = y;
        c = z;
    

    public int compareTo(Pair p) 
        if (this.a == p.a) 
            if (this.b == p.b) 
                return Integer.compare(this.c, p.c);
             else 
                return Integer.compare(this.b, p.b);
            
         else 
            return Integer.compare(this.a, p.a);
        
    

问:为什么要将字段声明为final? 答:为了防止在Pair 类本身以及该类及其字段可访问的其他类中对字段进行意外更改。

问:为什么要使用if ... else? A:因为它更清晰。并且因为 JIT 编译器在任何一种情况下都应该生成等效(最佳)代码。

问:compare 呢?方法调用不会低效吗? 答:可能不会。方法调用可能会被 JIT 编译器内联。

问:效率重要吗? 答:在您的示例中,这是无关紧要的。在一般情况下,它可能无关2


1 - 类名Pair 也不正确。应该是Triple。 2 - 只有在 1) 对代码进行基准测试、2) 对其进行分析以及 3) 确定 compareTo 方法确实是性能热点之后,您才应该尝试手动优化它。

【讨论】:

以上是关于意外行为 java 优先级队列。对象添加了一次但轮询了两次。怎么可能?的主要内容,如果未能解决你的问题,请参考以下文章

Java优先级队列heapify [重复]

java面向对象的栈 队列 优先级队列的比较

使用 LocalDate 字段读取对象时自定义 ObjectInputStream 的意外行为

堆栈视图和内容拥抱优先级 - 意外行为

48-优先级队列

优先队列