堆
文章目录
- 堆
- 1. 什么是堆?
- 2. 堆的基本操作
- 2.1 建堆
- 建堆时间复杂度分析:O(n)
- 2.2 堆的插入--O(logn)
- 堆插入建堆
- 堆插入建堆时间复杂度:O(nlogn)
- 2.3 堆的删除--O(logn)
- 2.4 堆排序--O(nlogn)
- 3. 优先级队列PriorityQueue
- 3.1 PriorityQueue与建堆
- 3.2 调整PriorityQueue的比较规则
- 3.3 PriorityQueue的应用
1. 什么是堆?
- 堆是由树延续过来的结构,相较于树链式存储,堆是一种顺序存储。从树的角度来看,堆中的元素是以树的层次遍历规则将结点依次放到顺序表中,且这个树一定是完全二叉树,这样可以保证高效存储。
- 堆中的元素是要经过调整才能成为堆,根据调整规则,分为大根堆和小根堆。
- 以0下标开始的堆,任意结点的左孩子的下标为 2i+1,右孩子的下标为2i+2,父节点的下标为(i-1)/2。
2. 堆的基本操作
2.1 建堆
- 这里的建堆是指将一组已经在堆结构中的数据调整成堆!!!
- 建堆的核心是向下调整堆。调整规则是从最后一个父节点开始向下调整,然后退到根节点调整完成后结束。
public int[] elem;
public int usedSize;
public Heap() {
this.elem = new int[11];
this.usedSize = 0;
}
// (以大根堆为例)
public void createHeap(int[] array) {
for (int j : array) {
if (isFull()) {
elem = Arrays.copyOf(elem, elem.length + (elem.length >>> 1));
}
elem[usedSize++] = j;
}
// int root = (usedSize - 1 - 1) >>> 1;// err当只有一个元素的时候直接整型最大值!!!
int root = (usedSize - 1 - 1) >> 1;
while (root >= 0) {
shiftDown(root--, usedSize);
}
}
public boolean isFull() {
return usedSize == elem.length;
}
// (以大根堆为例)时间复杂度O(n)
private void shiftDown(int root, int len) {
int child = root * 2 + 1;
while (child < len) {
if (child + 1 < len && elem[child] < elem[child + 1]) {
child = child + 1;
}
if (elem[root] > elem[child]) {
break;
}
swap(root, child);
root = child;
child = root * 2 + 1;
}
}
public void swap(int x, int y) {
int temp = elem[x];
elem[x] = elem[y];
elem[y] = temp;
}
建堆时间复杂度分析:O(n)
2.2 堆的插入–O(logn)
- 堆的插入的核心操作是向上调整;
- 整体的步骤是先把插入元素直接放到顺序表有效容量下标位置(usedSize),接着从该位置向上调整即可。
public void push(int val) {
if (isFull()) {
elem = Arrays.copyOf(elem, elem.length + (elem.length >>> 1));
}
elem[usedSize++] = val;
shiftUp(usedSize - 1);
}
// (以大根堆为例)时间复杂度O(n)
private void shiftUp(int child) {
while (child > 0) {
int parent = (child - 1) >>> 1;
if (elem[parent] < elem[child]) {
swap(parent, child);
child = parent;
}else{
break;
}
}
}
public boolean isFull() {
return usedSize == elem.length;
}
堆插入建堆
- 堆插入建堆指将数据挨个插入堆中,插入操作会直接向上调整成堆。
堆插入建堆时间复杂度:O(nlogn)
2.3 堆的删除–O(logn)
- 堆的删除操作的核心是向下调整。
- 删除操作删除的是堆顶元素,具体操作是先将堆顶元素与最后一个元素交换,然后有效容量减一,再从堆顶开始向下调整。
public void pollHeap() {
if (isEmpty()) {
throw new RuntimeException("Heap is Empty!");
}
swap(0, usedSize - 1);
usedSize--;
shiftDown(0, usedSize);
}
public boolean isEmpty() {
return usedSize == 0;
}
2.4 堆排序–O(nlogn)
堆排序分为两步:
- 建堆,如果升序则建大根堆,反之相反。此时的堆也叫初始堆。
- 排序,利用堆删除思想,每次都要将堆顶元素换下去,所以升序要建大根堆。
// nlogn
public void heapSort(int[] arr){
// 建堆 O(n)
createHeap(arr);
// 排序 O(nlogn)
int end = usedSize -1;
while (end>0){
swap(0,end);
shiftDown(0,end);
end--;
}
}
3. 优先级队列PriorityQueue
- PriorityQueue是集合框架中的一个实现类,其底层数据结构就是堆,存储结构为动态数组。
- PriorityQueue中的元素必须能够比较,因为它相当于把空间给你划分好了,每次插入或者删除都会按照大根堆/小根堆的规则去调整。
- PriorityQueue默认是以小根堆的规则调整。
3.1 PriorityQueue与建堆
- 首先PriorityQueue的构造方法有很多。
- 一个是你直接创建好一个优先级队列,不管你用哪个构造,然后将带处理的数据挨个插入(push)建堆。
- 另一个是直接使用带集合参数的构造,直接将待处理数据直接通过构造方法建堆。
3.2 调整PriorityQueue的比较规则
默认是小根堆的调整规则,如果改变调整规则,优先级队列要求提供比较器(Comparator)。
// 以Integer为例,改造使用匿名内部类
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
3.3 PriorityQueue的应用
- 求最大或者最小的K个数
- 求第K大或者第K小的数
以最小K个数为例。
具体算法思路:需要大堆,建立容量为K的默认优先级队列,将前K个树进堆,然后从第K+1个数开始遍历,依次和堆顶元素比较,比堆顶元素小则出(poll)一个堆元素然后进(offer)该元素,在继续遍历,最后的堆中即为最小的K个数。可以将堆顶元素视为整个数据中最大的值,然后在剩余数据中比他小的。
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] ret = new int[k];
if(arr.length == 0||k == 0){
return ret;
}
// 建大堆
PriorityQueue<Integer> queue = new PriorityQueue<>(k,new Comparator<Integer>(){
@Override
public int compare(Integer o1,Integer o2){
return o2-o1;
}
});
int i = 0;
//O(klogk)
for(;i<k;i++){
queue.offer(arr[i]);
}
// O((n-k)logk) -- 维护的是已建好的堆,k肯能为n
for(;i<arr.length;i++){
if(queue.peek()>arr[i]){
queue.poll();
queue.offer(arr[i]);
}
}
for(i= 0;i<k;i++){
ret[i] = queue.poll();
}
return ret;
}
}