优先级队列--堆
优先级队列(堆)
1. 二叉树的顺序存储
1.1 存储方式
使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中。
一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。
这种方式的主要用法就是堆的表示
1.2 下标关系
已知双亲(parent)的下标,则:
- 左孩子(left)下标 = 2 * parent + 1;
- 右孩子(right)下标 = 2 * parent + 2;
已知孩子(不区分左右)(child)下标,则:
- 双亲(parent)下标 = (child - 1) / 2;
2. 堆(heap)
2.1. 概念
- 堆逻辑上是一棵完全二叉树
- 堆物理上是保存在数组中
- 满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆
- 反之,则是小堆,或者小根堆,或者最小堆
- 堆的基本作用是,快速找集合中的最值
2.2. 将完全二叉树转换为大根堆
我们用到一种思路,叫作向下调整
- 找到下标最大的父亲节点parent,并将parent向前遍历
- 每次遍历都需要进行向下调整
向下调整过程:
- 声明孩子节点为2*parent+1
- 循环条件,孩子节点下标不能大于堆的长度。
- 找出左右孩子节点的最大值,和父亲节点的值进行比较
o:如果比父亲节点的值大,则交换最大孩子节点值和父亲节点值,并将父亲节点往下走到孩子节点处,孩子节点为当前父亲节点的孩子。
o:如果比父亲节点的值下,则退出循坏
具体实现代码
public class HeapDemo {
public int[] elem;
public int usedSize;
public HeapDemo() {
this.elem = new int[10];
}
/**
* 在这里为什么可以传len
* 因为每棵树的结束位置实际上都是一样的
* @param parent
* @param len
*/
public void adjustDown(int parent,int len) {
while(parent*2+1 < len) {
int left = parent*2+1;
int right;
if(left + 1 < len) {
right = left+1;
}else {
right = left;
}
int max = elem[left] >= elem[right] ? left : right;
if(elem[max] > elem[parent]) {
int tmp = elem[parent];
elem[parent] = elem[max];
elem[max] = tmp;
parent = max;
}else {
break;
}
}
}
public void adjustDown2(int parent,int len) {
int child = 2*parent+1;
//child < len 说明没有左孩子
while (child < len) {
//child+1 < len 判断是否有有孩子
if(child+1 < len && this.elem[child] < this.elem[child+1]) {
child++;
}
//child 下标一定是左右孩子的最大值下标
if(this.elem[child] > this.elem[parent]) {
int tmp = elem[parent];
elem[parent] = elem[child];
elem[child] = tmp;
parent = child;
child = parent*2+1;
}else {
break;
}
}
}
public void createBigHeap(int[] array) {
for(int i = 0; i < array.length; i++ ){
this.elem[i] = array[i];
this.usedSize++;
}
//elem已经存放了元素
//开始每个父亲节点向下调整
//已知孩子节点n,父亲节点为(n-1)/2
for(int i = (this.usedSize-2)/2; i >= 0 ; i--) {
adjustDown(i,this.usedSize);
}
}
public void show() {
for(int i = 0; i < this.usedSize; i++) {
System.out.print(this.elem[i] + " ");
}
System.out.println();
}
}
2.3. 优先级队列–新增元素
- 判断堆是否为满,若满则二倍扩容
- 新增元素,并进行向上调整
循环条件,child > 0
和父亲节点比较大小
若比他大,则交换,并继续向上
若比他笑,则跳出循环
/**
* 逻辑:放到数组的最后一个位置
* 然后向上调整
* @param child
*/
public void adjustUp(int child) {
int parent = (child-1)/2;
while(child > 0) {
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
child = parent;
parent = (child-1)/2;
}else {
break;
}
}
}
public void push(int val) {
//若堆为满,则进行二倍扩容
if(isFull()) {
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[usedSize] = val;
usedSize++;
//向上调整
adjustUp(usedSize-1);
}
public boolean isFull() {
return this.usedSize == this.elem.length;
}
2.4. 优先级队列–弹出元素
逻辑:
- 交换首尾元素
- 进行向下调整
public void adjustDown2(int parent,int len) {
int child = 2*parent+1;
//child < len 说明没有左孩子
while (child < len) {
//child+1 < len 判断是否有有孩子
if(child+1 < len && this.elem[child] < this.elem[child+1]) {
child++;
}
//child 下标一定是左右孩子的最大值下标
if(this.elem[child] > this.elem[parent]) {
int tmp = elem[parent];
elem[parent] = elem[child];
elem[child] = tmp;
parent = child;
child = parent*2+1;
}else {
break;
}
}
}
public int poll() {
if(isEmpty()) {
throw new RuntimeException("队列为空!");
}
//删除
//交换首尾位置元素
int ret = this.elem[0];
this.elem[0] = this.elem[usedSize-1];
this.elem[usedSize-1] = ret;
this.usedSize--;
adjustDown2(0,usedSize);
return ret;
}
public int peek() {
if(isEmpty()) {
throw new RuntimeException("队列为空!");
}
return this.elem[0];
}
public boolean isEmpty() {
return this.usedSize == 0;
}
2.5. 完整手撕优先级队列
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:优先级队列---堆
* User: starry
* Date: 2021 -04 -20
* Time: 15:22
*/
public class HeapDemo {
public int[] elem;
public int usedSize;
public HeapDemo() {
this.elem = new int[10];
}
/**
* 在这里为什么可以传len
* 因为每棵树的结束位置实际上都是一样的
* @param parent
* @param len
*/
public void adjustDown(int parent,int len) {
while(parent*2+1 < len) {
int left = parent*2+1;
int right;
if(left + 1 < len) {
right = left+1;
}else {
right = left;
}
int max = elem[left] >= elem[right] ? left : right;
if(elem[max] > elem[parent]) {
int tmp = elem[parent];
elem[parent] = elem[max];
elem[max] = tmp;
parent = max;
}else {
break;
}
}
}
public void adjustDown2(int parent,int len) {
int child = 2*parent+1;
//child < len 说明没有左孩子
while (child < len) {
//child+1 < len 判断是否有有孩子
if(child+1 < len && this.elem[child] < this.elem[child+1]) {
child++;
}
//child 下标一定是左右孩子的最大值下标
if(this.elem[child] > this.elem[parent]) {
int tmp = elem[parent];
elem[parent] = elem[child];
elem[child] = tmp;
parent = child;
child = parent*2+1;
}else {
break;
}
}
}
public void createBigHeap(int[] array) {
for(int i = 0; i < array.length; i++ ){
this.elem[i] = array[i];
this.usedSize++;
}
//elem已经存放了元素
//开始每个父亲节点向下调整
//已知孩子节点n,父亲节点为(n-1)/2
for(int i = (this.usedSize-2)/2; i >= 0 ; i--) {
adjustDown(i,this.usedSize);
}
}
/**
* 逻辑:放到数组的最后一个位置
* 然后向上调整
* @param child
*/
public void adjustUp(int child) {
int parent = (child-1)/2;
while(child > 0) {
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
child = parent;
parent = (child-1)/2;
}else {
break;
}
}
}
public void push(int val) {
//若堆为满,则进行二倍扩容
if(isFull()) {
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[usedSize] = val;
usedSize++;
//向上调整
adjustUp(usedSize-1);
}
public int poll() {
if(isEmpty()) {
throw new RuntimeException("队列为空!");
}
//删除
//交换首尾位置元素
int ret = this.elem[0];
this.elem[0] = this.elem[usedSize-1];
this.elem[usedSize-1] = ret;
this.usedSize--;
adjustDown2(0,usedSize);
return ret;
}
public int peek() {
if(isEmpty()) {
throw new RuntimeException("队列为空!");
}
return this.elem[0];
}
public boolean isEmpty() {
return this.usedSize == 0;
}
public boolean isFull() {
return this.usedSize == this.elem.length;
}
public void show() {
for(int i = 0; i < this.usedSize; i++) {
System.out.print(this.elem[i] + " ");
}
System.out.println();
}
}
3. 优先级队列
PriorityQueue 实现 Queue 接口
/**
* PriorityQueue 堆 优先级队列
* PriorityQueue 底层 默认是一个小根堆
* 每次存元素的时候,一定要保证数据进去堆后,依然可以维持为一个小堆/大堆
* 每次取出一个元素的时候,一定要保证剩下的元素,也要调整为一个小堆/大堆
*/
PriorityQueue<Integer> qu = new PriorityQueue<>();
qu.offer(21);
qu.offer(23);
qu.offer(2);
qu.offer(43);
qu.offer(5);
qu.offer(8);
System.out.println(qu.peek()); //2
qu.poll();
System.out.println(qu.peek()); //5
3.1 比较器构造方法
之前我们有说过PriorityQueue 默认是一个小根堆,那么我们想要一个大根堆,需要怎么办呢?
在底层,PriorityQueue 不仅有普通的构造方法,还有参数为比较器的构造方法,我们采取匿名内部类的方式来传入比较器的参数构成函数式接口,如下:
PriorityQueue<Integer> qu = new PriorityQueue<>(
new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
返回o1-o2时是小根堆
返回o2-o1时是大根堆
3.2 底层扩容方式
可以看到他是在
- 小于64个元素前每次变为原来长度的两倍+2
- 大于64个元素后每次右移一位,相当于1.5倍扩容
4. TopK问题
- 求前k个最小的元素------建大堆
- 求前k个最大的元素------建小堆
那么建多大的堆呢?
建一个大小为k的堆,这样只用维护大小为k的堆,调整的时间复杂度也会很低,达到Olog2的k
思路流程:
以求前k个最大元素举例
- 先把数组前三个放入小根堆中
- 然后一直循环遍历数组
- 如果遇到比堆顶元素大的元素,出队(向下调整)
- 再将该元素入队(向上调整)
- 遍历结束后,堆中就是前k个最大的元素了
核心:小堆堆顶元素必然是前k大元素中最小的那个,如果遇到比堆顶元素还大的元素,肯定要放入舍弃最小的,放入比最小还大的元素。
代码实现
public static void topK(int[] arr,int k) {
PriorityQueue<Integer> queue = new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
for(int i = 0; i < arr.length; i++) {
if(i < k) {
queue.offer(arr[i]);
}else {
int top = queue.peek();
if(arr[i] > top) {
queue.poll();
queue.offer(arr[i]);
}
}
}
for(int i = 0; i < k; i++) {
System.out.println(queue.poll());
}
}
如果要实现前k个最小的元素
- 把修改为大根堆:
return o2-o1;
- 修改比较条件:
arr[i] < top
ok,现在就变成求前k个最小的元素了!
如果求第k小的元素,思路还是一样的,最后堆顶元素就是第k小的元素
4.1. TopK问题应用练习
查找和最小的K对数字
给定两个以升序排列的整形数组 nums1 和 nums2, 以及一个整数 k。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2。
找到和最小的 k 对数字 (u1,v1), (u2,v2) … (uk,vk)。
示例 1:
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
示例 2:
输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:
[1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]
示例 3:
输入: nums1 = [1,2], nums2 = [3], k = 3
输出: [1,3],[2,3]
解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-k-pairs-with-smallest-sums
思路:
和topK问题类似
我们之前比较的是数值大小,现在变成每次比较的是list中前两个和的大小
思路还是一样,需要灵活变通以下
代码如下:
class Solution {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
List<List<Integer>> out = new ArrayList<>();
for(int i = 0; i < nums1.length; i++) {
for (int j = 0; j < nums2.length; j++) {
List<Integer> in = new ArrayList<>();
in.add(nums1[i]);
in.add(nums2[j]);
out.add(in);
}
}
PriorityQueue<List<Integer>> queue = new PriorityQueue<>(k, new Comparator<List<Integer>>() {
@Override
public int compare(List<Integer> o1, List<Integer> o2) {
return (o2.get(0)+o2.get(1)) - (o1.get(0)+o1.get(1));
}
});
for(int i = 0; i < out.size(); i++) {
if(i < k) {
queue.offer(out.get(i));
}else {
List<Integer> top = queue.peek();
if(top.get(0)+top.get(1) > out.get(i).get(0)+out.get(i).get(1)) {
queue.poll();
queue.offer(out.get(i));
}
}
}
List<List<Integer>> res = new ArrayList<>();
for(int i = 0; i < k; i++) {
if(queue.peek() != null) {
res.add(queue.poll());
}
}
return res;
}
}
我这个代码还可以优化,比如插入的时候就可以比较了,没必要再循环一遍和创建一个结果数组
5. 堆排序
思路:
- 我们如果想要从小往大排序,则建立大根堆
- 我们如果想要从大往小排序,则建立小根堆
逻辑:
- 我们交换首位的位置
- 然后向下调整尾位置以前的堆
- 循环到尾指针 == 0 时,排序结束
这样我们就保证了每次最大的元素放到了最后的位置,全部循环完毕后,则时从小到大排序的
public void heapSort() {
int end = usedSize-1;
while (end > 0) {
int top = elem[0];
elem[0] = elem[end];
elem[end] = top;
adjustDown2(0,end);
end--;
}
}
把它写成函数的完整形式为
public class SmallToBig{
public void adjustDown(int[] array,int parent, int len) {
int child = parent*2+1;
while (child < len) {
if(child+1 < len && array[child] < array[child+1]) {
child = child+1;
}
if(array[parent] < array[child]) {
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
parent = child;
child = 2*child+1;
}else {
break;
}
}
}
public void createBigHeap(int[] array) {
int len = array.length;
int parent = (len-2)/2;
for (int i = parent; i >= 0; i--) {
adjustDown(array,i,len);
}
}
public void heapSort(int[] array) {
int end = array.length-1;
while (end > 0) {
int top = array[0];
array[0] = array[end];
array[end] = top;
adjustDown(array,0,end);
end--;
}
}
public static void main(String[] args) {
int[] array = {27,15,19,18,28,34,65,49,25,37};
Work3 a = new Work3();
a.createBigHeap(array);
a.heapSort(array);
System.out.println(Arrays.toString(array));
}
}
时间复杂度:O (n*log2n)
额外空间复杂度:O (1)