有些情景不一定需要元素全部有序,而是需要每次处理键值最大的元素,如手机运行程序,程序有优先级,来电的优先级比游戏程序的优先级高。
这是就适合优先队列数据结构。优先队列的主要操作有 删除最大元素delMax()和插入元素insert()。高效地实现优先队列具有挑战性,我们可以基于二叉堆来实现优先队列。
通过插入一系列元素,然后一个个删除其中最小的元素合一实现排序算法,堆排序就是基于堆的优先队列的实现。
初级实现
数组/链表实现(无序)
用实现栈的方式实现,insert方法就像实现push方法一样将新加入的元素插入到栈顶。delMax的时候需要一段内循环,实现类似选择排序的循环找出最大的元素和栈顶元素交换,然后删除栈顶(pop)。如果用数组而不是链表来实现的话,还可以加入调整数组大小的机制来保证栈不会溢出。
insert复杂度O(1)
delMax复杂度O(N),需要循环栈内元素找到最大并交换位置删除。
数组/链表实现(有序)
和上边的方法类似,用栈实现,排序的过程放到insert()方法中,插入一个元素,像插入排序那样将新元素插入到合适的位置并将后边的元素依次后移,保证栈顶是最大元素,delMax方法就和pop一样了。
insert复杂度O(N)
delMax复杂度O(1)
二叉堆实现
二叉堆的定义
当一颗二叉树的每个节点都大于它的两个字节点时(每个节点都小于它的父节点),被称为堆有序。根节点是堆有序的二叉树的最大节点。
堆有序二叉树的表示如果用指针的话,每个节点需要三个指针,分别指向两个孩子和父节点,但是如果我们使用完全二叉树来表示对有序的话,就会很方便,只用数组而不用指针就可以表示。
二叉堆是一组能够用堆有序的完全二叉树排序的元素
我们不用位置0,从1开始储存元素,位置k的节点的父节点的位置为k/2向下取整,子节点的位置分别为2k,2k+1.
二叉堆的插入和删除最大元素时对数级别的。
堆有序化(上浮swim)
当某个节点的优先级下降了,我们需要由下至上的恢复对的顺序,如堆底加入了一个大节点,需要上浮。
即某节点比父节点大了,这时要和它的父节点交换位置,至此这个位置不一定比它现在的父节点小,所以要一直上溯到合适的位置。
1 | private void swim(int k) { //位置k需要上浮 |
堆有序化(下沉sink)
当某个节点的优先级上升了,我们需要由上至下的恢复对的顺序,如根加入了一个小节点,需要下沉。
即父节点比两个孩子或其中一个孩子小了,这时要和它的子节点中较大的一个节点交换位置,至此这个位置不一定比它现在的所有子节点大,所以要一直下沉到合适的位置。
1 | private void sink(int k){ |
插入元素
将新元素加入到数组末尾,上浮到合适位置
删除最大元素
从数组顶部删去最大元素,并将最后一个元素放到顶端,减小堆的大小,并让新的顶端元素下沉到合适位置。
Java PriorityQueue源码
PriorityQueue继承了AbstractQueue类,而AbstractQueue类是实现了Queue接口。
成员变量
1 | //默认大小 |
构造函数
1 | //创建一个优先队列对象,默认大小为11,队列中的元素按照自然顺序排序。 |
主要方法
在队列中加入元素
1 | //在队列中加入元素 |
扩容,原大小小于64时翻倍,大于64时1.5倍
1 | private void grow(int minCapacity) { |
上浮,使用默认比较器或使用指定比较器
1 | private void siftUp(int k, E x) { |
删除元素
1 | public E poll() { |
堆排序
堆排序分为两个步骤,首先是从最开始混乱的状态构建一个堆有序(非最终有序),构建了有序堆之后利用不断删除堆顶元素来进行排序,删掉的堆顶元素放入到数组中空出的位置。这是个原地排序。
找到N个元素中最大的M个元素
复杂度
排序算法实现:O(NlogN)
调用初级实现的优先队列:O(NM)
调用基于堆实现的优先队列:O(NlogM)