当前位置:   article > 正文

【数据结构-JAVA】堆和优先级队列

【数据结构-JAVA】堆和优先级队列

前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队 列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。 在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。

JDK1.8中的 PriorityQueue 底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。 下面将详细介绍堆。

1. 堆

1.1 堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一 个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大 堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质: 1. 堆中某个节点的值总是不大于或不小于其父节点的值。 2. 堆总是一棵完全二叉树。

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,而对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节 点,就会导致空间利用率比较低。 将元素存储到数组中后,假设 i 为节点在数组中的下标,则有: 如果 i 为0,则 i 表示的节点为根节点,否则 i 节点的双亲节点为 (i - 1)/2 如果 2 * i + 1 小于节点个数,则节点i的左孩子下标为 2 * i + 1 ,否则没有左孩子 如果 2 * i + 2 小于节点个数,则节点i的右孩子下标为 2 * i + 2 ,否则没有右孩子

1.2 堆的创建

堆是存储在一个一维数组中的,那么怎么用通过一个数组,让其变成一个堆呢?先简单介绍一下向下调整形成大根堆的思路:由于输入的数组是随机的,其对应的二叉树,也并不总是父亲节点的值大于孩子节点,所以我们要做的就是调整位置,让父亲节点的值与左右孩子节点的最大值做比较,小了,就与孩子节点交换位置,而大了,不做改变,从而让这个二叉树变成大根堆。

1.2.1 堆向下调整

  1. public class TextHeap {
  2. public int[] elem;
  3. public int usedSized;
  4. public TextHeap() {
  5. this.elem = new int[10];
  6. }
  7. public void initElem(int[] arr){
  8. for (int i = 0; i < arr.length; i++) {
  9. elem[i] = arr[i];
  10. usedSized++;
  11. }
  12. }
  13. /*
  14. //创建大根堆
  15. */
  16. public void createHeap(){
  17. //找到这个完全二叉树中,最后一个父亲节点的下标:
  18. //最后一个节点的父亲节点,也是该二叉树的最后一个父亲节点,以此开始
  19. for (int parent = (usedSized - 1 - 1 )/ 2; parent >= 0 ; parent--) {
  20. //使用 usedSize 作为向下调整的最大边界
  21. shiftDown(parent,usedSized);
  22. }
  23. }
  24. private void shiftDown(int parent,int len){
  25. //最起码有左孩子
  26. int child = 2 * parent + 1;
  27. // 可能需要多次向下调整:
  28. while(child < len){
  29. //如果有右孩子时,得让 child 这个下标对应左右孩子最大值的下标
  30. if(child + 1 < len && elem[child] < elem[child + 1]){
  31. child++;
  32. }
  33. if(elem[child] > elem[parent]){
  34. int temp = elem[child];
  35. elem[child] = elem[parent];
  36. elem[parent] = temp;
  37. parent = child;
  38. child = 2 * parent + 1;
  39. }else{
  40. break;
  41. }
  42. }
  43. }
  44. }

1.3 堆向下调整创建堆的时间复杂度

按照最坏的情况来看,即创建一棵高度为 h 的满二叉树的话,这样的堆的时间复杂度该如何计算呢?

高度为 h ,那么最后一层就是第 h 层,该层的节点总数为 ,这一层都是叶子节点,并不需要向下调整。而倒数第二层,即 h - 1 层,该层的节点总数为 ,而这里的每一个节点都需要向下调整 1 次。以此类推到第一层。

由② - ① 得:

化简得:

又因为总节点数 n 满足:

所以有

所以时间复杂度为

1.4 堆向上调整创建堆的时间复杂度

② - ① 得

化简得:

最后:

因此时间复杂度为:

1.5 堆的插入

将要插入的元素放在堆的最后位置,向上调整,时间复杂度为二叉树的高度,只需一路向上,跟父亲节点比较交换即可。

  1. private void shiftUp(int child){
  2. int parent = (child - 1)/2;
  3. while(child > 0){
  4. if(elem[child] > elem[parent]){
  5. int temp = elem[child];
  6. elem[child] = elem[parent];
  7. elem[parent] = temp;
  8. child = parent;
  9. parent = (child - 1)/2;
  10. }else{
  11. break;
  12. }
  13. }
  14. }
  15. //堆的插入
  16. public void offer(int val){
  17. if(isFull()){
  18. //满了就扩容
  19. elem = Arrays.copyOf(elem,elem.length+5);
  20. }
  21. elem[usedSized++] = val;
  22. shiftUp(usedSized - 1);
  23. }
  24. private boolean isFull(){
  25. return usedSized == elem.length;
  26. }

1.6 堆的删除

堆的删除,只能删除根节点元素。删除的思路:把根节点元素与堆中最后一个元素交换,usedSize--,同时向下调整。

  1. //堆的删除
  2. public void pop(){
  3. if(isEmpty()){
  4. return;
  5. }
  6. elem[0] = elem[usedSized - 1];
  7. usedSized--;
  8. //只需从0下标的这个父亲节点去向下调整就可以了
  9. shiftDown(0,usedSized);
  10. }
  11. private boolean isEmpty(){
  12. return usedSized == 0;
  13. }

1.7 堆排序

堆排序:将存储堆的数组里的元素,从小到大或从大到小排序

从小到大,建大堆

  1. public class Text{
  2. public static void main(String[] args) {
  3. int[] arr = {1,4,6,235,15,75,123,61,70,13};
  4. TextHeap heap = new TextHeap();
  5. heap.initElem(arr);
  6. heap.createHeap();
  7. heap.heapSort();
  8. System.out.println();
  9. }
  10. }
  11. public class TextHeap{
  12. ........
  13. //省略的部分可往前文查找
  14. private void swap(int[] arr,int i ,int j){
  15. int temp = arr[i];
  16. arr[i] = arr[j];
  17. arr[j] = temp;
  18. }
  19. //堆排序
  20. public void heapSort(){
  21. int end = usedSized - 1;
  22. while(end > 0 ){
  23. swap(elem,0,end);
  24. shiftDown(0,end);
  25. end--;
  26. }
  27. }
  28. }

时间复杂度O(nlogn),空间复杂度O(1)。

而从大到小,则建小堆。这里就不再赘述。

2. 优先级队列(Priority Queue)

2.1 优先级队列的性质

Java集合框架中提供了 PriorityQueue 和 PriorityBlockingQueue 两种类型的优先级队列,PriorityQueue是线 程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。

关于PriorityQueue的使用要注意: 1. 使用时必须导入PriorityQueue所在的包,即:

import java.util.PriorityQueue;

2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常

比如以下代码,priorityQueue中存放的是Student的对象,此时这些对象并不能比较大小:

  1. public static void main(String[] args) {
  2. Queue<Student> priorityQueue = new PriorityQueue<>();
  3. priorityQueue.offer(new Student("huahua",15));
  4. priorityQueue.offer(new Student("liu",34));
  5. System.out.println(priorityQueue.poll());
  6. }

因此报错:

3. 不能插入null对象,否则会抛出NullPointerException

4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容 5. 插入和删除元素的时间复杂度为,也就是二叉树的高度 6. PriorityQueue底层使用了堆数据结构 7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素

  1. //检验一下 PriorityQueue 真的默认为小根堆?
  2. public static void main(String[] args) {
  3. Queue<Integer> priorityQueue = new PriorityQueue<>();
  4. priorityQueue.offer(1);
  5. priorityQueue.offer(88);
  6. System.out.println(priorityQueue.poll());
  7. }

2.2 top-k 问题

设计一个算法,找出数组中最小或最大的前k个数。以任意顺序返回这k个数均可。

2.2.1 排序思路——效率较低

将这个数组建成小根堆,拿这小根堆的前10个元素,但这种方法效率低下。

  1. //top-k问题
  2. //排序思路
  3. public int[] topK1(int[] arr,int k){
  4. int[] ret = new int[k];
  5. if(arr == null || k == 0){
  6. return ret;
  7. }
  8. Queue<Integer> priorityQueue = new PriorityQueue<>();
  9. for (int i = 0; i < arr.length; i++) {
  10. priorityQueue.offer(arr[i]);
  11. }
  12. for (int i = 0; i < k; i++) {
  13. ret[i] = priorityQueue.poll();
  14. }
  15. return ret;
  16. }

2.2.2 善于利用堆排序解决topk问题

如果是输出数组中前k个最大的元素,其实只要创建容量为k的优先队列即可,而不是像3.2.1那样,创建一个容量为arr.length的优先队列。

此时容量为k的优先队列默认为小根堆,让数组的前k个元素放入优先级队列中,之后让数组从第k+1个元素往后遍历,每一个都与小根堆的堆顶元素做比较。由于是小根堆,堆顶元素就是k个元素中,最小的一个,与数组第k+1个元素及以后的元素作比较,挨个置换比根节点大的元素,那么等数组遍历完之后,小根堆里存储的元素都比数组中其余元素要大,不然的话,还是会被置换出来的。如此一来,小根堆存储的就是数组中前k个最大元素。

  1. public static int[] topK2(int[] arr,int k){
  2. int[] ret = new int[k];
  3. if(arr == null || k == 0){
  4. return ret;
  5. }
  6. Queue<Integer> priorityQueue = new PriorityQueue<>(k);
  7. for (int i = 0; i < k; i++) {
  8. priorityQueue.offer(arr[i]);
  9. }
  10. for (int i = k; i < arr.length; i++) {
  11. if(arr[i] > priorityQueue.peek()){
  12. priorityQueue.poll();
  13. priorityQueue.offer(arr[i]);
  14. }
  15. }
  16. for (int i = 0; i < k; i++) {
  17. ret[i] = priorityQueue.poll();
  18. }
  19. return ret;
  20. }
  21. public static void main(String[] args) {
  22. int[] arr = {1,6,43,2,7,88,67,17,29,257,35};
  23. int[] ret = topK2(arr,3);
  24. System.out.println(Arrays.toString(ret));
  25. }

输出:

[67, 88, 257]

问题换一换,找第k大的元素?

其实就是堆顶元素啦~~

那如果要求的是前k个最小元素呢?这时候就要用到大根堆,可优先级队列默认是小根堆,那该如何在优先级队列中创建大根堆呢?

2.3 优先级队列构造方法

此处只是列出了PriorityQueue中常见的几种构造方式。

构造器

功能介绍

PriorityQueue()

创建一个空的优先级队列,默认容量是11

PriorityQueue(int initialCapacity)

创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异 常

PriorityQueue(int initialCapacity, Comparator<? super E> comparator)

创建一个初始容量为initialCapacity的优先级队列,并且有比较器

PriorityQueue(Collection<? extends E> c)

用一个集合来创建优先级队列

  1. public static void main(String[] args) {
  2. // 创建一个空的优先级队列,底层默认容量是11
  3. Queue<Integer> q1 = new PriorityQueue<>();
  4. // 创建一个空的优先级队列,底层的容量为 100
  5. Queue<Integer> q2 = new PriorityQueue<>(100);
  6. // 用ArrayList对象来构造一个优先级队列的对象
  7. // q3中已经包含了三个元素
  8. List<Integer> list = new ArrayList<>();
  9. list.add(1);
  10. list.add(45);
  11. list.add(23);
  12. Queue<Integer> q3 = new PriorityQueue<>(list);
  13. q3.offer(9);
  14. System.out.println(q3.size());
  15. }

输出:

4

2.4 对象的比较方式

优先级队列在插入元素时有个要求:插入的元素不能是null或者元素之间必须要能够 进行比较,为了简单起见,我们只是插入了Integer类型,那优先级队列中能否插入自定义类型对象呢?

优先级队列底层使用堆,而向堆中插入元素时,为了满足堆的性质,必须要进行元素的比较,而在优先级队列的性质那一小节中,优先级队列插入的是引用类型,是没有办 法直接进行比较的,因此抛出异常。因此在这里引出一个问题,对象的比较方式,以下介绍三种方法:

2.4.1 equals方法

  1. class Student{
  2. public String name;
  3. public int age;
  4. public Student(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. }
  9. public static void main(String[] args) {
  10. Student student1 = new Student("shanshan",15);
  11. Student student2 = new Student("shanshan",15);
  12. System.out.println(student1==student2);
  13. System.out.println(student1.equals(student2));
  14. }

输出:

false

false

==比较的是地址,输出是false;而当我们一开始直接就用equals时,返回的依旧是false。这是因为Student类中并无equals方法,而所有类都是Object类的子类,所以调用的就是Object类的equals方法:

我们发现,此处的equals方法也等同于==,比较的是引用变量的地址。

但是此处,我们需要比较的是对象中的内容,那该如何处理呢?基于此,我们可以覆写基类的equals方法。(可右击自动生成)

  1. class Student{
  2. public String name;
  3. public int age;
  4. public Student(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. @Override
  9. public boolean equals(Object o) {
  10. if (this == o) return true;
  11. if (o == null || getClass() != o.getClass()) return false;
  12. Student student = (Student) o;
  13. return age == student.age && Objects.equals(name, student.name);
  14. }
  15. @Override
  16. public int hashCode() {
  17. return Objects.hash(name, age);
  18. }
  19. }

输出:

false

true

覆写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行比较。

因此,优先级队列并不采用equals方法。

2.4.2 Compareble接口

Comparble是JDK提供的泛型的比较接口类,源码实现具体如下:

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }

对用用户自定义类型,如果要想按照大于、小于的方式进行比较时:在定义类时,实现Comparble接口即可,然后在类 中重写compareTo方法,举例如下:

  1. class Student implements Comparable<Student>{
  2. ......
  3. @Override
  4. public int compareTo(Student o) {
  5. return this.age - o.age;
  6. }
  7. }

但这样compareTo就只能比较年龄了,那要是想比较学生的成绩或是其他,该怎么办呢?

2.4.2 基于比较器比较

Comparator部分源代码:

  1. public interface Comparator<T> {
  2. int compare(T o1, T o2);
  3. ......
  4. }

按照比较器方式进行比较,具体步骤如下: 用户自定义比较器类,实现Comparator接口并且重写Comparator中的compare方法:

  1. //姓名比较器
  2. class nameComparator implements Comparator<Student>{
  3. @Override
  4. public int compare(Student o1, Student o2) {
  5. return o1.name.compareTo(o2.name);
  6. }
  7. }
  8. //年龄比较器
  9. class AgeComparator implements Comparator<Student>{
  10. @Override
  11. public int compare(Student o1, Student o2) {
  12. return o1.age - o2.age;
  13. }
  14. }

上面的compareTo方法中,因为name是String类的对象,可以调用String中的compareTo方法,这里比较的是两个字符串的大小。

  1. NameComparator nameComparator = new NameComparator();
  2. System.out.println(nameComparator.compare(student1,student2));

2.5 优先级队列的比较方式

集合框架中的PriorityQueue底层使用堆结构,因此其内部的元素必须要能够比大小,PriorityQueue采用了: Comparble和Comparator两种方式。 1. Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接 口,并覆写compareTo方法 2. 用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现 Comparator接口并覆写compare方法。

继续用上面Student类的例子,来演示如何将自定义类型对象插入优先级队列,以及如何创建大根堆:

Comparble接口:

  1. class Student implements Comparable<Student>{
  2. public String name;
  3. public int age;
  4. public Student(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. //为创建大根堆
  9. @Override
  10. public int compareTo(Student o) {
  11. return o.age - this.age;
  12. }
  13. }
  14. public static void main(String[] args) {
  15. Student student1 = new Student("shansan",10);
  16. Student student2 = new Student("shanan",15);
  17. PriorityQueue<Student> priorityQueue = new PriorityQueue<>();
  18. priorityQueue.offer(student1);
  19. priorityQueue.offer(student2);
  20. System.out.println("sssssssss");
  21. }

Comparator接口:

  1. class InCmp implements Comparator<Integer>{
  2. @Override
  3. public int compare(Integer o1, Integer o2) {
  4. //调用了Integer这个类中的compareTo方法
  5. return o2.compareTo(o1);
  6. //return o1 - o2;
  7. }
  8. }
  9. public static void main(String[] args) {
  10. InCmp inCmp = new InCmp();
  11. PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(inCmp);
  12. priorityQueue.offer(5);
  13. priorityQueue.offer(1);
  14. priorityQueue.offer(98);
  15. priorityQueue.offer(23);
  16. priorityQueue.offer(51);
  17. System.out.println("2222222222222222222");
  18. }

还可以使用匿名内部类的方法写:

  1. //一个实现了Comparator接口的匿名内部类
  2. PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>(new Comparator<Integer>() {
  3. @Override
  4. public int compare(Integer o1, Integer o2) {
  5. //小根堆
  6. return o1.compareTo(o2);
  7. }
  8. });

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/613400
推荐阅读
相关标签
  

闽ICP备14008679号