欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

数据结构-----优先级队列(堆)

程序员文章站 2022-06-05 09:14:54
...

一、PriorityQueue 的使用

  • 概念
    队列是一种先进先出(FIFO)的数据结构,但有时候,数据可能带有优先级,出队列时,可能需要优先级高的元素先出队列,在这种情况下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
  1. 常用接口介绍
    1、JDK:
    提供了两种类型的优先级队列:①PriorityQueue是线程不安全的,②PriorityBlockingQueue是线程安全的。

2、PriorityQueue使用前,应先导入包

import java.util.PriorityQueue;

【注意】

  • 插入的元素不能为null,且元素之间必须要能够进行比较;
  • 插入、删除------在插入和删除元素期间优先级队列中的元素会自定进行调整(不论怎么调整,0号位置的元素始终是最小的);插入和删除的时间复杂度: O(logN)。
  • 底层结构—堆(暂时不用管什么是堆) 也可以通过某种方式使首元素始终最大

3、PriorityQueue包含的接口:

  • 构造方式:

  • PriorityQueue()空的优先级队列:默认的容量为11 ;

PriorityQueue q1 = new PriorityQueue<>();

  • PriorityQueue(capacity):指定初始化容量大小 (capacity不能小于1,如果小于1会抛异常 )

PriorityQueue p2 = new PriorityQueue<>(20);

  • PriorityQueue(集合对象):用一个集合中的元素来构造优先级队列
ArrayList<Integer> list = new ArrayList<>();
     list.add(4);
     list.add(3);
     list.add(2);
     list.add(1);
  //用ArrayList对象来构造一个优先级队列的对象
 //q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
  • 常用接口:
  • offer(x) :插入元素---->O(logN)
  • poll():删除元素
  • size():获取有效元素个数
  • isEmpty(): 检测是否为空
  • clear():将优先级队列中的元素清空
 public static void TestPriorityQueue() {
        int[] arr = {4, 1, 9, 2, 8, 0, 7, 3, 6, 5};
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
        PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
        for (int e : arr) {
            q.offer(e);
        }
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
        // 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
        q.poll();
        q.poll();
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
        q.offer(0);
        System.out.println(q.peek()); // 获取优先级最高的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
        q.clear();
        if (q.isEmpty()) {
            System.out.println("优先级队列已经为空!!!");
        } else {
            System.out.println("优先级队列不为空");
        }
    }

二、堆的概念及实现

  • 概念
    有一个集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
    数据结构-----优先级队列(堆)
    如图所示为小堆,根节点的值小于左右子树的值,反之则为大堆。

  • 存储方式
    堆是一棵完全二叉树,因此可以按照层序的规则采用顺序的方式存储。对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。

对于具有n个结点的完全二叉树,按照从上到下、从左到右的顺序对节点从0开始编号,假设i为节点在数组中的下标,则有:

  • 若i>0,双亲序号: (i-1)/2; i=0, i为根节点编号,无双亲节点.
  • 2i+1<n, 左孩子序号: 2i+1,否则无左孩子
  • 2i+2<n, 右孩子序号: 2i+2,否则无右孩子

堆的创建

  • 向下调整
    如图, 对于集合{ 27,15,19,18,28,34,65,49,25,37 },根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
    数据结构-----优先级队列(堆)
    过程(以小堆为例):
    1、parent为本次调整节点的下标,调整以parent为根的二叉树;
    (调整之前,要保证parent的左右子堆已经满足小堆的性质)

2、检测parent是否满足小堆性质:将parent与其孩子的比较

  • 满足:以parent为根节点的二叉树已经是小堆
  • 不满足:说明parent比其孩子大,将parent与其较小的孩子进行交换,交换后,parent的向下移动,
    可能导致其子树不满足小堆性质,继续调整;
private  void shiftDown(int parent){
        //使用child标记parent较小的孩子
        //默认情况下,先标记左孩子(完全二叉树:可能有左孩子没有右孩子)
        int child = parent * 2 + 1;

        while (child<size){
            //找parent中较小的孩子
            if(res[child+1] < res[child]){//如果右孩子比左孩子小
                child+=1;
            }

            //比较parent与child的大小
            if(res[parent]>res[child]){
                swap(parent,child);

                //调整后,如果子树不满足小堆的性质,继续调整
                parent=child;
                child=parent*2+1;

            }else{
                return;//以parent为根的二叉树已经满足小堆性质
            }
        }
    }
        private void swap(int parent,int child){
           int temp = res[parent];
           res[parent]=res[child];
           res[child]=temp;
        }

  • 堆的创建
    对于根节点的左右子树不满足堆的特性,我们需要不断地进行调整:
public static void createHeap(int[] array) {
 //找到第一个非叶子节点(根) child=parent*2+1 ----- parent = (child-1)/2
        int lastLeaf= (size-1)-1>>1;
        //调整很多个根节点 从下往上找叶子节点
        for (int root = lastLeaf; root >=0 ; root--) {
            shiftDown(root);//每一个根节点都要向下调整
        }
}
  • 堆的插入
    堆的插入总共需要两个步骤:
  1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
  2. 将最后新插入的节点向上调整,直到满足堆的性质
 private boolean offer(int x){
            grow();//扩容

            //1、将元素尾插到数组中
            res[size++] = x;

            //2、检测新元素插入后是否破坏小堆的性质
            shiftUp(size-1);//向上调整
            return true;
        }
        
  private void shiftUp(int child) {
            int parent = (child - 1) >> 1;

            if (res[parent] > res[child]) {
                swap(parent, child);


                //调整后,可能导致不满足堆的性质 继续向上调整
                child = parent;
                parent = (child - 1) >> 1;
            } else {
                return;
            }
        }
        
  private void swap(int parent,int child){
           int temp = res[parent];
           res[parent]=res[child];
           res[child]=temp;
        }
   private void grow(){
          if(size>= res.length){
              int oldCapacity = res.length;
              int newCapacity = oldCapacity+ ((oldCapacity<64)?(oldCapacity + 2):(oldCapacity >> 1));
            }
        }
  • 堆的删除
    堆的删除一定删除的是堆顶元素:
  1. 将堆顶元素对堆中最后一个元素交换;
  2. 将堆中有效数据个数减少一个;
  3. 对堆顶元素进行向下调整;
private int poll(){
          int a = res[0];
          swap(0,size-1);//0号元素和新插入元素交换
          size--;

          shiftDown(0);//向下调整
          return a;
        }