1. 内容大纲

本章节代码:https://github.com/wicksonZhang/data-structure/tree/main/12-BinaryHeap

二叉堆


2. 基础知识

2.1. 二叉堆解决了什么问题

二叉堆解决的主要问题是在一个动态集合中找到最大或最小元素,并且支持快速的插入和删除操作。

  • 找到最大或最小元素的时间复杂度:O(1)
  • 删除和插入的时间复杂度:O(logn)

2.2. 二叉堆是什么

二叉堆(Binary Heap):二叉堆是一种特殊的二叉树数据结构,二叉堆的核心概念如下:

  1. 堆(heap):任意节点 i 的值总是 大于等于(>=) 或者 小于等于(<=) 子节点 的值。
  2. 最大堆(Max Heap): 对于任意节点 i 的值如果 大于等于(>=) 子节点 的值。
  3. 最小堆(Min Heap):对于任意节点 i 的值如果 小于等于(<=)子节点 的值。

image-20240131214432923

2.3. 二叉堆的性质

image-20240131215306300

二叉堆的本质上是一颗完全二叉树,所以二叉堆的索引 i 具备以下性质:

  1. 注意,以下的 i 代表的是索引,n 代表的是元素数量
  2. 如果 i = 0,它是根节点。
    • 例如,节点 72.
  3. 如果 i > 0,它的父节点的索引为 floor((i - 1) / 2)

    • 例如,节点 43、38 的父节点都是 68。 floor((3 - 1) / 2) = 1
  4. 如果 2i + 1 <= n - 1,它的左子节点的索引为 2i + 1

    • 例如,节点 68 的左子节点 43。2 * 1 + 1 = 3
  5. 如果 2i + 1 > n - 1,它无左子节点。

    • 例如,节点 47 无左子节点。2 * 5 + 1 > 10 -1
  6. 如果 2i + 2 <= n - 1,它的右子节点的索引为 2i + 2

    • 例如,节点 68 的右子节点就是 38。2 * 1 + 2 = 4
  7. 如果 2i + 2 > n - 1,它无右子节点。

    • 例如,节点47。2 * 5 + 2 > 10 - 1

2.4. 二叉堆优缺点

优点

  1. 高效的插入和删除操作: 二叉堆对于插入和删除操作的时间复杂度都是 O(log n),其中 n 是堆中元素的数量。
  2. 高效的查找最大值或最小值:如果是最大堆或者是最小堆,根节点就是最大值或者最小值,时间复杂度为O(1)。

缺点

  1. 不支持动态大小:因为二叉堆一般底层可以采用数组实现,所以是不具备动态扩容的。

3. 接口设计

image-20240319203637199

  • 如下是堆 Heap 的接口设计
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
/**
* 堆
*
* @param <E>
*/
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
/**
* 二叉堆
*
* @param <E>
*/
@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];
}

/**
* 二叉堆一定是具有可比较性的
*
* @param element1 元素1
* @param element2 元素2
* @return int
*/
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: 一致持续这个操作,最后如果没有父级节点就推出循环。

image-20240201201527954

实现代码

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
/**
* 添加元素
*
* @param element 元素
*/
@Override
public void add(E element) {
elementCheck(element);
// 扩容
ensureCapacity(size + 1);
elements[size++] = element;
// 上滤
siftUp(size - 1);
}

/**
* 扩容
*
* @param capacity 容量
*/
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;
}
}

/**
* 上滤
*
* @param index 索引
*/
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: 如果比子节点大,或者没有子节点,则退出循环。

image-20240201211657259

实现代码

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
/**
* 删除元素
*
* @return E
*/
@Override
public E remove() {
emptyCheck();
int lastIndex = --size;
E root = elements[0];
elements[0] = elements[lastIndex];
elements[lastIndex] = null;

siftDown(0);
return root;
}

/**
* 下滤
*
* @param index
*/
private void siftDown(int index) {
E element = elements[index];
int half = size >> 1;
// 第一个叶子节点的索引 == 非叶子节点的数量
// index < 第一个叶子节点的索引
// 必须保证index位置是非叶子节点
while (index < half) {
// index的节点有2种情况
// 1.只有左子节点
// 2.同时有左右子节点

// 默认为左子节点跟它进行比较
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;

// 将子节点存放到index位置
elements[index] = child;
// 重新设置index
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
/**
* 删除堆顶元素同时插入一个新的元素
*
* @param element
* @return
*/
@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. 参考博文