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

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堆的性质

(1)堆中某个节点的值总是不大于或不小于其父节点的值;
(2)堆总是一棵完全二叉树。
特别注意:若一个堆为大根堆,只表明堆的每个节点的值大于其左子节点和右子节点,至于左子节点和右子节点谁大谁小不确定。小根堆也是如此。举例如下:
在这里插入图片描述

2.3堆的存储方式

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

将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2;
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子;
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子。

2.4堆的创建

2.4.1堆的向下调整

对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?
整个创建过程如下图所示:
在这里插入图片描述
下面我们来思考一个问题,为什么要求从最后一棵子树开始,而不是从根节点开始向下调整呢?
在这里插入图片描述
接下来文字叙述以下以向下调整的方式创建堆的过程(以创建大根堆为例):

  1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:由于数组的种有效数据的个数是usedSize,因此堆的最后一个节点的下标是usedSize - 1,从而最先开始调整的子树的根节点下标是(usedSize - 1 - 1) / 2,parent如果有孩子一定先是有左孩子);
  2. 如果parent的左孩子存在,即:child < usdSize(其中usedSize是原始数组种有效数据的个数)。 进行以下操作,直到parent的左孩子不存在。
    ①.parent右孩子是否存在,若存在找到左右孩子中最大的孩子,用child进行标记,若右孩子不存在,直接用 child标记左孩子。此时child记录的是parent孩子种的最大值的下标。
    ②.将parent值与child值比较,如果:
    parent值大于的child值,调整结束。否则:交换parent与child的值,交换完成之后,可能导致子树不满足堆的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续步骤2。
    注意:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。

创建大根堆核心部分代码如下:

public class Heap {
    public int[] elem;
    public int usedSize;//记录数组中有效数据的个数
    public Heap(){
        this.elem = new int[10];
    }
    /**
     * 初始化elem数组
     * @param array
     */
    public void initHeap(int[] array){
        for(int i = 0;i < array.length;i++){
            elem[i] = array[i];
            usedSize++;
        }
    }
    /**
     * 创建大根堆
     */
    public void creatBigHeap(){
        for(int parent = (usedSize - 1 - 1)/2;parent >= 0;parent--){
            shiftDown(parent,usedSize);
        }
    }
    /**
     * 线下调整
     * @param parent
     * @param end
     */
    private void shiftDown(int parent,int end){
        int child = 2 * parent + 1;
        while(child < end){
            if(child + 1 < usedSize && elem[child] < elem[child + 1]){
                child++;
            }
            //到这里,child就是左右孩子最大值对应的下标
            if(elem[child] > elem[parent]){
                // 交换
                swap(child,parent);
                parent = child;
                child = 2 * parent + 1;
            }else{
                break;//左右孩子的最大值已经已经小于根节点的值,说明此时已经是大根堆了
            }
        }
    }
    /**
     * 交换变量的值
     * @param i
     * @param j
     */
    private void swap(int i,int j){
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }

}

测试代码如下:

public class Test {
    public static void main(String[] args) {
        int[] array = new int[]{ 27,15,19,18,28,34,65,49,25,37};
        Heap heap = new Heap();
        heap.initHeap(array);
        heap.creatBigHeap();
    }
}

运行结果如下:
在这里插入图片描述
对比我们下图的预期结果,说明代码没有问题。
在这里插入图片描述

2.4.2堆的向下调整的时间复杂度

最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(log2n)
在这里插入图片描述

2.4.3以向下调整建堆的时间复杂度

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

2.5堆的插入和删除

2.5.1堆的插入

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

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

如图所示是一个小根堆的插入过程:
在这里插入图片描述
代码实现(以小根堆为例):

	/**
     * 堆的插入
     * @param val
     */
    public void offer(int val){
        //1.判断数组是否满了,如果满了,需要扩容
        if(isFull() == true){
            elem = Arrays.copyOf(elem,2*elem.length);//2倍扩容
        }
        //2.插入元素
        elem[usedSize] = val;
        usedSize++;
        //3.向上调整
        shiftUp(usedSize-1);
    }

    /**
     * 向上调整
     * @param child
     */
    public void shiftUp(int child){
        int parent = (child-1)/2;
        while(child > 0){
            if(elem[child] < elem[parent]){
                swap(parent,child);//交换下标parent和child处元素的值
                child = parent;
                parent = (child - 1) / 2;
            }else{
                break;
            }
        }
    }
    /**
     * 判断数组元素是否满
     * @return
     */
    public boolean isFull(){
        return usedSize == elem.length;
    }

由堆的插入我们可以得到启发,在建堆的过程中我们可以边插入元素边调整,即用元素插入法建堆(也叫向上调整法)

2.5.1.1元素插入法建堆的时间复杂度

在这里插入图片描述

2.5.2堆的删除

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

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

如图所示是一个大根堆的删除过程:
在这里插入图片描述
代码实现:

/**
     * 删除堆顶元素
     * @return
     */
    public int poll(){
        int tmp = elem[0];//标记堆顶元素
        swap(0,usedSize-1);//交换堆中首末元素
        usedSize--;//有效数据值-1
        shiftDown(0,usedSize);//堆顶元素向下调整
        return tmp;
    }

注意:上述代码执行完毕后,数据10仍在数组中,但是由于我们进行了usdSize-1的操作,我们认为堆中的数据实际上已经不包含10了,从而达到删除堆顶元素的目的。

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值