1. 内容大纲
本章节代码:https://github.com/wicksonZhang/data-structure/tree/main/12-BinaryHeap
2. 基础知识
2.1. 二叉堆解决了什么问题
二叉堆解决的主要问题是在一个动态集合中找到最大或最小元素,并且支持快速的插入和删除操作。
- 找到最大或最小元素的时间复杂度:O(1)
- 删除和插入的时间复杂度:O(logn)
2.2. 二叉堆是什么
二叉堆(Binary Heap):二叉堆是一种特殊的二叉树数据结构,二叉堆的核心概念如下:
- 堆(heap):任意节点 i 的值总是 大于等于(>=) 或者 小于等于(<=) 子节点 的值。
- 最大堆(Max Heap): 对于任意节点 i 的值如果 大于等于(>=) 子节点 的值。
- 最小堆(Min Heap):对于任意节点 i 的值如果 小于等于(<=)子节点 的值。
2.3. 二叉堆的性质
二叉堆的本质上是一颗完全二叉树,所以二叉堆的索引 i 具备以下性质:
- 注意,以下的 i 代表的是索引,n 代表的是元素数量。
- 如果 i = 0,它是根节点。
如果 i > 0,它的父节点的索引为 floor((i - 1) / 2)。
- 例如,节点 43、38 的父节点都是 68。 floor((3 - 1) / 2) = 1
如果 2i + 1 <= n - 1,它的左子节点的索引为 2i + 1。
- 例如,节点 68 的左子节点 43。2 * 1 + 1 = 3
如果 2i + 1 > n - 1,它无左子节点。
- 例如,节点 47 无左子节点。2 * 5 + 1 > 10 -1
如果 2i + 2 <= n - 1,它的右子节点的索引为 2i + 2。
- 例如,节点 68 的右子节点就是 38。2 * 1 + 2 = 4
如果 2i + 2 > n - 1,它无右子节点。
- 例如,节点47。2 * 5 + 2 > 10 - 1
2.4. 二叉堆优缺点
优点
- 高效的插入和删除操作: 二叉堆对于插入和删除操作的时间复杂度都是 O(log n),其中 n 是堆中元素的数量。
- 高效的查找最大值或最小值:如果是最大堆或者是最小堆,根节点就是最大值或者最小值,时间复杂度为O(1)。
缺点
- 不支持动态大小:因为二叉堆一般底层可以采用数组实现,所以是不具备动态扩容的。
3. 接口设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
public interface Heap<E> {
int size();
boolean isEmpty();
void clear();
void add(E e);
E get();
E remove();
E replace(E e);
}
|
4. 代码实现
4.1. 初始化元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
@SuppressWarnings("unchecked") public class BinaryHeap<E> implements Heap<E> {
private E[] elements;
private int size;
private static final int DEFAULT_CAPACITY = 1 << 4;
private Comparator<E> comparator;
public BinaryHeap() { this(null); }
public BinaryHeap(Comparator<E> comparator) { this.comparator = comparator; this.elements = (E[]) new Object[DEFAULT_CAPACITY]; }
private int compare(E element1, E element2) { if (comparator != null) { return comparator.compare(element1, element2); } return ((Comparable<E>) element1).compareTo(element2); }
}
|
4.2. 获取堆顶元素
1 2 3 4 5 6 7 8 9 10 11
| @Override public E get() { emptyCheck(); return elements[0]; }
private void emptyCheck() { if (size == 0) { throw new IndexOutOfBoundsException("Heap is empty"); } }
|
4.3. 添加元素
实现思路
1 2 3
| 1. Step-1: 我们需要将元素添加在数组的最后一个元素。 2. Step-2: 我们通过判断添加的元素是否比父节点大,如果比父节点大就交换位置。 3. Step-3: 一致持续这个操作,最后如果没有父级节点就推出循环。
|
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
@Override public void add(E element) { elementCheck(element); ensureCapacity(size + 1); elements[size++] = element; siftUp(size - 1); }
private void ensureCapacity(int capacity) { if (capacity - elements.length > 0) { int newCapacity = capacity + (capacity >> 1); E[] newElement = (E[]) new Object[newCapacity]; for (int i = 0; i < size; i++) { newElement[i] = elements[i]; } elements = newElement; } }
private void siftUp(int index) { E element = elements[index]; while (index > 0) { int parentIndex = index - 1 >> 2; E parentElement = elements[parentIndex]; if (compare(parentElement, element) >= 0) { break; } elements[index] = parentElement; index = parentIndex; } elements[index] = element; }
private void elementCheck(E e) { if (e == null) { throw new NullPointerException("Element is not null"); } }
|
4.4. 删除
实现思路
- 这个不能使用常规思维进行解决,如果直接删除堆顶元素。那么所有的元素需要向前移动,时间复杂度又变为了 O(n) 。
1 2 3 4
| 1. Step-1: 将堆顶元素与数组最后一个元素进行交换位置,然后将最后一个元素删除。 2. Step-2: 再将新的堆顶元素与子节点进行比较。 3. Step-3: 如果比子节点小,则将最大子节点的元素进行交换。 4. Step-4: 如果比子节点大,或者没有子节点,则退出循环。
|
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
|
@Override public E remove() { emptyCheck(); int lastIndex = --size; E root = elements[0]; elements[0] = elements[lastIndex]; elements[lastIndex] = null;
siftDown(0); return root; }
private void siftDown(int index) { E element = elements[index]; int half = size >> 1; while (index < half) {
int childIndex = (index << 1) + 1; E child = elements[childIndex];
int rightIndex = childIndex + 1;
if (rightIndex < size && compare(elements[rightIndex], child) > 0) { child = elements[childIndex = rightIndex]; }
if (compare(element, child) >= 0) break;
elements[index] = child; index = childIndex; } elements[index] = element; }
|
4.5. 删除堆顶元素同时插入一个新的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Override public E replace(E element) { elementCheck(element); E root = null; if (size == 0) { elements[0] = element; size++; } else { root = elements[0]; elements[0] = element; siftDown(0); } return root; }
|
5. 参考博文