授课语音

堆的基本操作

堆是一种完全二叉树,它可以分为最大堆和最小堆两种类型。最大堆的特点是每个节点的值都大于或等于其子节点的值,而最小堆则是每个节点的值都小于或等于其子节点的值。堆通常用来实现优先队列,它在许多算法中都扮演着重要的角色,比如堆排序、图的最短路径算法、动态中位数等。

在堆的实现中,我们通常通过数组来表示堆结构,这样可以利用数组的索引关系来方便地获取父节点和子节点的位置。我们将讨论堆的基本操作:插入操作、删除操作、堆化操作。

堆的基本概念

堆是一种特殊的完全二叉树,它满足堆的性质。我们来了解一下堆的性质:

  1. 完全二叉树:堆是一颗完全二叉树,也就是说,它除了最后一层外,每一层都是满的,且最后一层的节点都集中在左侧。
  2. 堆的性质
    • 在最大堆中,父节点的值大于或等于子节点的值。
    • 在最小堆中,父节点的值小于或等于子节点的值。

在数组中表示堆时,堆的根节点位于数组的第一个位置,父节点和子节点的关系通过索引来维护:

  • 对于一个节点i,左子节点的索引为 2i + 1,右子节点的索引为 2i + 2
  • 父节点的索引为 (i - 1) / 2

堆的插入操作

堆的插入操作就是将一个新的元素添加到堆中,并保持堆的性质。插入的过程一般分为两个步骤:

  1. 将新元素放到堆的最后一个位置(数组的末尾)。
  2. 通过“上浮”操作,比较新元素与其父节点的值,直到堆的性质得以恢复。

插入操作的时间复杂度

插入操作的时间复杂度是O(log n),因为每次插入时,我们最多需要经过堆的高度进行调整,堆的高度是log n。

堆的删除操作

堆的删除操作通常是删除堆顶元素(即最大堆的最大元素或最小堆的最小元素)。删除堆顶元素的过程如下:

  1. 将堆顶元素与堆的最后一个元素交换。
  2. 删除堆顶元素。
  3. 通过“下沉”操作调整堆,确保堆的性质得以恢复。

“下沉”操作会将堆顶元素与其子节点进行比较,并交换位置,直到堆的性质恢复。

删除操作的时间复杂度

删除操作的时间复杂度是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();
    }
}

代码解释

  1. 构造函数:我们初始化一个数组来存储堆的数据,并设置当前堆的大小为0。
  2. 插入操作:插入一个新元素时,我们将元素放到数组的末尾,并调用heapifyUp方法来上浮元素,确保堆的性质得以恢复。
  3. 删除操作:删除堆顶元素时,我们将堆顶元素与堆的最后一个元素交换,然后调用heapifyDown方法来下沉元素,恢复堆的性质。
  4. heapifyUpheapifyDown操作:这两个操作用于调整堆,使得堆的性质得到维护。heapifyUp从当前节点开始,一直上浮到堆的根部;heapifyDown从堆顶开始,一直下沉到叶子节点。

运行结果

堆的内容:
[30, 20, 5, 10]
删除堆顶元素:30
堆的内容:
[20, 10, 5]

堆操作的时间复杂度

  1. 插入操作:O(log n),因为我们可能需要通过上浮操作调整堆的结构,最多需要调整树的高度。
  2. 删除操作:O(log n),因为我们需要通过下沉操作调整堆的结构,同样最多需要调整树的高度。
  3. 堆化操作:O(n),因为我们需要对每个节点执行下沉操作,虽然每个节点可能最多执行log n次下沉,但总的来说堆化的时间复杂度为O(n)。

总结

堆是一个非常重要的数据结构,特别适用于需要频繁获取最大值或最小值的场景。它的插入和删除操作都具有较好的时间复杂度,并且可以通过数组来高效地实现。了解堆的基本操作和时间复杂度,有助于在实际问题中选择合适的数据结构进行优化。

去1:1私密咨询

系列课程: