likes
comments
collection
share

深入理解Java优先级队列

作者站长头像
站长
· 阅读数 8

前言

在软件开发的过程中,我们经常需要处理涉及优先级的任务或元素,这时优先级队列便成为一个强大而高效的工具。最近,我在使用Java的优先级队列时遇到了一些小问题,这促使我深入研究了这一数据结构的实现和应用。

在本文中,我将分享我对Java优先级队列的深入理解,并探讨了在使用过程中遇到的一些问题。通过深入探讨优先级队列的基本概念、实现原理以及一些实际应用场景,我希望能够为您提供一份全面的指南,使您能更加自信和高效地应对类似的问题。

介绍

  • 什么是优先级队列?

    优先级队列是一种特殊的数据结构,它与普通队列相似,但在元素处理的顺序上有所不同。在优先级队列中,每个元素都关联有一个优先级或权重,确保元素按照这个优先级顺序进行处理。这意味着拥有更高优先级的元素将在队列中拥有更高的位置,优先被处理,而拥有较低优先级的元素则相对较晚被处理。

  • 为什么使用优先级队列?

  1. 任务调度: 在任务调度系统中,优先级队列能够确保高优先级的任务被迅速而有效地执行,以满足特定需求,这个也是开发者最常见的使用方法。
  2. 图算法: 在图算法中,如Dijkstra算法,通过使用优先级队列能够高效地选择下一个要处理的节点,从而加速图遍历和路径查找。
  3. 哈夫曼编码: 优先级队列在构建哈夫曼树时起到关键作用,这是一种用于数据压缩的常见算法。
  • Java中的优先级队列是如何工作的?

    在Java中,优先级队列通常通过PriorityQueue类实现。PriorityQueue是一个基于优先级堆的无界队列。堆是一种二叉树结构,其中每个节点的值满足堆的性质。在Java中,PriorityQueue默认使用元素的自然顺序(小顶堆),或者通过提供比较器(Comparator)来自定义排序规则(自定义大顶堆)。

Java优先级队列概述

  • PriorityQueue类的基本概念

    PriorityQueue是Java中表示优先级队列的类,位于java.util包下。它是一个无界队列,可以动态地根据需要调整大小。优先级队列的元素可以是自然有序或通过提供比较器进行排序。

  • 底层数据结构及实现原理

    Java的PriorityQueue基于二叉堆实现。堆是一种树状结构,分为最小堆和最大堆。在最小堆中,每个节点的值都小于其子节点的值,而在最大堆中,每个节点的值都大于其子节点的值。PriorityQueue默认使用最小堆,以确保队首元素是最小值。底层数据结构的选择使得插入和删除元素的操作效率较高。在最小堆中,插入操作的时间复杂度为O(log n),而删除最小元素的操作也是O(log n)。这使得PriorityQueue非常适合需要频繁插入和删除元素的场景。

  • 如何使用Java优先级队列

    使用PriorityQueue非常简单。以下是一些基本操作的示例:

  1. 创建优先级队列:

    PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
    
  2. 插入元素:

    priorityQueue.offer(5);
    priorityQueue.offer(2);
    priorityQueue.offer(8);
    
  3. 获取并删除队首元素:

    int highestPriority = priorityQueue.poll();
    
  4. 获取但不删除队首元素:

    int highestPriority = priorityQueue.peek();
    
  5. 自定义比较器:

    PriorityQueue<Integer> customPriorityQueue = new PriorityQueue<>(Comparator.reverseOrder());
    

以上操作示例演示了如何创建、插入、删除元素以及如何使用自定义比较器。PriorityQueue的灵活性和高效性使其成为处理具有不同优先级元素的理想选择。

优先级队列的操作和方法

  • 插入元素
  • 获取队首元素
  • 删除队首元素
  • 修改队列元素
  • 遍历队列

1. 插入元素:

使用 offer() 方法向优先级队列插入元素。

PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(5);
priorityQueue.offer(2);
priorityQueue.offer(8);

2. 获取队首元素:

使用 peek() 方法获取但不删除队首元素。

int highestPriority = priorityQueue.peek();
System.out.println("队首元素: " + highestPriority);

3. 删除队首元素:

使用 poll() 方法获取并删除队首元素。

int highestPriority = priorityQueue.poll();
System.out.println("删除的队首元素: " + highestPriority);

4. 修改队列元素:

由于PriorityQueue不直接提供修改队列元素的方法,修改的一种方式是先删除旧元素,然后插入新元素。

// 假设要将队列中的元素2修改为新值10
priorityQueue.remove(2);
priorityQueue.offer(10);

4. 遍历队列元素:

Queue<Integer> queue = new PriorityQueue(Comparator.reverseOrder());
queue.offer(5);
queue.offer(2);
queue.offer(8);

/**
 * 这种遍历无序
 */
for (Integer item : queue) {
    System.out.println(item);
}
System.out.println("-----------");
/**
 * 按照优先级排序
 */
while (!queue.isEmpty()) {
    System.out.println(queue.poll());
}

一般只会使用 :

while (!queue.isEmpty()) {
    System.out.println(queue.poll());
}

否则优先级队列失去了意义。

自定义优先级

  • 实现Comparable接口
public class Task implements Comparable<Task> {
 private String name;
 private int priority;

 public Task(String name,int priority){
     this.name = name;
     this.priority = priority;
 }
 public int getPriority() {
     return priority;
 }
 @Override
 public int compareTo(Task task) {
     return -(this.priority - task.priority);
 }
 @Override
 public String toString() {
     return "Task{" +
             "name='" + name + ''' +
             ", priority=" + priority +
             '}';
 }
PriorityQueue<Task> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(new Task("TaskA", 2));
priorityQueue.offer(new Task("TaskB", 1));
priorityQueue.offer(new Task("TaskC", 3));

while (!priorityQueue.isEmpty()) {
    Task task = priorityQueue.poll();
    System.out.println(task);
}

输出:

    Task{name='TaskC', priority=3}
    Task{name='TaskA', priority=2}
    Task{name='TaskB', priority=1}
  • 使用自定义对象比较器
PriorityQueue<Task> priorityQueue2 = new PriorityQueue<>(new Comparator<Task>() {
    @Override
    public int compare(Task t1, Task t2) {
        return t1.priority - t2.priority;
    }
});

priorityQueue2.offer(new Task("TaskA", 2));
priorityQueue2.offer(new Task("TaskB", 1));
priorityQueue2.offer(new Task("TaskC", 3));

// 遍历队列
while (!priorityQueue2.isEmpty()) {
    Task task = priorityQueue2.poll();
    System.out.println(task);
}

输出:

    Task{name='TaskB', priority=1}
    Task{name='TaskA', priority=2}
    Task{name='TaskC', priority=3}

复杂度分析

  • 插入、删除等操作的时间复杂度
  1. 插入操作 (offer()):

    • 在最小堆中,插入一个元素的时间复杂度为O(log n),其中n是堆中元素的数量。这是因为插入后需要保持堆的性质,可能需要进行上浮操作。
  2. 删除队首元素操作 (poll()):

    • 删除堆顶元素的时间复杂度为O(log n),其中n是堆中元素的数量。删除后,需要进行下沉操作,以维护堆的性质。
  3. 获取队首元素操作 (peek()):

    • 获取堆顶元素的时间复杂度为O(1)。这是因为堆顶元素总是存储在数组的第一个位置,不需要进行额外的遍历。
  • 底层数据结构对性能的影响

PriorityQueue的底层数据结构是二叉堆,通常是一个完全二叉树。在Java中,PriorityQueue默认使用最小堆实现,也可以通过提供自定义的比较器来使用最大堆。

  1. 最小堆的影响:

    • 最小堆的特性使得堆顶元素是最小的,因此poll()操作总是能够迅速获取到当前队列中最高优先级的元素。
    • 但是,对于其他位置的元素,由于它们没有严格的顺序,因此在遍历或访问时的性能可能受到一定影响。
  2. 堆的调整操作:

    • 插入和删除元素时,需要对堆进行调整以保持堆的性质。这些调整操作的时间复杂度主要受到堆的高度的影响,即O(log n)。
  3. 自定义比较器的影响:

    • 如果使用自定义比较器,其实现效率也可能对性能产生影响。合理设计比较器可以提高排序的效率。

总体而言,PriorityQueue通过使用最小堆实现,在插入、删除和获取队首元素等操作上能够提供较高的效率。性能的影响主要取决于元素的插入和删除频率以及底层数据结构的特性。

注意事项

  • 优先级相同情况怎么处理

    优先级相同的时候,PriorityQueue 是不能保证顺序出队的,比如下面这个:

    PriorityQueue<Task> priorityQueue = new PriorityQueue<Task>();
    priorityQueue.offer(new Task("TaskA", 1));
    priorityQueue.offer(new Task("TaskB", 1));
    priorityQueue.offer(new Task("TaskC", 1));
    priorityQueue.offer(new Task("TaskD", 1));
    
    while (!priorityQueue.isEmpty()) {
        Task task = priorityQueue.poll();
        System.out.println(task);
    }
    

    输出:

    Task{name='TaskA', priority=1}
    Task{name='TaskD', priority=1}
    Task{name='TaskC', priority=1}
    Task{name='TaskB', priority=1}
    

    这不是我想要的,我想要是如果优先级相同,先入队的先出队,这个时候,就要加字段来处理了。

    public class Task implements Comparable<Task> {
      private String name;
      private int priority;
    
      private int enqueueCount;
    
    
    public Task(String name,int priority){
        this.name = name;
        this.priority = priority;
    }
    public Task(String name,int priority,int enqueueCount){
        this.name = name;
        this.priority = priority;
        this.enqueueCount = enqueueCount;
    }
    public int getPriority() {
        return priority;
    }
    @Override
    public int compareTo(Task task) {
        if(this.priority == task.priority){
            return this.enqueueCount - task.enqueueCount;
        }
        return -(this.priority - task.priority);
    }
    @Override
    public String toString() {
        return "Task{" +
                "name='" + name + ''' +
                ", priority=" + priority +
                '}';
    }
    

    创建Task

PriorityQueue<Task> priorityQueue = new PriorityQueue<Task>();
AtomicInteger atomicInteger = new AtomicInteger(0);
priorityQueue.offer(new Task("TaskA", 1,atomicInteger.getAndIncrement()));
priorityQueue.offer(new Task("TaskB", 1,atomicInteger.getAndIncrement()));
priorityQueue.offer(new Task("TaskC", 1,atomicInteger.getAndIncrement()));
priorityQueue.offer(new Task("TaskD", 1,atomicInteger.getAndIncrement()));

while (!priorityQueue.isEmpty()) {
    Task task = priorityQueue.poll();
    System.out.println(task);
}

输出:

   Task{name='TaskA', priority=1}
   Task{name='TaskB', priority=1}
   Task{name='TaskC', priority=1}
   Task{name='TaskD', priority=1}
  • 如何处理并发操作

    如果多线程处理,保证并发安全,PriorityQueue直接改成PriorityBlockingQueue 即可。

总结

Java的PriorityQueue提供了一种基于优先级的队列实现,使用最小堆来实现默认的排序,但也支持自定义的比较器。通过该数据结构,我们可以轻松地处理按照优先级顺序访问元素的场景。

在实际应用中,PriorityQueue经常用于任务调度、事件处理等场景,其中不同任务或事件具有不同的优先级,需要按照优先级顺序进行处理。通过合理设置元素的优先级,可以有效地管理和调度任务,提高系统的效率。

在使用PriorityQueue时,当两个任务的优先级相同时,需要特殊处理,上面的处理方式是按照入队次数升序排序,确保相同优先级的任务按照入队先后顺序出队。

转载自:https://juejin.cn/post/7338791751849885722
评论
请登录