Java数据结构——堆和优先级队列

优先级队列

按照优先级的大小动态出队(动态指的使元素个数动态变化,而非固定

普通队列:FIFO按照元素的入队顺序出队,先入先出

现实生活中的优先级队列PriorityQueue

  1. 医生根据病情排手术

    病情情况相同先来先排,病情较重,优先安排

    和排序有什么关系?能否按照病情排序,然后按排序后的病人数组安排手术?

    排序是数组元素个数确定的情况,而排手术时,病人的人数都在动态变化

  2. 操作系统中的任务调度

    系统的任务优先级一般都比普通的应用要高

    CPU、内存等资源有限,当资源不够时,优先让优先级较高的应用获取资源

时间复杂度比较

入队出队(出当前队列 的最大值)
普通链式队列O(1)O(n)
优先级队列(堆)O(log n)O(log n)

在计算机领域,若见到logN时间复杂度,近乎一定和“树”结构相关(并非一定要构造一棵树结构,算法过程逻辑上一定是一颗树)

归并,快速递归过程也是一个“递归”树 nlogn

基于二叉树的堆称为二叉堆,应用最广的堆

本节讲的二叉堆的特点:

  1. 是一颗完全二叉树,基于数组存储(元素都是靠左排列,数组中存储时不会浪费空间),普通二叉树要存储空节点,浪费大量空间,因此只有完全二叉树适合用数组这种结构存储,其他的二叉树都要用链式结构

  2. 关于节点值:

    堆中根节点值>=子树中节点值(最大堆,大根堆)

    堆中根节点值<=子树中节点值(最小堆,小根堆,JDK中的PriorityQueue默认是基于最小堆的实现)

    即满足 完全二叉树

    ​ 根>=子树 / 根<=子树(从上至下每个树)

​ 注意:节点的层次和节点的大小没有关系,即并不是高层的节点一定大于低层的。因此只能保证在当前树中,树根是最大值,其他节点层次大小关系不确定

  1. 因为堆是基于数组的层序遍历来存储的,节点之间的关系通过数组下标来表示 ,即从0开始编号

    假设此时节点编号为i,且存在父子节点:

    • ​ 父节点编号parent = (i-1)/ 2;

    • ​ 左子树的编号 left = 2*i+1;

    • ​ 右子树的编号 right = 2*i+2;

    节点之间通过索引下标来找到父子节点

基于动态数组ArrayList实现的最大堆

核心方法(三个)

1. 向堆中添加元素——核心 siftUP上浮操作:

​ 堆是数组实现的,因此添加元素时先将元素放在数组的最后一位即size处

​ 此时这个堆仍然满足完全二叉树的性质,但此时这个完全二叉树就不满足堆的条件了,需要进行上浮操作

​ 不断将此时的索引k和故节点的索引i对应的元素进行大小关系比较,若大于父节点的值则交换彼此的节点值,直到当前节点值<=父节点值 或 走到树根为止

​ 上浮终止条件:

​ 当前已经上浮到树根 =》这个元素一定是最大值(k>0表示还有父节点,且当前元素值>父节点值)

​ 当前元素<=父节点对应

siftUp(int k){
    while(k>0 && data[k]>data[parent(k)]){
        swap(k,parent(k));
        k = parent(k);
    }
}

2. 在最大堆中取出最大值——siftDown下沉操作

最大堆的最大值一定处在树根结点,直接取出树根即可

需要融合左右两个子树,使得取出树根后这颗树仍是最大堆(此时融合操作比较复杂,因为左右子树大小关系不确定,且结点的大小和层次没有必然联系)

siftDown思路:

1)直接取根节点作为当前堆的最大值

2)将堆中最后一个元素顶到堆顶,然后进行元素的下沉操作,使其仍满足最大堆性质

3. heapify——堆化操作

任意一个整型数组,将其调整成最大堆,即任意整型数组都可以看作一个 完全二叉树,距离最大堆只差元素调整操作

方法步骤:

1)将这n个元素依次调用add方法添加到一个新的最大堆中,遍历原数组,创建一个新的最大堆,调用最大堆的add方法即可

时间复杂度O(nlogn) 空间复杂度O(n)

2)原地heapify

从最后一个非叶子节点开始进行元素siftDown操作(从当前二叉树中最后一个小子树开始调整,不断将子树调整为最大堆,因此走到树根时,左右子树已经全都是最大堆)

最后一个叶子结点是size-1,故最后一个非叶子结点就是最后一个叶子结点的父亲结点

/**
 * 基于动态数组实现的最大堆
 **/
public class MaxHeap {
    // 实际存储元素的数组
    private List<Integer> elementData;
    // 当前堆中元素个数
    private int size;

    public MaxHeap() {
        this(10);
    }

    public MaxHeap(int size) {
        elementData = new ArrayList<>(size);
    }

    /**
     * 将任意的整型数组arr调整为堆
     * @param arr
     */
    public MaxHeap(int[] arr) {
        elementData = new ArrayList<>(arr.length);
        // 1.先将所有元素复制到data数组中
        for (int i : arr) {
            elementData.add(i);
            size ++;
        }
        // 2.从最后一个非叶子结点开始进行siftDown操作
        // 最后一个叶子结点是size-1,故最后一个非叶子结点就是最后一个叶子结点的父亲结点
        for (int i = parent(size - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

    /**
     * 向堆中添加值为val的元素
     * @param val
     */
    public void add(int val) {
        // 1.直接向数组末尾添加元素
        elementData.add(val);
        size ++;
        // 2.进行元素的上浮操作
        siftUp(size - 1);
    }

    /**
     * 获取堆的最大值
     * @return
     */
    public int extractMax() {
        if (size == 0) {
            throw new NoSuchElementException("heap is empty!cannot extract!");
        }
        // 树根就是最大值结点
        int max = elementData.get(0);
        // 将数组的末尾元素顶到堆顶
        elementData.set(0,elementData.get(size - 1));//将索引0处的元素值设为索引size-1处的值
        // 将数组的最后一个元素从堆中删除
        elementData.remove(size - 1);
        size --;
        // 进行元素的下沉操作,从索引为0开始
        siftDown(0);
        return max;
    }

    /**
     * 返回堆中最大值
     * @return
     */
    public int peekMax() {
        if (isEmpty()) {
            throw new NoSuchElementException("heap is empty!cannot peek!");
        }
        return elementData.get(0);
    }

    /**
     * 元素下沉
     * @param k
     */
    private void siftDown(int k) {
        // 存在子树
        while (leftChild(k) < size) {
            int j = leftChild(k);
            // 存在右子树且右子树的值还比左子树大
            if (j + 1 < size && elementData.get(j + 1) > elementData.get(j)) {
                j ++;
            }
            // j为左右子树的最大值索引
            if(elementData .get(k) >= elementData.get(j)) {
                // 当前元素 >= 左右子树最大值,下沉结束,元素k落在了最终位置
                break;
            }else {
                swap(k,j);
                k = j;
            }
        }
    }

    /**
     * 元素上浮
     * @param k
     */
    private void siftUp(int k) {
        while (k > 0 && elementData.get(k) > elementData.get(parent(k))) {
            swap(k,parent(k));
            k = parent(k);
        }
    }

    /**
     * 交换
     * @param k
     * @param parent
     */
    private void swap(int k, int parent) {
        int child = elementData.get(k);
        int parentVal = elementData.get(parent);
        elementData.set(k,parentVal);
        elementData.set(parent,child);
    }


    public int getSize() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 找到结点k对应的父节点的索引
     * @param k
     * @return
     */
    private int parent(int k) {
        return (k - 1) >> 1;
    }

    /**
     * 找到结点k对应的左子树的索引
     * @param k
     * @return
     */
    private int leftChild(int k) {
        return (k << 1) + 1;
    }

    /**
     * 找到结点k对应的右子树的索引
     * @param k
     * @return
     */
    private int rightChild(int k) {
        return (k << 1) + 2;
    }

    @Override
    public String toString() {
        return elementData.toString();
    }
}

拓展:元素的比较——策略模式

策略模式:把Student类的大小关系比较从Student类中“解耦”,此时的比较策略非常灵活,需要哪种方式只需要创新一个类的实现Comparator接口即可,根据不同的大小关系的需求调用不同的比较器对象。(遵循开闭原则)

如Arrays.sort方法,就是根据比较器c的规则给不具备比较能力的某个对象数组a排序
在这里插入图片描述

基于最大堆的优先级队列

实现

/**
 * 基于最大堆的优先级队列
 */
public class PriorityQueue implements Queue<Integer> {

    private MaxHeap heap;

    public PriorityQueue(){
        heap = new MaxHeap();
    }

    // 向优先级队列中添加元素,就是最大堆中的添加并上浮操作
    @Override
    public void offer(Integer val) {
        heap.add(val);
    }

    // 在优先级队列中,每次的出队元素就是堆中最大值
    @Override
    public Integer poll() {
        return heap.extractMax();
    }

    @Override
    public Integer peek() {
        return heap.peekMax();
    }

    @Override
    public boolean isEmpty() {
        return heap.isEmpty();
    }

    public static void main(String[] args) {
        int[] data = new int[]{3,5,7,6,2,1,9,4};
        Queue<Integer> queue = new PriorityQueue();
        for (int i:
             data) {
            queue.offer(i);
        }
        System.out.println(queue.poll());

    }
}

注意:JDK中默认优先级队列是一个最小堆,需要改造成一个最大堆

PriorityQueue<Integer> queue = new PriorityQueue<>();
// 1. 使用比较器
class IntegerReverse implements Comparator<Integer>{
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2-o1;
    }
}
// 传入比较器对象
PriorityQueue<Integer> queue1 = new PriorityQueue<>(new IntegerReverse());

// 2. 使用匿名内部类改造成最大堆
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });
// 3. 函数式编程:Lambda表达式——优化匿名内部类
PriorityQueue<Integer> queue = new PriorityQueue<>((o1,o2)->o2-o1);

TopK问题

与一组数据中的前/后/最大的/最小的k个数有关的问题

​ TopK问题都可以使用优先级队列,取大用小,取小用大:

若需要取出前k个最大元素,构造最小堆

若需要取出前k个最小元素,构造最大堆

leetcode面试题17.14

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

思路:

  1. 排序,依次取出前k个数 时间复杂度O(nlogn)
  2. 若要时间复杂度优于时间复杂度O(nlogn),则使用优先级队列,构造最大堆,此时时间复杂度为O(nlogk) k<<n

随着堆顶元素不断交换,会把堆顶元素不断变小,最终队列扫描结束就存放了最小的k个数

public int[] smallestK(int[] arr, int k){
        int[] ret = new int[k];
        // 注意边界条件
        if(arr.length == 0 || k == 0){
            return ret;
        }
        PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });

        for (int i = 0; i < arr.length; i++) {
            	
                if(queue.size()<k){
                    queue.offer(arr[i]);
                }else {
                    if(arr[i]<queue.peek()){
                        queue.poll();
                        queue.offer(arr[i]);
                    }
                }

        }
        int i = 0;
        while (!queue.isEmpty()){
            ret[i] = queue.poll();
            i++;
        }
        return ret;
    }

匿名内部类

所谓内部类就是一个类嵌套到另一个类的内部的操作

匿名内部类

PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2-o1;
    }
});

Comparator是一个接口,接口无法直接实例化对象,必须由其子类进行向上转型进而对接口实例化,因此new Comparator接口实际上就是

class 没名字 implements Compatator{}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值