堆(PriorityQueue)

1. 优先级队列

1.1 概念
队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。

2. 优先级队列的模拟实现

JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。

2.1 堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一
个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大
堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。

总的来说:
大根堆:根节点的值大于左右孩子节点的值
小根堆:根节点的值小于左右孩子节点的值
在这里插入图片描述

2.2 堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,
在这里插入图片描述
从图上我们可以看出一般的二叉树来建立堆可能会导致空间浪费,对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。

将元素存储到数组中后,可以根据二叉树的性质对树进行还原。假设i为节点在数组中的下标,则有:

如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

2.3堆的创建

2.3.1 堆向下调整

对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?
在这里插入图片描述
仔细观察上图后发现:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。

public int[] elem;
	//用来保存数组中元素的个数
    public int usedSize;
	//给数组默认初始值大小
    public static final int DEFAULT_SIZE=10;
    public Heap(){
        this.elem=new int[DEFAULT_SIZE];
    }
    //将array数组存储到elem数组中
    public void initElem(int[] array){
        for (int i = 0; i < array.length; i++) {
            elem[i]=array[i];
            usedSize++;
        }
    }

向下过程(大根堆为例)

 public void createHeap(){
 		//如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
 		//通过这个性质我们可以知道最后一棵子树的根节点为数组下标为4的节点
        for (int parent = (usedSize-1-1)/2; parent >0 ; parent--) {
        		//向下调整,将根节点和数组长度传入
                siftDown(parent,usedSize);
        }
    }
    public void siftDown(int parent,int len){
        // child先标记parent的左孩子,因为parent可能有左孩子左没有右孩子
        int child = 2*parent+1;
        
        while (child<len){
            //如果child越界,就会报错,所以加入限制条件防止报错(child+1<len)
           // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
            if (child+1<len&&elem[child]<elem[child+1]){
            	//就是将左右孩子的最大值的下标赋值给child
                child=child+1;
            }
            if(elem[child]>elem[parent]){
            	//将双亲与较小的孩子交换
                swap(child,parent);
                // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

在这里插入图片描述
大根堆为例:
在这里插入图片描述

2.3.3 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是
近似值,多几个节点不影响最终结果):
在这里插入图片描述
因此:建堆的时间复杂度为O(n).

2.4 堆的插入与删除

2.4.1 堆的插入
堆的插入总共需要两个步骤:

  1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
  2. 将最后新插入的节点向上调整,直到满足堆的性质

小根堆为例添加元素:
在这里插入图片描述

 public void push(int val){
        //首先我们要判断元素是否已经存储满了,如果满了我们还会需要扩容
        if(isFull()){
            elem=Arrays.copyOf(elem,elem.length*2);
        }
        siftUp(usedSize);

        usedSize++;
    }
public void siftUp(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;
            }
        }
    }

在这里插入图片描述
采用向上调整添加元素

2.4.2 堆的删除

注意:堆的删除一定删除的是堆顶元素。具体如下:

  1. 将堆顶元素对堆中最后一个元素交换
  2. 将堆中有效数据个数减少一个
  3. 对堆顶元素进行向下调整

在这里插入图片描述

public int pop(){
        //判断是不是空
        if(isEmpty()){
            throw new Nornull("为空不能删除");
        }
        int oldVal=elem[0];
        swap(0,usedSize);
        usedSize--;
        siftDown(0,usedSize);
        return oldVal;
    }
    public void siftDown(int parent,int len){
        //至少有左孩子
        int child = 2*parent+1;
        while (child<len){
            //如果child越界,就会报错,所以
            if (child+1<len&&elem[child]<elem[child+1]){
                child=child+1;
            }
            if(elem[child]>elem[parent]){
                swap(child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值