堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
这个就是非完全二叉树,不适合用堆
将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:
如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
但是在堆中又分为大根堆和小根堆两种:
大根堆
这个就是大根堆,对于任意节点 i,其值(通常称为键值或权重)大于或等于其所有子节点的值。换句话说,每个节点的值都至少大于其两个子节点的值。根节点(即树的顶端)具有整个堆中的最大值。
这个就是小根堆,在小根堆中,对于任意节点 i,其值(或称键值、权重)小于或等于其所有子节点的值。换言之,每个节点的值都不大于其两个子节点的值。堆的根节点(顶部节点)拥有整个堆中最小的值。
那么我们了解了什么是大根堆和小根堆,那么怎么用代码实现出来呢?下面我举例大根堆的创建,请看下面代码
大根堆的创建
public class TestHeap {
public int elem[];
public int usedSize;
public TestHeap(){
this.elem = new int[10];
}
public void init(int[] array){
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
public void creatHeap(){
for (int parent = (usedSize-1-1)/2; parent >=0 ; parent--) {
shifDown(parent,usedSize);
}
}
private void swap(int i,int j){
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
public void shifDown(int parent,int end){
int child = parent*2+1;
while(child < end){
if(child+1<end && elem[child] < elem[child+1]){
child++;
}
//child就是左右孩子的最大值
if(elem[child] > elem[parent]){
swap(child,parent);
parent = child;
child = parent*2+1;
} else {
break;
}
}
}
}
让我来解释一下这段代码:
首先creatHeap函数用来把当前最后一个元素的根节点和数组的长度传过去
swap函数用来交换两个元素的位置
ShifDown函数向下调整,可以根据传过来的根节点确定孩子结点,第一个if语句用来判断左右孩子哪个大,执行完这个if语句,child指向的就是大的那个结点,第二个if语句用来比较根节点和孩子结点哪个大,如果孩子结点比根结点大,那么利用swap函数进行交换,然后根结点的位置变成当前孩结点的位置,根据前面的公式可得,孩子结点的位置变成当前根节点的*2+1,如果走到最后孩子结点的下标比数组长度大,那么退出循环,进行下一次的creatHeap
堆的插入
堆的插入总共需要两个步骤:
1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
2. 将最后新插入的节点向上调整,直到满足堆的性质
//堆的插入
public void offer(int val){
if(isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
usedSize++;
shifUp(usedSize-1);
}
public void shifUp(int child){
int parent = (child-1)/2;
while(parent>=0){
if(elem[parent] < elem[child]){
swap(parent,child);
child = parent;
parent = (child-1)/2;
} else {
break;
}
}
}
public boolean isFull(){
return elem.length == usedSize;
}
private void swap(int i,int j){
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
堆的删除
注意:堆的删除一定删除的是堆顶元素。具体如下:
1. 将堆顶元素对堆中最后一个元素交换
2. 将堆中有效数据个数减少一个
3. 对堆顶元素进行向下调整
public int poll(){
if(isEmpty()){
return -1;
}
int old = elem[0];
swap(0,usedSize-1);
usedSize--;
shifDown(0,usedSize);
return old;
}
public boolean isEmpty(){
return usedSize == 0;
}
private void swap(int i,int j){
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
public void shifDown(int parent,int end){
int child = parent*2+1;
while(child < end){
if(child+1<end && elem[child] < elem[child+1]){
child++;
}
//child就是左右孩子的最大值
if(elem[child] > elem[parent]){
swap(child,parent);
parent = child;
child = parent*2+1;
} else {
break;
}
}
}
PriorityQueue:
关于PriorityQueue的使用要注意:
1. 使用时必须导入PriorityQueue所在的包,即:
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException异常
3. 不能插入null对象,否则会抛出NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5. PriorityQueue底层使用了堆数据结构
6. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
// 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
函数名 | 功能介绍 |
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
堆排序 以及 最小k个数链接
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
我们可以来一道例题来学习堆排序
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都
不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
我们已经了解了实现的大概原理,那我们来看一下代码是如何实现的吧!
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] tmp = new int[k];
if(k == 0){
return tmp;
}
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new IntCmp());
for (int i = 0; i < k; i++) {
maxHeap.offer(arr[i]);
}
for (int i = k; i < arr.length; i++) {
int val = maxHeap.peek();
if(val>arr[i]){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
tmp[i] = maxHeap.poll();
}
return tmp;
}
}
代码解释:
1.首先实现大根堆必须传入一个自定义的 Comparator,
该比较器定义了元素之间的比较逻辑,使得较大的元素被认为具有更高的优先级。
2.将k个元素放入队列中
3.将队列的顶元素分别与没进入队列的元素依次比较,向下调整
4.进行到这一步,说明队列中已经是最小的k个元素了,把他们依次放入tmp列表,最后返回就行了