天天看点

Java 优先级队列(堆)

目录

    • 1. 优先级队列(堆)的概念
    • 2. 建立大根堆(向下调整算法)
    • 3. 堆插入元素(向上调整算法)
    • 4. 堆删除元素(向下调整算法)
    • 5. 优先级队列PriorityQueue的特性
    • 6. 优先级队列PriorityQueue的构造方法(默认小根堆)
    • 7. Java对象的比较
      • 1. euqals方法
      • 2. Comparable<>接口CompareTo方法(原类上实现)
      • 3. Compartor接口Compare方法(重写一个类实现)
      • 4. 三种方法的对比
    • 8. 大根堆的创建
    • 9. Top-k问题
      • 1.整体建小根堆
      • 2.局部建容量为k的大根堆

1. 优先级队列(堆)的概念

  1. 优先级队列PriorityQueue底层使用了堆这种数据结构,堆是一棵顺序存储的完全二叉树。
  2. 堆的性质

    ①堆中某个结点的值不大于/不小于其父节点的值(因为一旦称之为堆,则一定是大根堆or小根堆)

    ②堆是一棵完全二叉树(因为堆是顺序存储的二叉树,如果非完全二叉树就会存在null结点浪费了数组顺序存储的空间)

  3. 堆是一颗完全二叉树,采用按层编号进行顺序存储,十分的高效。那么假设i为结点在数组中的下标,则有:

    ①i的父结点下标为(i-1)/2

    ②i的左孩子下标为2i+1,i的右孩子下标为2i+2

2. 建立大根堆(向下调整算法)

思路:从最后一个非叶子结点开始进行向下调整,如果父节点的值已经满足大于两个孩子结点的值,则直接跳出循环调整下一个结点;否则就与孩子结点进行交换,交换完之后可能会造成子树不满足堆的性质,因此需要继续向下调整。

建堆的时间复杂度为O(N)

向下调整的时间复杂度为O(logN)

代码:

//建立大根堆(向下调整)
    public void createHeap() {
        //从最后一个非叶子结点开始进行向下调整
        for (int i = (usedsize-1-1) / 2; i >= 0; i--) {
            shiftDown(i,usedsize);
        }
    }
    public void shiftDown(int parent,int len) {
        int child = 2 * parent + 1;

        while (child < len) {
            if (child + 1 < len && elem[child] < elem[child +1]) {
                child = child + 1;
            }

            if (elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child;
                child = 2 * parent + 1;
            }else {
                break;
            }
        }
    }
           

3. 堆插入元素(向上调整算法)

思路:堆插入元素是直接插在有效元素的后面的【注定是最后一个叶子结点】,然后对这个叶子结点所在的位置进行向上调整,注意这个叶子不用在跟其兄弟叶子进行比较,只用跟其父结点进行比较。

向上调整的时间复杂度为O(logN)

向上调整建堆的时间复杂度为O(NlogN)

因此一般选用上下调整的算法建堆,因为最后2层的结点最多,要调整的元素也多,时间会变慢。

代码:

public void inset(int val) {
        if (isFull()) {
            addSize();
        }
        elem[usedsize++] = val;
        //向上调整算法
        shiftUp(usedsize-1);
    }
    public void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while (parent >= 0) {
            if (elem[parent] < elem[child]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }

    public boolean isFull() {
        if (elem.length == usedsize) {
            return true;
        }
        return false;
    }
    public void addSize() {
        elem = Arrays.copyOf(elem,elem.length*2);
    }
           

4. 堆删除元素(向下调整算法)

思路:每次删除的元素是堆顶的元素,将堆顶元素和堆中的最后一个元素进行交换,然后向下调整交换后的堆顶元素。

代码:

public void swap(int i, int j) {
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }
    public void deleElem() {
        if (usedsize <= 0) {
            return;
        }
        swap(0,usedsize-1);
        usedsize--;
        shiftDown(0,usedsize);
    }
           

5. 优先级队列PriorityQueue的特性

  1. PriorityQueue中放置的元素必须要可比较,否则就不能进行向上/下调整了,并且会抛出类型转换异常ClassCastException。
  2. 不能插入null对象,否则会抛出空指针异常NullPointerException。
  3. 插入和删除元素的时间复杂度是O(logN),即树的高度。
  4. PriorityQueue的底层使用堆数据结构,默认是小根堆,即每次获取到的元素都是最小的。

6. 优先级队列PriorityQueue的构造方法(默认小根堆)

  1. 无参构造方法实例化优先级队列对象

底层代码:

Java 优先级队列(堆)

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

  1. 传入一个整型参数实例化优先级队列对象

底层代码:

Java 优先级队列(堆)
Java 优先级队列(堆)

分析:会创建一个初始容量为initialCapacity的优先级队列,这里创建的容量是100,注意初始容量不能小于1,否则会抛出异常。

  1. 传入一个集合参数实例化优先级队列对象
LinkedList<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(list);
           

分析:用LinkedList对象来构造一个优先级队列的对象。

注意:实例化优先级对象默认创建的是小根堆,如果是大根堆需要提供比较器【涉及到对象的比较】。

7. Java对象的比较

在Java中,基本数据类型可以大于、小于、等于去比较,但是对于对象的比较则需要用到以下几种方法。

1. euqals方法

①如果自定义的类没有重写euqals方法,那么默认是继承的Object类中的equals方法,使用的是“==”。

Java 优先级队列(堆)

②自定义类重写equals方法

Java 优先级队列(堆)

注意:euqals只能按照相等进行比较,不能比较大于、小于。

测试:

Student student1 = new Student("张三",22);
        Student student2 = new Student("张三",22);
        System.out.println(student1.equals(student2));
           

如果没有在Student类中重写euqals方法,则输出false;如果重写了euqals方法,则输出true。

2. Comparable<>接口CompareTo方法(原类上实现)

Java 优先级队列(堆)

注意:

①在接口的后面要加上泛型

②这种写法侵入性比较强,不易修改

3. Compartor接口Compare方法(重写一个类实现)

class AgeComparator implements Comparator<Student> {

    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}
           

实例化构造器对象,并且调用构造器中的compare方法。

public static void main(String[] args) {
        Student student1 = new Student("张三",22);
        Student student2 = new Student("三",22);
        AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(student1, student2));
    }
           

4. 三种方法的对比

比较的方法 说明
Object.equals 不重写的话是继承Object的方法,需要重写覆写,只能比较相等与否
Comparable.compareTo 需要手动实现接口,不够灵活
Comparator.compare 需要实现一个比较器类对象

8. 大根堆的创建

优先级队列默认是小根堆,如果要创建大根堆,那么就传入比较方法。

比较器:(注意用到泛型)

class IntCompare implements Comparator<Integer> {

    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}

           

实例化比较器、优先级队列对象,并传入比较器参数

public class MaxHeap {
    public static void main(String[] args) {
        IntCompare intCompare = new IntCompare();
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(intCompare);
        priorityQueue.offer(3);
        priorityQueue.offer(2);
        priorityQueue.offer(9);
        priorityQueue.offer(1);
        System.out.println(priorityQueue);
    }
}
           

运行结果:

Java 优先级队列(堆)

9. Top-k问题

力扣 17.14. 最小K个数

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

示例:

输入: arr = [1,3,5,7,2,4,6,8], k = 4

输出: [1,2,3,4]

1.整体建小根堆

思路:因为有大量的数据需要排序,因此采用堆这种数据结构,把所有的数据全部建成一个小根堆,然后每次取堆顶的元素。

代码:

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        //小根堆
        for (int i = 0; i < arr.length; i++) {
            priorityQueue.offer(arr[i]);
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }
}
           

2.局部建容量为k的大根堆

思路:建立容量为k的大根堆,如果下一个待比较的元素比大根堆的元素小,就把大根堆的元素出堆,下一个待比较的元素进入堆中,这样最后得到的就是k个最小的元素。

【注意要判断数组为空 以及 k为0的情况,否则会出现空指针异常】

代码:

class IntComp implements Comparator<Integer> {

    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        if(arr.length == 0 || k == 0) {
            return ret;
        }
        //建立大根堆
        IntComp intComp = new IntComp();
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(intComp);
        for (int i = 0; i < k; i++) {
            priorityQueue.offer(arr[i]);
        }
        for (int i = k; i < arr.length; i++) {
            if (arr[i] < priorityQueue.peek()) {
                priorityQueue.poll();
                priorityQueue.offer(arr[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }
    
}