第2课_堆的基本操作
热度🔥:16 免费课程
授课语音
堆的基本操作
堆是一种完全二叉树,它可以分为最大堆和最小堆两种类型。最大堆的特点是每个节点的值都大于或等于其子节点的值,而最小堆则是每个节点的值都小于或等于其子节点的值。堆通常用来实现优先队列,它在许多算法中都扮演着重要的角色,比如堆排序、图的最短路径算法、动态中位数等。
在堆的实现中,我们通常通过数组来表示堆结构,这样可以利用数组的索引关系来方便地获取父节点和子节点的位置。我们将讨论堆的基本操作:插入操作、删除操作、堆化操作。
堆的基本概念
堆是一种特殊的完全二叉树,它满足堆的性质。我们来了解一下堆的性质:
- 完全二叉树:堆是一颗完全二叉树,也就是说,它除了最后一层外,每一层都是满的,且最后一层的节点都集中在左侧。
- 堆的性质:
- 在最大堆中,父节点的值大于或等于子节点的值。
- 在最小堆中,父节点的值小于或等于子节点的值。
在数组中表示堆时,堆的根节点位于数组的第一个位置,父节点和子节点的关系通过索引来维护:
- 对于一个节点i,左子节点的索引为
2i + 1
,右子节点的索引为2i + 2
。 - 父节点的索引为
(i - 1) / 2
。
堆的插入操作
堆的插入操作就是将一个新的元素添加到堆中,并保持堆的性质。插入的过程一般分为两个步骤:
- 将新元素放到堆的最后一个位置(数组的末尾)。
- 通过“上浮”操作,比较新元素与其父节点的值,直到堆的性质得以恢复。
插入操作的时间复杂度
插入操作的时间复杂度是O(log n),因为每次插入时,我们最多需要经过堆的高度进行调整,堆的高度是log n。
堆的删除操作
堆的删除操作通常是删除堆顶元素(即最大堆的最大元素或最小堆的最小元素)。删除堆顶元素的过程如下:
- 将堆顶元素与堆的最后一个元素交换。
- 删除堆顶元素。
- 通过“下沉”操作调整堆,确保堆的性质得以恢复。
“下沉”操作会将堆顶元素与其子节点进行比较,并交换位置,直到堆的性质恢复。
删除操作的时间复杂度
删除操作的时间复杂度是O(log n),因为每次删除堆顶元素后,调整堆的过程中我们最多需要经过堆的高度。
堆化操作
堆化是将一个无序的数组变成一个堆的过程。堆化的基本思想是从最后一个非叶子节点开始,依次进行下沉操作,直到整个树满足堆的性质。堆化操作常用于堆排序算法中。
堆化的时间复杂度
堆化操作的时间复杂度是O(n),因为对于每个节点的下沉操作,在最坏的情况下最多进行log n次,而堆化过程需要对每个节点进行下沉操作。
Java代码案例
接下来,我们通过一个简单的Java代码示例来演示堆的插入和删除操作。我们将实现一个最大堆,并展示如何进行插入和删除操作。
最大堆的实现
import java.util.Arrays;
public class MaxHeap {
private int[] heap;
private int size;
// 构造函数,初始化堆的大小和数组
public MaxHeap(int capacity) {
heap = new int[capacity];
size = 0;
}
// 获取父节点索引
private int parent(int i) {
return (i - 1) / 2;
}
// 获取左子节点索引
private int leftChild(int i) {
return 2 * i + 1;
}
// 获取右子节点索引
private int rightChild(int i) {
return 2 * i + 2;
}
// 插入新元素
public void insert(int value) {
if (size == heap.length) {
System.out.println("堆已满,无法插入");
return;
}
heap[size] = value; // 将元素放到堆的末尾
size++;
heapifyUp(size - 1); // 上浮操作,保持堆的性质
}
// 上浮操作,确保堆的性质
private void heapifyUp(int i) {
while (i > 0 && heap[parent(i)] < heap[i]) {
// 如果父节点小于当前节点,交换它们
swap(i, parent(i));
i = parent(i);
}
}
// 删除堆顶元素
public int remove() {
if (size == 0) {
System.out.println("堆为空,无法删除");
return -1;
}
int root = heap[0]; // 获取堆顶元素
heap[0] = heap[size - 1]; // 将堆顶元素替换为最后一个元素
size--;
heapifyDown(0); // 下沉操作,恢复堆的性质
return root;
}
// 下沉操作,确保堆的性质
private void heapifyDown(int i) {
int left = leftChild(i);
int right = rightChild(i);
int largest = i;
// 找到左右子节点中较大的那个
if (left < size && heap[left] > heap[largest]) {
largest = left;
}
if (right < size && heap[right] > heap[largest]) {
largest = right;
}
// 如果最大值不是当前节点,交换并继续下沉
if (largest != i) {
swap(i, largest);
heapifyDown(largest);
}
}
// 交换两个节点的值
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
// 打印堆的内容
public void printHeap() {
System.out.println(Arrays.toString(Arrays.copyOf(heap, size)));
}
public static void main(String[] args) {
MaxHeap maxHeap = new MaxHeap(10);
maxHeap.insert(10);
maxHeap.insert(20);
maxHeap.insert(5);
maxHeap.insert(30);
System.out.println("堆的内容:");
maxHeap.printHeap();
System.out.println("删除堆顶元素:" + maxHeap.remove());
maxHeap.printHeap();
}
}
代码解释
- 构造函数:我们初始化一个数组来存储堆的数据,并设置当前堆的大小为0。
- 插入操作:插入一个新元素时,我们将元素放到数组的末尾,并调用
heapifyUp
方法来上浮元素,确保堆的性质得以恢复。 - 删除操作:删除堆顶元素时,我们将堆顶元素与堆的最后一个元素交换,然后调用
heapifyDown
方法来下沉元素,恢复堆的性质。 heapifyUp
和heapifyDown
操作:这两个操作用于调整堆,使得堆的性质得到维护。heapifyUp
从当前节点开始,一直上浮到堆的根部;heapifyDown
从堆顶开始,一直下沉到叶子节点。
运行结果
堆的内容:
[30, 20, 5, 10]
删除堆顶元素:30
堆的内容:
[20, 10, 5]
堆操作的时间复杂度
- 插入操作:O(log n),因为我们可能需要通过上浮操作调整堆的结构,最多需要调整树的高度。
- 删除操作:O(log n),因为我们需要通过下沉操作调整堆的结构,同样最多需要调整树的高度。
- 堆化操作:O(n),因为我们需要对每个节点执行下沉操作,虽然每个节点可能最多执行log n次下沉,但总的来说堆化的时间复杂度为O(n)。
总结
堆是一个非常重要的数据结构,特别适用于需要频繁获取最大值或最小值的场景。它的插入和删除操作都具有较好的时间复杂度,并且可以通过数组来高效地实现。了解堆的基本操作和时间复杂度,有助于在实际问题中选择合适的数据结构进行优化。