Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)

引入-二叉树的顺序存储

  • 如何顺序存储?就是用层序遍历将二叉树的节点一个个的读取出来,然后依次放入数组。这种存储方式只适合完全二叉树,非完全二叉树有些层的节点可能不满,放入数组会造成空间浪费。
    Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第1张图片
  • 以上顺序存储方式可以用来表示堆。

  • 堆是什么?堆就是一种数据结构,在逻辑上认为是一棵完全二叉树,但在物理上使用数组来存储的一种数据结构。简单来说,堆就是用数组实现的二叉树
  • 堆分为大根堆和小根堆。满足任意节点的值都大于其子树中节点的值,叫做大根堆。满足任意节点的值都小于其子树中节点的值,叫做小根堆。
  • 堆有什么用?堆最常见的作用,就是用来快速寻找一个集合或者一组数据中的最值,最大值或者最小值。
    Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第2张图片

堆属性

  • 堆分为大根堆和小根堆,也叫最大堆和最小堆。两者的差别在于节点的排序方式不同。
  • 最大堆中, 父节点的值比每一个子节点的值都要大。最小堆中,父节点的值比每一个子节点的值都要小。这种特性就叫堆属性,并且这个属性对堆中的每一个节点都成立。
  • 举例:

Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第3张图片

  • 这个堆是最大堆,因为每一个父节点的值都大于子节点。(10大于72 7又大于51
  • 注意:最大堆总是将最大的值放在根节点,最小堆总是将最小值放在根节点。但其他节点的大小和排序顺序是不确定的,最小的元素未必是最后一个叶子节点。只能确定的是父节点和子节点之间的大小。

堆的实现

  • 堆的实现需要用数组(就是二叉树的顺序存储。)
  • 这个数组中随便拿出一个元素就是二叉树中的一个节点,那么如何确定其父节点和子节点分别是哪些元素?这需要用到下标。根据父节点和子节点之间的下标关系来确定。

下标关系
Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第4张图片

  • 可以借助下图来理解数组索引和节点位置之间的关系:
    Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第5张图片
  • 上图中二叉树的数字就是对应数组中的下标。

堆的基本操作(向下调整&建堆)

堆中有两个基本操作:向上调整和向下调整。

  • shiftUp():如果一个节点比它的父节点大(最大堆)或者小(最小堆),那么需要将它同父节点交换位置。这样是这个节点在数组的位置上升。
  • shiftDown():如果一个节点比它的子节点小(最大堆)或者大(最小堆),那么需要将它向下移动。这个操作也称作“堆化(heapify)”。
   public void adjustDown(int root,int len) {
        int parent = root;
        int child = 2*parent+1;
        while(child < len) {
            //找到左右孩子的最大值
            //1、前提是你得有右孩子
            if(child+1 < len && this.elem[child] < this.elem[child+1]) {
                child++;
            }
            //保证,child下标的数据  一定是左右孩子的最大值的下标
            if(this.elem[child] > this.elem[parent]) {
                int tmp = this.elem[child];
                this.elem[child] = this.elem[parent];
                this.elem[parent] = tmp;
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

    public void adjustUp(int child) {
        int parent = (child-1)/2;
        while (child > 0) {
            if(this.elem[child] > this.elem[parent]) {
                int tmp = this.elem[parent];
                this.elem[parent] = this.elem[child];
                this.elem[child] = tmp;
                child = parent;
                parent = (child-1)/2;
            }else {
                break;
            }
        }
    }

插入操作
步骤:
1.将新元素插入到堆的末尾。
2.按照优先顺序,将新元素与其父节点比较,根据大小进行向上或者向下调整。
3.不断进行第2步操作,直到不需要交换新元素和父节点,或者达到堆顶。
4.最后得到一个最小堆。
Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第6张图片
代码如下:

    public void push(int val) {
        if(isFull()) {
            //扩容
            this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
        }
        this.elem[this.usedSize] = val;//10
        this.usedSize++;//11
        adjustUp(this.usedSize-1);//10下标
    }

    public boolean isFull() {
        return this.usedSize == this.elem.length;
    }

删除操作
堆的删除与插入操作相反,插入是将元素从下往上调整,而删除是将元素从上往下调整
步骤:
1.删除堆定元素
2.比较左右节点的元素,将小的元素上调(向上或向下调整)
3.不断进行步骤2,知道不需要调整或者调整到堆底。
Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第7张图片

    public void pop() {
        if(isEmpty()) {
            return;
        }
        int tmp = this.elem[0];
        this.elem[0] = this.elem[this.usedSize-1];
        this.elem[this.usedSize-1] = tmp;
        this.usedSize--;//9 删除了
        adjustDown(0,this.usedSize);
    }

    public int peek() {
        if(isEmpty()) {
           throw new RuntimeException("队列为空");
        }
        return this.elem[0];
    }

    public boolean isEmpty() {
        return this.usedSize == 0;
    }

建堆操作
创建一个数组,初始化堆,然后调整堆中的数据,不断向上或者向下,最后调整成堆。
以大堆为例:
Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第8张图片
步骤:
Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第9张图片
代码如下:

import java.util.Arrays;

public class TestHeap {

    public int[] elem;
    public int usedSize;

    public TestHeap() {
        this.elem = new int[10];
    }
    //建大堆
    public void createHeap(int[] array) {

        for (int i = 0; i < array.length; i++) {
            this.elem[i] = array[i];
            this.usedSize++;
        }
        //parent 就代表每颗子树的根节点
        for(int parent = (array.length-1-1)/2;parent >= 0;parent--) {
            //第2个参数  每次调整的结束位置应该是:this.usedSize.
            adjustDown(parent,this.usedSize);
        }
    }

}

优先级队列

  • 队列Queue本身是先入先出的结构,但是某些情况下,数据本身可能带有优先级,就需要先处理优先级更高的数据。比如玩游戏的时候受到电话,那么应该先处理电话,因为电话优先级高
  • 这种时候的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,另一个是添加新对象。这种数据结构就是优先级队列(PriorityQueue)
  • 优先级队列有很多实现方式,最常见的就是用堆来实现。

PriorityQueue的使用

  • JAVA集合框架中提供了PriorityQueue和PriorityBlockingQueue两种优先级队列。前者线程不安全,后者线程安全。这里主要使用PriorityQueue。
  • PriorityQueue集合底层是用堆数据结构实现的。
  • PriorityQueue中放置的元素必须是能够比较大小(只有实现了 Comparable 和 Comparator 接口的类才能比较大小),不能插入无法比较大小的对象,否则会抛出异常。
  • PriorityQueue中可以插入任意多的元素,因为可以自动扩容,但是不能为null对象。
  • PriorityQueue默认是小根堆。
  • PriorityQueue常见的构造方式如下:

Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第10张图片

  • PriorityQueue常用方法:
    Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第11张图片
    使用:
 public static void main(String[] args) {

        //创建一个空的优先级队列,默认底层容量是11
        PriorityQueue<Integer> queue1=new PriorityQueue<>();

        //创建一个空的优先级队列
        PriorityQueue<Integer> queue2=new PriorityQueue<>(50);

        ArrayList<Integer> list=new ArrayList<>();
        list.add(4);
        list.add(0);
        list.add(2);
        list.add(3);

        //用ArrayList集合来创建一个优先级队列的对象。
        PriorityQueue<Integer> queue3=new PriorityQueue<>(list);
        System.out.println(queue3.size());
        System.out.println(queue3.peek());//默认是最小堆,所以优先级最高的是最小元素


        System.out.println(queue3.poll()); //0 优先级最高的元素弹出。
//        for(Integer x:queue3){
//            System.out.println(queue3.poll());
//        }
        System.out.println("优先级队列是否为空:"+queue3.isEmpty());

        queue1.addAll(list);
        System.out.println(queue1.isEmpty());
        queue1.clear(); //清空队列
        System.out.println(queue1.isEmpty());
    }

TopK问题

最小的k个数
Java~数据结构(五)~优先级队列(堆的基本概念、操作及实现&优先级队列、PriorityQueue的使用、TopK问题、堆排序)_第12张图片
思路:将所有元素放入优先级队列,返回前k个。

class Solution {
    public int[] smallestK(int[] arr, int k) {
            int[] ret=new int[k];
            if(arr ==null || k<=0){
                return ret;
            }
            PriorityQueue<Integer> queue=new PriorityQueue<>();
            for(int i=0;i<arr.length;i++){
                queue.offer(arr[i]);
            }
            for(int i=0;i<k;i++){
                ret[i]=queue.poll();
            }
            return ret;
    }
}

这只是一种,topk问题是在一组数据中求前k个最小元素或者前k个最大元素
比如在N个元素中求前K个最小元素,常见思路:

  • 1.建立大小为N的小堆,每次弹出堆定元素,弹K次
  • 2.建立大小为K的大堆:
    1)将待排序的前K个元素建成大根堆 。
    2)遍历剩下待排序元素,每拿到一个元素,就和堆定元素比较
    3)如果大于堆定元素,不用管,下一个 。
    4)如果小于堆定元素,弹出堆定,然后将这个元素放入堆中。每次放入和弹出都有堆的调整过程。

堆排序

  • 由于堆本身就是一个数组,可以利用堆的独特特性来将数组从小到达进行排序。时间复杂度O(nlogn)
  • 在排序中都学过,就不详细介绍了。
    /**
     * 一定是先创建大堆
     *      调整每棵树
     * 开始堆排序:
     *     先交换  后调整  直到 0下标
     */
    public void heapSort() {
        int end = this.usedSize-1;
        while(end > 0) {
            int tmp = this.elem[0];
            this.elem[0] =this.elem[end];
            this.elem[end] = tmp;
            adjustDown(0,end);
            end--;
        }
    }

你可能感兴趣的:(Java,API,Java,数据结构,数据结构,java,算法)