本文主要是对堆的一个简单介绍,如果你是刚学数据结构的话,十分推荐看这篇文章,通过本文你将对堆这个数据结构有一个大致的了解,同时学习JDK自带的堆实现类
PriorityQueue
类,如何基于数组手写一个堆。
什么是堆?
堆(Heap)是一种常见的数据结构,堆可以基于数组实现,也可以基于链表实现。堆的定义如下:n个元素的序列 { k 1 , k 2 , k i , … , k n } {\{k1,k2,ki,…,kn\}} {k1,k2,ki,…,kn} 当且仅当满足下关系 ( k i < = k 2 i , k i < = k 2 i + 1 ) (ki <= k2i,ki <= k2i+1) (ki<=k2i,ki<=k2i+1)或者 ( k i > = k 2 i , k i > = k 2 i + 1 ) , ( i = 1 , 2 , 3 , 4... n / 2 ) (ki >= k2i,ki >= k2i+1), (i = 1,2,3,4...n/2) (ki>=k2i,ki>=k2i+1),(i=1,2,3,4...n/2)时,称之为堆。堆是具备完全二叉树的特点的,二叉堆就是一种特别的完全二叉树,多叉堆也具有完全二叉树的特点。
备注:完全二叉树的特点是,除了最后一层,其他层的叶子节点都是满的,也就是非最后一层节点的度要么是2要么是0,这个度是就是子节点的个数,堆也是具有这个特点的
以下是一些常见的堆:
最大堆:父节点永远是最大的,顶点为堆中最大元素
9
/ \
7 8
/ \ / \
6 5 2 3
/ \
1 4
最小堆:父节点永远是最小的,顶点为堆中最小元素
0
/ \
1 2
/ \ / \
6 5 7 3
/ \
9 8
/
4
多叉堆:
0
/ / \ \
/ / \ \
2 3 4 5
/ | \
6 7 8
所以也可以组合命名,比如:多叉最小堆、多叉最大堆、二叉最小堆、二叉最大堆,其中最为常见的就是二叉最大堆(一般直接简称最大堆)和二叉最小堆(一般直接简称最小堆),而本文中的堆实现就是主要讲解 最大堆 和 最小堆
堆的上浮和下浮操作
上浮(Heapify Up):也称为向上调整或上升,是指在插入元素到堆时将其移动到正确的位置以满足堆的性质。对于最大堆来说,上浮操作是将新插入的元素不断与其父节点进行比较和交换,直到满足堆的性质。具体步骤如下:
上浮操作保证了插入后的堆仍然保持堆的性质,即对于最大堆,父节点的值大于等于子节点的值。
下沉(Heapify Down):也称为向下调整或下降,是指在删除堆顶元素后,将最后一个元素移到堆顶并将其下沉到正确的位置以满足堆的性质。对于最大堆来说,下沉操作是将堆顶元素不断与其子节点进行比较和交换,直到满足堆的性质。具体步骤如下:
下沉操作保证了删除堆顶元素后的堆仍然保持堆的性质,即对于最大堆,父节点的值大于等于子节点的值。
基于堆的特点,堆的常见应用有:
在Java中,有一个名为PriorityQueue
的类实现了堆(Heap)的功能。PriorityQueue
是一个优先队列,基于堆的数据结构实现。
使用PriorityQueue
可以方便地操作堆的插入、删除和获取极值等操作。它根据元素的优先级进行排序,并保证每次操作都能够高效地获取最高优先级的元素。
以下是一些PriorityQueue
常用方法的示例:
add(E e)
或offer(E e)
:网堆中添加元素。将元素插入优先队列。remove()
或poll()
:删除堆的根节点。删除并返回队列中的最高优先级元素。peek()
:获取极值。返回队列中的最高优先级元素,但不删除。size()
:获取堆中元素的数量。返回队列中的元素个数。至于堆化和构建堆的操作 JDK 底层自动实现了,我们只管调用无需关心实现,这就是 Java 的好处吧
备注:PriorityQueue
默认是最小堆(小顶堆),即优先级较低的元素具有更高的优先级。如果需要使用最大堆(大顶堆),可以通过提供自定义的Comparator
来实现。
示例
示例一:最小堆(默认)
堆顶元素永远是最小的
import java.util.PriorityQueue;
public class HeapExample {
public static void main(String[] args) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.add(5);
minHeap.add(2);
minHeap.add(8);
System.out.println(minHeap.peek()); // 输出: 2
System.out.println(minHeap.poll()); // 输出: 2
System.out.println(minHeap.peek()); // 输出: 5
System.out.println(minHeap.size()); // 输出: 2
}
}
示例二:最大堆
堆顶元素永远是最大的
import java.util.Comparator;
import java.util.PriorityQueue;
public class MaxHeapExample {
public static void main(String[] args) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.add(5);
maxHeap.add(2);
maxHeap.add(8);
System.out.println(maxHeap.peek()); // 输出: 8
System.out.println(maxHeap.poll()); // 输出: 8
System.out.println(maxHeap.peek()); // 输出: 5
System.out.println(maxHeap.size()); // 输出: 2
}
}
注意:以下实现都是基于数组实现的,基于链表实现的我就没有写了,一般而言堆都是基于数组实现的,因为数组相对链表,内存是连续的,不需要存引用,相较而言,查询性能更好,内存占用也会相对少很多
手写堆提供的API
Heap()
:创建堆的无参构造方法,默认容量为10
Heap(int capacity)
:创建堆的有参构造方法,指定堆的初始容量
add(int val)
:往堆中添加一个元素,超过容量会自动进行扩容,扩容为原来容量的两倍
remove()
:移除并返回堆顶的元素(也就是极大值)
remove(int val)
:移除指定元素并返回,如果不存在抛异常
printHeap()
:打印堆中的元素,相当于是层序遍历二叉树
isEmpty()
:判断堆是否为空
/**
* @author ghp
* @title
* @description
*/
public class Heap {
/**
* 堆的默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 堆数组,用于存储堆中的元素
*/
private int[] heapArray;
/**
* 堆的容量
*/
private int capacity;
/**
* 堆中元素的数量
*/
private int size;
public Heap() {
this.capacity = DEFAULT_CAPACITY;
this.heapArray = new int[capacity];
this.size = 0;
}
public Heap(int capacity) {
this.capacity = capacity;
this.heapArray = new int[capacity];
this.size = 0;
}
/**
* 遍历打印堆中所有的元素
*/
public void printHeap() {
for (int i = 0; i < size; i++) {
System.out.print(heapArray[i] + " ");
}
System.out.println();
}
/**
* 判断堆是否为空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 往队中添加一个元素
*
* @param value
*/
public void add(int value) {
if (size == capacity) {
// 扩容为当前容量的两倍
resize(capacity * 2);
}
heapArray[size] = value;
heapifyUp(size++);
}
/**
* 移除堆顶元素
*
* @return
*/
public int remove() {
if (isEmpty()) {
throw new IllegalStateException("Heap is empty");
}
int max = heapArray[0];
heapArray[0] = heapArray[--size];
heapifyDown(0);
return max;
}
/**
* 移除堆中指定元素
*
* @param value
*/
public void remove(int value) {
int index = findIndex(value);
if (index == -1) {
throw new IllegalArgumentException("Element does not exist in the heap");
}
// 使用最后一个节点覆盖要删除的元素,这样才能确保节点都能进行一个更新
heapArray[index] = heapArray[--size];
heapifyDown(index);
}
/**
* 寻找到指定元素的索引
*
* @param value
* @return
*/
private int findIndex(int value) {
for (int i = 0; i < size; i++) {
if (heapArray[i] == value) {
return i;
}
}
return -1;
}
/**
* 上浮(新增操作时节点上移)
*
* @param index
*/
private void heapifyUp(int index) {
// 定位父节点
int parent = (index - 1) / 2;
if (parent >= 0 && heapArray[parent] < heapArray[index]) {
// 如果父节点比子节点小,则进行交换,然后递归上浮,确保父节点是永远大于子节点的
swap(parent, index);
heapifyUp(parent);
}
}
/**
* 下浮(删除操作时节点下移)
*
* @param index
*/
private void heapifyDown(int index) {
// 左节点的索引
int left = 2 * index + 1;
// 右节点的索引
int right = 2 * index + 2;
// 父节点索引
int parent = index;
// 判断左右节点是否大于父节点
if (left < size && heapArray[left] > heapArray[parent]) {
parent = left;
}
if (right < size && heapArray[right] > heapArray[parent]) {
parent = right;
}
if (parent != index) {
// 如果父节点发生了更新,则交换父节点和子节点的位置,同时递归下浮,确保父节点永远是最大的
swap(parent, index);
heapifyDown(parent);
}
}
/**
* 交换堆数组索引为 i 和 j 的两个元素
*
* @param i
* @param j
*/
private void swap(int i, int j) {
int temp = heapArray[i];
heapArray[i] = heapArray[j];
heapArray[j] = temp;
}
/**
* 扩容堆大小
*
* @param newCapacity
*/
private void resize(int newCapacity) {
if (newCapacity < DEFAULT_CAPACITY){
newCapacity = DEFAULT_CAPACITY;
}
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
}
heapArray = Arrays.copyOf(heapArray, newCapacity);
capacity = newCapacity;
}
}
备注:如果想要使用最小堆,则只需要修改上浮和下浮的if
判断条件即可
测试:
public class Test {
public static void main(String[] args) {
Heap heap = new Heap(0);
heap.add(1);
heap.add(2);
heap.add(3);
heap.add(4);
heap.add(5);
heap.printHeap();
heap.remove(1);
heap.printHeap();
}
}