堆
认识了二叉树,也学会了一些遍历算法(DFS、BFS),可是二叉树可以干什么呢,那么其中一个实际应用他来了-堆(Heap).
堆的特点
- 堆是完全二叉树(逻辑上)。
- 堆的底层实现,其实是顺序表(元素顺序为二叉树的层序遍历,可以抽象看成一颗树)。
- 分为大顶堆/小顶堆两种。
- 大顶堆:树中的每一个元素都是≥他的孩子们的。
- 小顶堆:树中的每一个元素都是≤他的孩子们的。
应用: 由于在大/小顶堆的每一个节点都是≥/≤他们的孩子们的。因此,堆可以在一组元素中,快速的定位最值(堆顶元素),大顶堆定位最大值,小顶堆定位最小值。
构建一个堆
小顶堆-向下调整:
private static void shiftDownForSmall(long[] array, int size, int index) {
while (true) {
int leftIndex = 2 * index + 1;
if (leftIndex >= size) {
return;//要调整的位置已经是叶子了,直接返回
}
int rightIndex = 2 * index + 2;//int rightIndex = leftIndex + 1
int minIndex = leftIndex;
if (rightIndex < size && array[rightIndex] < array[leftIndex]) {
minIndex = rightIndex;
}
if (array[index] <= array[minIndex]) {
return;//要调整的位置,已经符合小顶堆的特点,不需要调整了
}
long temp = array[index];
array[index] = array[minIndex];
array[minIndex] = temp;
index = minIndex;
}
}
- 完全二叉树的性质:按照层序遍历的顺序,依次给节点标号为0,1,2……,如果双亲节点序号为
i
,那么左孩子序号为2*i+1
,右孩子序号为2*i+2
;如果孩子序号为i
(不区分左右孩子),双亲的序号就为(i-1)/2
(其实是进行了向下取整)。 - 思路:
- 首先是,按照要调整的位置的下标,计算出该节点的左孩子所在的下标,然后判断是否合法(越界),如果不合法,说明没有左孩子(左孩子都没有,右孩子一定也是没有的),那么,就说明要调整的位置为叶子节点,不需要调整,直接
return
就好。 - 如果左孩子存在,那么就在左孩子和右孩子中,挑选二者中最小的那个。①如果要调整的节点比两个孩子中最小的还小(或者等于),那么这个节点已经满足小顶堆的情况了,也是不需要调整的。②如果两个孩子中最小的那个比要调整的节点还要小,那么交换他俩,把要调整的位置,更改为刚刚最小的下标(因为,这个位置的节点更改过了,所以也要进行向下调整的判断,继续while循环),直到全部调整结束。
大顶堆-向下调整:
private static void shiftDownForBig(long[] array, int size, int index) {
while (true) {
int leftIndex = 2 * index + 1;
if (leftIndex >= size) {
return;
}
int rightIndex = leftIndex + 1;
int maxIndex = leftIndex;
if (rightIndex < size && array[rightIndex] > array[leftIndex]) {
maxIndex = rightIndex;
}
if (array[index] >= array[maxIndex]) {
return;
}
long temp = array[index];
array[index] = array[maxIndex];
array[maxIndex] = temp;
index = maxIndex;
}
}
- 思路和小顶堆的完全一致,只是在比较的时候是挑出一个最大的值,然后进行比较。
现在我们可以调整一个节点了,那么我们如果从完全二叉树的底部开始,一路往上,不断进行向下调整,最终,就会构建出一个堆了。
public static void creatHeap(long[] array, int size) {
for (int i = (size - 2) / 2; i >= 0; i--) {//从底向上不断调整
//shiftDownForSmall(array, size, i);
shiftDownForBig(array,size,i);
}
}
- 我们其实只需要找到,整棵树的最后一个叶子的双亲节点就好了,从这个双亲节点开始,一路向上,不断向下调整即可。
- 因为,我们所使用的是层序遍历,所以,恰好是调整的范围是
[最后一个有孩子的双亲节点,0]
- 最后一个有孩子的双亲节点的下标就是
((size-1)-1)/2
,(size-1)
是最后一个节点的下标。
PriorityQueue
PriorityQueue
是Java中的一个使用到堆(小顶堆)的一个类,名为优先级队列。
- 接下来简单实现一个
MyPriorityQueue
: - 自己实现的这个简单的优先级队列,没有使用泛型,而是仅仅使用了
String
,也没有使用扩容函数,采用了默认20个元素,传入优先级队列的元素必须具备比较能力,由于String
已经具备比较能了,如果本身的比较不适用于你,那么你可在new对象
的时候传入构造方法一个Comparator
。 - Java官方文档
import java.util.*;
/**
* 优先级队列为小顶堆
*/
public class MyPriorityQueue implements Iterable<String> {
private String[] heap = new String[20];
private int size;
private Comparator<String> comparator;
public MyPriorityQueue() {
//无参构造
}
public MyPriorityQueue(Comparator<String> comparator) {
this.comparator = comparator;//传入comparator比较器
}
/**
* 插入函数
*
* @param e 要插入的元素
* @return 如果插入,则返回true,否则返回false
*/
public boolean add(String e) {
heap[size++] = e;
shiftUp(size - 1);//调整刚刚插入的那个元素
return true;
}
/**
* 删除队列的头,即堆顶元素
*
* @return 如果存在则返回删除的元素,否则抛出异常
*/
public String remove() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return poll();
}
/**
* 删除队列的头,即堆顶元素
*
* @return 如果存在则返回删除的元素,否则返回null
*/
public String poll() {
if (isEmpty()) {
return null;
}
String remove = heap[0];
heap[0] = heap[size - 1];
heap[size - 1] = null;
size--;
shiftDown(0);
return remove;
}
/**
* 返回堆顶元素,但是不删除
* @return 如果存在,则返回堆顶元素,否则返回null
*/
public String peek() {
if (isEmpty()) {
return null;
}
return heap[0];
}
/**
* 返回堆顶元素,但是不删除
* @return 如果存在,则返回堆顶元素,否则抛出异常
*/
public String element() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return heap[0];
}
/**
* 最简单的小顶堆向下调整
*
* @param index 要调整的下标
*/
private void shiftDown(int index) {
while (true) {
int leftIndex = 2 * index + 1;
if (leftIndex >= size) {
return;
}
int rightIndex = leftIndex + 1;
int minIndex = leftIndex;
if (rightIndex < size && compare(heap[rightIndex], heap[leftIndex]) < 0) {
minIndex = rightIndex;
}
if (compare(heap[index], heap[minIndex]) <= 0) {
return;
}
String temp = heap[index];
heap[index] = heap[minIndex];
heap[minIndex] = temp;
index = minIndex;
}
}
/**
* 向上调整函数
*
* @param index 从这个下标开始向上调整
*/
private void shiftUp(int index) {
while (index > 0) {
int parentIndex = (index - 1) / 2;//向下取整
int compareResult = compare(heap[index], heap[parentIndex]);
if (compareResult >= 0) {
return;
}
String temp = heap[index];
heap[index] = heap[parentIndex];
heap[parentIndex] = temp;
index = parentIndex;
}
}
/**
* 为了方便比较,单独拉出一个函数
*
* @param insert 刚刚插入的节点的值
* @param parent 该节点的双亲节点的值
* @return 比较结果,如果前者小,返回负数,如果两者相等,返回0,如果前者大,返回正数
*/
private int compare(String insert, String parent) {
if (comparator == null) {
return insert.compareTo(parent);
} else {
return comparator.compare(insert, parent);
}
}
/**
* 返回优先级队列的元素个数
*
* @return int元素个数
*/
public int size() {
return size;
}
/**
* 优先级队列是否为空
*
* @return 如果为空,返回true,否则返回false
*/
public boolean isEmpty() {
return size == 0;
}
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private int nowIndex = 0;
@Override
public boolean hasNext() {
return nowIndex < size;
}
@Override
public String next() {
return heap[nowIndex++];
}
};
}
@Override
public String toString() {
return "MyPriorityQueue{" +
"heap=" + Arrays.toString(heap) + '}';
}
}
得益于堆优秀的插入性能(O(log(n))),堆的使用很是广泛,优先级队列的只是其中的一个。还有堆排序等等……
文章中如有任何错误,请提出来,谢谢了😜。