Java进阶 | 集合系列(一)PriorityQueue优先队列 | 综合运用 | 深入理解

参考资料:Java语言程序设计(进阶篇) ——梁勇先生 著 戴开宇先生 译

运行环境


  • Windows10
  • JDK8
  • IDEA

前言


有一天
在这里插入图片描述
大雄惊讶地看着自己零分的考卷...


哆啦A梦说:"大雄,你怎么又考了个零分呀!

在这里插入图片描述
大雄:"什么嘛,哆啦A梦,因为考的题太难了!我不会呀"

然后......

哆啦A梦随着大雄来到学校进行了一场考试

结果是:

名字 成绩
大雄 0
哆啦A梦 100
静香 96
胖虎 60
小夫 90

哆啦A梦:哎,不愧是大雄,又是零分,咦? 怎么成绩没有排序啊?


哆啦A梦:接下来,就让我掏出一个宝贝来解决把!当当当当,\(Priority Queue优先队列\) !接下来,先创建一个实体类 \(Student.java\)来表示大雄他们吧~

class Student{
    private String id;
    private Character gender;
    private Double score;

    public Student(String id, Character gender, Double score) {
        this.id = id;
        this.gender = gender;
        this.score = score;
    }
    public String getId() {  return id; }
    public Character getGender() { return gender;}
    public Double getScore() {return score; }

    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", gender=" + gender +
                ", score=" + score +
                '}';
    }
}

1. 队列与优先队列概述


队列 \(queue\) 是一种 \(FIFO\) 即先进先出的数据结构,元素被追加到队列末尾,然后从队列头删除。

优先队列 \(Priority Queue\) 是 在 \(queue\) 的基础上给每个队列元素提供了一个优先级来进行比较

本篇文章,有许多关于比较器 \(Comparator\) 的知识,若需要了解,除了在网络上查询资料外,可以参考这篇博文:
【Java进阶】Comparable接口与Comparator接口的深入理解 | 有关排序的都会用到

2. 优先队列结构剖析


2.1 集合框架中的优先队列

PriorityQueue的UML图 制作工具: IDEA
在这里插入图片描述
从图中可以看出

  • 优先队列实现了 \(Serializable\) 接口,所以是可序列化的。

  • 继承了\(AbstractQueue\) 类 ,这个类实现了 \(Iterable\) 接口的间接子接口\(Queue\),所以优先队列是可迭代的。故可通过 \(foreach\) 语句循环遍历(注: 输出的结果无序的

PriorityQueue<String> pq = new PriorityQueue<>();

for(String s : pq){
	// System.out.println(s);
}

2.2 成员变量

成员变量除了自身的成员变量,其内部类中也有成员变量

可以通过下面的UML图进行了解:

在这里插入图片描述

2.2 常用方法


Java集合框架的方法有许多,而且有些方法是来自于接口,有的是本身独有的方法,非常繁杂,笔者认为没有必要记住所有的使用方法,最主要的是

  • 记住所有的构造方法 ,方便创建对象

  • 记住常用的方法,比如在队列中的peek() 、poll()、add()、offer()、remove() 等

2.2.1 构造器

在这里插入图片描述
优先队列构造器用到的主要对象有:

  • \(Comparator\) 比较器,通过调用比较器的compare方法赋予元素优先级
  • \(int\) ,整型变量表示优先队列的容量
  • \(Collection\) 集合容器接口的实现类,这里并不表示接纳所有的集合类,具体得查看源码,之后会进行分析
  • \(SortedSet\) 有序集合接口的实现类

最后一个可排序的集合有什么用呢?在成员变量中没有看到该类型,这说明肯定是在构造函数中调用了其他方法对该对象进行了类型变换,后经查询资料发现:

  • PriorityQueue内部实现是采用 堆排序

接下来,先编写一个函数运用反射机制用于输出一个类的所有的成员变量,包括私有成员,这样方便查看不同构造器下的差异。
用到的包

import java.lang.reflect.Field;
import java.util.PriorityQueue;

printPriority() 方法

    public static <T> void printPriority(PriorityQueue<T> q) {
        // 反射机制获取类的内容
        Field[] info = q.getClass().getDeclaredFields();        
        for (Field field : info) {
            // 打开所有访问权限
            field.setAccessible(true);      
            try {
                //格式化输出
                System.out.println(
                        String.format("Type:%-30s Name: %-25s Value:%-10s",
                                field.getType(),
                                field.getName(),
                                field.get(q))); 
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

(1) 无参构造 new PriorityQueue<>()

public class Demo {
    public static void main(String[] args) {
        PriorityQueue<Student> q = new PriorityQueue<>();
        printPriority(q);
    }
}

运行结果

Type:long                           Name: serialVersionUID          Value:-7720805057305804111
Type:int                            Name: DEFAULT_INITIAL_CAPACITY  Value:11        
Type:class [Ljava.lang.Object;      Name: queue                     Value:[Ljava.lang.Object;@12a3a380
Type:int                            Name: size                      Value:0         
Type:interface java.util.Comparator Name: comparator                Value:null      
Type:int                            Name: modCount                  Value:0         
Type:int                            Name: MAX_ARRAY_SIZE            Value:2147483639

(2) 传入容量参数 new PriorityQueue<>(int initialCapacity)

public class Demo {
    public static void main(String[] args) {
        PriorityQueue<Student> q = new PriorityQueue<>(100);
        printPriority(q);
    }
}

运行结果

Type:long                           Name: serialVersionUID          Value:-7720805057305804111
Type:int                            Name: DEFAULT_INITIAL_CAPACITY  Value:11        
Type:class [Ljava.lang.Object;      Name: queue                     Value:[Ljava.lang.Object;@12a3a380
Type:int                            Name: size                      Value:0         
Type:interface java.util.Comparator Name: comparator                Value:null      
Type:int                            Name: modCount                  Value:0         
Type:int                            Name: MAX_ARRAY_SIZE            Value:2147483639

观察结果可以发现,优先队列本身size的值不会因此发生变化,因为它表示当前的元素个数

初始化时,实际变化的是其成员queue的容量,在源码中是这样实现的:

this.queue = new Object[initialCapacity];

(3) 传入比较器, 使用匿名内部类实现

public class Demo {
    public static void main(String[] args) {
        PriorityQueue<Student> q = new PriorityQueue<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getScore() - o2.getScore() > 0 ? 1 : -1;
            }
        });
        printPriority(q);
    }
}

运行结果

Type:long                           Name: serialVersionUID          Value:-7720805057305804111
Type:int                            Name: DEFAULT_INITIAL_CAPACITY  Value:11        
Type:class [Ljava.lang.Object;      Name: queue                     Value:[Ljava.lang.Object;@12a3a380
Type:int                            Name: size                      Value:0         
Type:interface java.util.Comparator Name: comparator                Value:Demo$1@29453f44
Type:int                            Name: modCount                  Value:0         
Type:int                            Name: MAX_ARRAY_SIZE            Value:2147483639

可以观察到优先队列中的comparator 变量发生了变化,从而定义优先队列元素的优先级

对于其他的方法,部分是来自于父类 AbstracQueue 和父类实现的接口 Queue
在这里插入图片描述

在这里插入图片描述

2.2.2 添加元素 | 入队 | add方法 | offer方法

public boolean add(E e){}
public boolean offer(E e) {}

优先队列 PriorityQueue 继承了 AbstractQueue 类,实现了Queue接口。

这两个方法都是在Queue接口中声明的,抽象类实现了其中的 add() 方法

在优先队列源码中,add()方法的原理就是调用自身实现的offer() 方法

它们的区别在于:

  • offer() 方法 会抛出异常

在源码中,Queue接口的 offer() 方法 规定 若参数为null则抛出一个异常

2.2.3 删除元素 | 出队 | remove方法 | poll 方法

在队列中, remove(Object o) 方法执行时,若o==null,则会报错,但在优先队列里并没有这个规定

通过查看源码,队列的另一个java.util.LinkedList实现类中的remove方法才会在空值时抛出异常

public boolean remove(Object o) {}
public E poll() {}

3. 遍历优先队列的三种方式


遍历前,先插入测试的数据:


      q.offer(new Student("大雄", 'M', 0.0));
      q.offer(new Student("哆啦A梦", 'M', 100.0));
      q.offer(new Student("静香", 'M', 96.0));
      q.offer(new Student("胖虎", 'W', 60.0));
      q.offer(new Student("小夫", 'M', 90.0));

3.1 直接For循环遍历 (无序)

for (Student student : q) {
	System.out.println(student);
}

3.2 出队方式遍历 (有序)

PriorityQueue<Student> temp = new PriorityQueue<>(q);
while(!temp.isEmpty()){
    System.out.println(temp.poll());
}

3.3 迭代器遍历 (无序)

Iterator<Student> iterator = temp.iterator();
while(iterator.hasNext()) {
	System.out.println(iterator.next());
}

3.4 toArray方法遍历 (无序)

Object[] students = (Object[]) q.toArray();
for (Object student : students) {
     System.out.println(student);
}

3.5 反射机制遍历(无序) (少用)

try {
    Field field = q.getClass().getDeclaredField("queue");
    field.setAccessible(true);
    Object []obj = (Object[]) (field.get(q));
    for (Object o : obj) {
         System.out.println(o);
    }
} catch (NoSuchFieldException e) {
     e.printStackTrace();
} catch (IllegalAccessException e) {
     e.printStackTrace();
}

4. 优先队列的自动化排序机制


4.1 单一排序 升序

单一排序是指仅仅以一个参数作为标准来进行比较,当创建优先队列的对象没有指定比较器时,如果是可比较的类型(String、int等),那么队列会默认以升序进行排列,比如:

PriorityQueue<Integer> q2 = new PriorityQueue<>();
q2.add(1);
q2.add(3);
q2.add(2);
while(!q2.isEmpty())
	System.out.println(q2.poll());

运行结果

1
2
3

自定义类的比较器实现

public static void main(String[] args) {
    PriorityQueue<Student> q = new PriorityQueue<>(new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getScore() - o2.getScore() > 0 ? 1 : -1;
        }
    });
    q.offer(new Student("大雄", 'M', 0.0));
    q.offer(new Student("哆啦A梦", 'M', 100.0));
    q.offer(new Student("静香", 'M', 96.0));
    q.offer(new Student("胖虎", 'W', 60.0));
    q.offer(new Student("小夫", 'M', 90.0));

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

运行结果

Student{id='大雄', gender=M, score=0.0}
Student{id='胖虎', gender=W, score=60.0}
Student{id='小夫', gender=M, score=90.0}
Student{id='静香', gender=M, score=96.0}
Student

4.2 单一排序 降序

public static void main(String[] args) {
    PriorityQueue<Integer> q2 = new PriorityQueue<>(Collections.reverseOrder());
    q2.add(1);
    q2.add(3);
    q2.add(2);
    while(!q2.isEmpty())
        System.out.println(q2.poll());
}

运行结果

3
2
1

自定义类 比较器

这里其实不用调用 Collections.reverseOrder() 方法,直接修改compare的返回值也是可以的。

public static void main(String[] args) {
    PriorityQueue<Student> q = new PriorityQueue<>(Collections.reverseOrder(new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getScore() - o2.getScore() > 0 ? 1 : -1;
        }
    }));
    q.offer(new Student("大雄", 'M', 0.0));
    q.offer(new Student("哆啦A梦", 'M', 100.0));
    q.offer(new Student("静香", 'M', 96.0));
    q.offer(new Student("胖虎", 'W', 60.0));
    q.offer(new Student("小夫", 'M', 90.0));
    while (!q.isEmpty())
        System.out.println(q.poll());
}

4.3 综合排序

假设 " 女士优先 " ,先把女孩子都排在前面,然后再根据成绩排序,实现如下:

public static void main(String[] args) {
    PriorityQueue<Student> q = new PriorityQueue<>(new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            if(!o1.getGender().equals(o2.getGender()))
                return o1.getGender().equals('W') ? -1 : 1;
            else
                return o1.getScore() - o2.getScore() > 0 ? -1 : 1;
        }
    });
    q.offer(new Student("大雄", 'M', 0.0));
    q.offer(new Student("哆啦A梦", 'M', 100.0));
    q.offer(new Student("静香", 'M', 96.0));
    q.offer(new Student("胖虎", 'W', 60.0));
    q.offer(new Student("小夫", 'M', 90.0));
    while (!q.isEmpty())
        System.out.println(q.poll());
}

运行结果

Student{id='胖虎', gender=W, score=60.0}
Student{id='哆啦A梦', gender=M, score=100.0}
Student{id='静香', gender=M, score=96.0}
Student{id='小夫', gender=M, score=90.0}
Student

emmmm,笔者不小心把胖虎性别弄成女孩了,问题不大...

5. 总结

  • 优先队列是基于队列 FIFO 先进先出的一种数据结构
  • 优先队列 通过比较器 Comparator 为元素设置优先级,队列内的元素会按优先级的高低排序
  • 优先队列的底层是通过堆排序实现的
  • 除了出队方式遍历以外,其他方式遍历的优先队列结果都将是无序的,只能通过出队的方式才是有序的
  • 优先队列默认是升序,降序的话可以通过Collections.reverseOrder()方法获得一个降序的比较器
  • 继承于 AbstractQueue 类 ,直接实现了 java.io.Serializable接口 表示可序列化,间接实现Queue接口
  • remove()方法不会抛异常
posted @ 2021-11-22 20:31  Unirithe  阅读(307)  评论(0)    收藏  举报