堆 和 优先级队列

目录

一、堆

二、优先级队列 

1、初识优先级队列 

2、实现一个优先级队列 

3、PriorityQueue 

(1)实现了Comparable接口,重写了compareTo方法

(2)实现了Comparator接口,重写了compare方法

4、 PriorityQueue 的应用

Top-k问题:使用 PriorityQueue 来做


一、堆

  1. 堆 是一种数据结构,是在完全二叉树的基础上进行了一些调整。
  2. 堆 分为大根堆 和 小根堆,根结点比左右结点都大的堆叫做 大根堆,根结点比左右结点都小的堆叫小根堆。
  3. 堆 的存储结构(物理结构):顺序存储,即会有一个数组,按照层序的方式顺序存储
  4. 若 父结点下标为 i,则左孩子下标为 2*i+1,右孩子下标为2*i+2;若 子结点下标为 i,则父结点下标为 (i-1)/2

二、优先级队列 

1、初识优先级队列 

优先级队列(PriorityQueue)底层是小根堆。

2、实现一个优先级队列 

采用顺序存储的结构,使用数组来实现。

建堆的两种方法:

向下调整建堆的时间复杂度是O(n) ,向上调整建堆的时间复杂度是O(n*logn)

1、先给elem数组赋值,创建一棵完全二叉树。然后从最后一棵子树的根结点开始调整,将每棵子树都调整成小根堆。调整的方案是向下调整(shiftDown)

shiftDown的时间复杂度:O(log n)

2、从无到有,不先创建完全二叉树,而是每放一个数据,都需要调整成小根堆。调整的方案是向上调整(shiftUp)

shiftUp的时间复杂度: O(log n)

对于优先级队列来说,入队和出队的时间复杂度 O(log n)  

import java.util.Arrays;

//底层是小根堆
public class MyPriorityQueue {
    public int[] elem;
    public int size;//数组的有效长度
    public static final int DEFAULT_SIZE = 10;
    public MyPriorityQueue(){
        this.elem = new int[10];
    }
    /**
     * 2、建堆第二种方法:从无到有,不先创建完全二叉树,而是每放一个数据,都需要调整成小根堆。
     */
    public void createHeap2(int data){
        if(isEmpty()){
            elem[0] = data;
            size++;
            return;
        }
        if(isFull()){
            elem = Arrays.copyOf(elem,elem.length*2);
        }
        elem[size] = data;
        size++;
        shiftUp(size-1);
    }
    /**
     * 1、建堆第一种方法:先给elem数组赋值,创建一棵完全二叉树。然后从最后一棵子树的根结点开始调整,将每棵子树都调整成小根堆。
     */
    //给数组赋值,创建了一棵完全二叉树
    public void createTree(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            this.elem[i] = arr[i];
            this.size++;
        }
    }
    //将完全二叉树调整成小根堆
    public void createHeap(){
        //从最后一个根结点开始往前调整
        int parent = ((size-1)-1)/2;
        for (int i = parent; i >= 0 ; i--) {
            shiftDown(i);
        }
    }
    //将根为parent的树调整为小根堆
    public void shiftDown(int parent){
        int child = 2*parent+1;
        while(child < size){
            //child+1 < size 保证有右树
            if(child+1 < size && elem[child+1] < elem[child]){
                child++;
            }
            //走到这,child 是左右子树最小值的下标
            if(elem[child] < elem[parent]){
                swap(child,parent);
                parent = child;
                child = 2*parent+1;
            }else{
                break;
            }
        }
    }
    public void swap(int x,int y){
        int tmp = elem[x];
        elem[x] = elem[y];
        elem[y] = tmp;
    }
    //入队:时间复杂度 O(log n)
    public void offer(int data){
        if(isFull()){
            elem = Arrays.copyOf(elem,elem.length*2);
        }
        elem[size] = data;
        size++;
        shiftUp(size-1);
    }
    public boolean isFull(){
        return size == this.elem.length;
    }
    public void shiftUp(int child){
        int parent = (child-1)/2;
        while(child > 0){
            if(elem[child] < elem[parent]){
                swap(child,parent);
                child = parent;
                parent = (child-1)/2;
            }else{
                break;
            }
        }
    }
    //出队:时间复杂度 O(log n)
    public int poll(){
        if(isEmpty()){
            return -1;
        }
        int delete = elem[0];
        swap(0,size-1);
        size--;
        //这样就只需要将parent=0的这棵树调整为小根堆就行了
        shiftDown(0);
        return delete;
    }
    public boolean isEmpty(){
        return size == 0;
    }
    //获取队顶元素但不删除
    public int peek(){
        if(isEmpty()){
            return -1;
        }
        return elem[0];
    }

}

3、PriorityQueue 

  1. PriorityQueue 底层是小根堆。
  2. PriorityQueue 没有传数组容量时,默认的初始容量是11;如果传容量,不能<1,否则
    会抛 IllegalArgumentException 异常
  3. PriorityQueue 放入的数据必须得能比较大小,即插入的数据要么实现了Comparable<T>接口,要么 实现了Comparator<T>接口,否则会抛出 ClassCastException异常
  4. PriorityQueue 放入的数据如果实现了比较器,要把比较器传过去,优先使用比较器来比较
  5. PriorityQueue 不能插入null对象,否则会抛出NullPointerException异常
  6. PriorityQueue 底层会自动扩容,容量<64时会2倍扩容,容量>=64时会1.5倍扩容

(1)实现了Comparable<T>接口,重写了compareTo方法

import java.util.PriorityQueue;
class Student implements Comparable<Student>{
    public int age;
    public Student(int age){
        this.age = age;
    }

    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }
}
public class Test {
    public static void main(String[] args) {
        PriorityQueue<Student> priorityQueue = new PriorityQueue<>();
        priorityQueue.offer(new Student(20));
        priorityQueue.offer(new Student(10));
        priorityQueue.offer(new Student(30));
        System.out.println();
    }
}

我们发现,数据确实有序了,而且是小根堆的形式。当然,我们也可以把它变成大根堆。

我们将  return this.age - o.age;改成 return o.age - this.age; 后,就变成了大根堆的形式。

(2)实现了Comparator<T>接口,重写了compare方法

那么,Integer怎么变成大根堆形式呢,Integer的compareTo方法是在源码里写好的,我们改不了源码。于是就用到了比较器,传个比较器过去,在比较器里重写compare方法,就可以改了。

class IntCmp implements Comparator<Integer>{
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}
public class Test {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());
        priorityQueue.offer(20);
        priorityQueue.offer(10);
        priorityQueue.offer(30);
        System.out.println();
    }
}

我们将  return o1.compareTo(o2);改成return o2.compareTo(o1);后,就变成了大根堆的形式。

4、 PriorityQueue 的应用

Top-k问题:使用 PriorityQueue 来做

求最的前k个元素或第k大的元素,就 将前 k 个数建立成小根堆;

求最的前k个元素或第k小的元素,就 将前 k 个数建立成大根堆。

时间复杂度:O(n*logk)

原因:

堆的大小是k,数组中元素的个数是n,

建立大小为k的堆的时间复杂度是O(k*logk),

遍历剩下的n-k个元素,每个都和堆顶元素进行比较,的时间复杂度是O((n-k)*logk)

所以,时间复杂度是O(n*logk)

(1)数据集合中最大或最小的前k个元素(一般情况下,数据量都会非常大。)

如:找出数组中最小的k个数,以任意顺序返回这k个数均可。 

解题思路: 

  1. 将前 k 个数建立成大根堆
  2. 从第 k+1 个数据开始,每次都和堆顶元素比较,如果比堆顶元素小,就弹出堆顶元素,把这个元素放进堆
  3. 那么最后,堆中的这k个元素就是数组中最小的k个数

为什么找 最小的k个数 要建大根堆呢?

因为,大根堆的堆顶元素是最大的,

若后面的数据比它大,说明肯定不属于 最小的k个数;若后面的数据比它小,说明它肯定属于 最小的k个数,那么就把它弹出,把这个数据放进去。以此类推。最后这个k大小的堆中就是最小的k个数了。

 //找出数组中最小的k个数
    public static int[] topK(int[] arr,int k){
        if(arr == null || k == 0) {
            return new int[0];
        }
        PriorityQueue<Integer> min = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        for (int i = 0; i < k; i++) {
            min.offer(arr[i]);
        }
        //到这里,建了一个大小为3的 大根堆
        for (int i = k; i < arr.length; i++) {
            int peek = min.peek();
            if(arr[i] < peek){
                min.poll();
                min.offer(arr[i]);
            }
        }
        //走到这,min 里就是数组中最小的k个数
        int[] tmp = new int[k];
        for (int i = 0; i < k; i++) {
            tmp[i] = min.poll();
        }
        return tmp;
}

(2)第k小/第k大的元素

如:找出数组中第k小的元素

解题思路:

  1. 将前 k 个数建立成大根堆
  2. 从第 k+1 个数据开始,每次都和堆顶元素比较,如果比堆顶元素小,就弹出堆顶元素,把这个元素放进堆
  3. 那么最后,堆顶元素就是第k小的元素
//找出数组中第k小的数
    public static int least(int[] arr,int k){
        if(arr == null || k == 0) {
            return -1;
        }
        PriorityQueue<Integer> min = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        for (int i = 0; i < k; i++) {
            min.offer(arr[i]);
        }
        //到这里,建了一个大小为3的 大根堆
        for (int i = k; i < arr.length; i++) {
            int peek = min.peek();
            if(arr[i] < peek){
                min.poll();
                min.offer(arr[i]);
            }
        }
        //走到这,min 里就是数组中最小的k个数
        return min.peek();
}
  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值