“从根到叶:深入理解堆数据结构“

news2024/11/28 16:48:10

​​​​​​​

一.堆的概念及实现

1.1堆的概念

在数据结构中,堆是一种特殊的树形数据结构。堆可以分为最大堆和最小堆两种类型。

最大堆:对于堆中的任意节点,其父节点的值都不小于它的值。换句话说,最大堆中的根节点是堆中的最大值。并且,最大堆的任意子树也都是最大堆。

最小堆:对于堆中的任意节点,其父节点的值都不大于它的值。最小堆中的根节点是堆中的最小值,且任意子树也都是最小堆。

堆通常用一个数组来表示,其中每个元素对应堆的一个节点。堆的性质保证了数组中的元素满足特定的顺序关系。

在堆中,通常可以进行以下操作:

  • 插入:将一个元素插入到堆中的合适位置,保持堆的性质。
  • 删除根节点:删除堆中的根节点,并保持堆的性质。对于最大堆,删除的是最大值;对于最小堆,删除的是最小值。
  • 堆化:将一个无序的数组转换为堆的形式。
  • 取最值 :获取堆中的最大值或最小值,即根节点的值。

堆广泛应用于各种算法和数据结构中,例如堆排序、优先队列、图算法(如最短路径算法中的Dijkstra算法)等。堆的特性使得这些算法具有高效的时间复杂度。

举个简单的例子解释堆的概念:

假设有一堆学生的成绩数据,每个学生都有一个分数,表示他们的学术表现。最大堆代表这种情况:每个学生的分数都比他们的孩子(下面的学生)更高。

现在,你想根据学生成绩来组织这些数据。你将最高分的学生放在最前面,其次是次高分的学生,以此类推。这样,你会得到一个最大堆,其中根节点是分数最高的学生,而任何一个学生的分数都不会超过他的父节点。

当你要添加新的学生成绩时,你需要将其放置在正确的位置,以保持最大堆的性质。如果新的学生的分数比他的父节点更高,你可能需要将他与父节点交换位置,以确保最大堆的性质。

最小堆的情况则相反。假设你有一堆学生的成绩数据,每个学生的分数都比他们的孩子更低。这意味着你将最低分的学生放置在最前面,而任何一个学生的分数都不会低于他的父节点。

1.2堆的性质

堆是一种特殊的数据结构,具有以下性质:

1. 堆是一个完全二叉树:堆是由完全二叉树组成的,意味着除了最后一层外,其它层都必须填满,且最后一层的节点都靠左排列。

2. 最大堆性质:对于最大堆,父节点的值大于或等于其子节点的值。换句话说,堆中的最大元素位于根节点。

3. 最小堆性质:对于最小堆,父节点的值小于或等于其子节点的值。换句话说,堆中的最小元素位于根节点。

4. 堆序性质:堆中的每个节点都必须满足堆的性质,即父节点的值要么大于等于(最大堆)或小于等于(最小堆)子节点的值。这意味着在堆中,无论是最大堆还是最小堆,根节点都是堆中的最大或最小元素。

5. 堆的高度:堆的高度是指从根节点到叶子节点的最长路径的长度。对于一个有 n 个节点的堆,其高度通常为 O(log n)。

这些性质使得堆成为一种非常有用的数据结构,尤其在优先队列、堆排序和图算法中经常被使用。堆的特点使得我们能够高效地访问和操作具有最大或最小优先级的元素,从而提高算法的效率和性能。


1.3堆的存储方式

堆可以使用数组来进行存储。在数组表示中,每个元素对应堆中的一个节点,通过数组的索引来确定节点之间的关系。

对于一个堆,我们使用以下规则来存储节点:

  1. 根节点存储在数组的索引位置 0 处。
  2. 对于任意节点 i,其父节点存储在索引位置 (i-1)/2 处。
  3. 对于任意节点 i,其左子节点存储在索引位置 2i+1 处。
  4. 对于任意节点 i,其右子节点存储在索引位置 2i+2 处。

比如如下存储最大堆

数组的存储结构是这样:

数组表示: [90, 80, 75, 60, 55, 40, 30, 20, 10, 25]
索引表示:  0   1   2   3   4   5   6   7   8   9

 数组的逻辑结构是这样:

简单解释索引位置的关系:

这样的关系是由完全二叉树的性质决定的。在完全二叉树中,每个节点都有可能存在左子节点和右子节点,且它们的位置是固定的。通过将完全二叉树的节点按照一定顺序存储在数组中,我们可以利用数组的索引来表示节点之间的关系。

  1. 根节点存储在数组的索引位置 0 处:因为完全二叉树的根节点始终位于最上层,所以它在数组中的位置是固定的,即索引位置 0 处。

  2. 对于任意节点 i,其父节点存储在索引位置 (i-1)/2 处:通过简单的数学计算,我们可以确定父节点在数组中的位置。对于任意节点 i,我们将节点索引减去 1,然后除以 2,可以得到父节点在数组中的索引位置。

  3. 对于任意节点 i,其左子节点存储在索引位置 2i+1 处:根据完全二叉树的性质,左子节点总是在父节点的左侧,所以我们可以通过将节点索引乘以 2,再加上 1,得到左子节点在数组中的索引位置。

  4. 对于任意节点 i,其右子节点存储在索引位置 2i+2 处:类似地,根据完全二叉树的性质,右子节点总是在父节点的右侧,所以我们可以通过将节点索引乘以 2,再加上 2,得到右子节点在数组中的索引位置


  5. 例子:

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储

注意 :对于 非完全二叉树,则不适合使用顺序方式进行存储 ,因为为了能够还原二叉树, 空间中必须要存储空节 点,就会导致空间利用率比较低
将元素存储到数组中后,可以根据二叉树 对树进行还原。假设  为节点在数组中的下标,则有:
  • 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
  • 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
  • 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
  • 比如:

二.堆的创建及时间复杂度

2.1堆向下过程(以小堆为例)

堆的创建是指将一个无序的数组或数据集转换为堆的过程。创建堆的常见方法是使用向下过程

以下是使用向下过程创建堆的一般步骤:

  1. 从最后一个非叶子节点开始,依次向上迭代直到根节点。最后一个非叶子节点的索引为数组长度的一半减一(n/2 - 1)。
  2. 对于每个节点,执行向下过程:
  3. 比较当前节点与其子节点的值,并找到值最大(或最小)的子节点。如果有左子节点,其索引为2 * i + 1;如果有右子节点,则索引为2 * i + 2。
  4. 如果当前节点的值小于其最大(或最小)子节点的值,则交换当前节点与最大(或最小)子节点的值。
  5. 更新当前节点的索引为最大(或最小)子节点的索引,即`i`的值更新为最大(或最小)子节点的索引。
  6. 重复步骤3至5,直到节点`i`不再有子节点或其值大于(或小于)其子节点的值。
  7. 重复步骤2,直到根节点。

通过以上步骤,将数组或数据集中的元素逐个进行向下过程,最终可以创建一个满足堆的性质的堆。在最大堆中,每个节点的值都大于或等于其子节点的值;在最小堆中,每个节点的值都小于或等于其子节点的值。

需要注意的是,堆的创建过程的时间复杂度为O(n),其中n是数组或数据集的大小。

参考图示如下:

  1. parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
  2. 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在 parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标识。
  3. parent与较小的孩子child比较,如果parent小于较小的孩子child,调整结束
  4. 否则交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = childchild = parent*2+1。

参考代码如下:

public void shiftDown(int[] array, int parent) {
    // child先标记parent的左孩子,因为parent可能右左没有右
    int child = 2 * parent + 1;
    int size = array.length;
    while (child < size) {
    // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
    if(child+1 < size && array[child+1] < array[child]){
    child += 1;
}
    // 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
if (array[parent] <= array[child]) {
             break;
}else{
    // 将双亲与较小的孩子交换
    int t = array[parent];
    array[parent] = array[child];
    array[child] = t;
    // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
    parent = child;
    child = parent * 2 + 1;
        }
    }
}
注意:在调整以 parent 为根的二叉树时,必须要满足 parent 的左子树和右子树已经是堆了才可以向下调整。
最坏的情况 即图示的情况, 从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度
为O(log2N)。
如果对于普通的序列 { 1,5,3,8,7,6 } ,即根节点的左右子树不满足堆的特性,又该如何调整呢?
参考代码:
public static void createHeap(int[] array) {
// 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
int root = ((array.length-2)>>1);
for (; root >= 0; root--) {
      shiftDown(array, root);
    }
}

2.2堆的时间复杂度

建堆的时间复杂度为O(n),其中n是堆中的元素数量。

建堆的过程可以通过向下过程来完成。在向下过程中,每个节点最多需要下降到其合适的位置,而每次下降的过程中,最多需要比较和交换节点的次数与树的高度成正比。树的高度通常为log(n),其中n是堆中的元素数量。

因此,对于n个元素的堆来说,最坏情况下,每个节点可能需要进行log(n)次比较和交换。由于堆中有n个节点,所以总的比较和交换次数为n乘以log(n),因此建堆的时间复杂度为O(nlog(n))。

然而,这是最坏情况的时间复杂度。在实际应用中,建堆的平均时间复杂度要小于O(nlog(n))。具体而言,通过巧妙的实现和优化技巧,可以将建堆的平均时间复杂度降低到O(n)。这是因为向下过程的时间复杂度取决于节点的高度,而大多数节点的高度都较小。

因此,总体而言,建堆的时间复杂度为O(n),但在最坏情况下可能达到O(nlog(n))。

参考如图所示:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明 ( 时间复杂度本来看的就是近似值,多几个节点不影响最终结果)

三.堆的插入与删除

3.1堆的插入

堆的插入操作:是将一个新元素插入到堆中,并保持堆的性质不变。通常,插入操作是在堆的最后一个位置进行。

以下是堆的插入操作的一般步骤:

  1. 将新元素插入到堆的最后一个位置。
  2. 将新元素与其父节点进行比较,如果新元素的值大于(或小于,具体取决于是最大堆还是最小堆)其父节点的值,则交换新元素与父节点的值。
  3. 更新新元素的位置为其父节点的位置。
  4. 重复步骤2和3,直到新元素的值小于(或大于)其父节点的值,或者新元素成为根节点。
  5. 插入操作完成。

注意:插入操作的时间复杂度为O(log(n)),其中n是堆中的元素数量。这是因为插入操作需要从新元素所在的位置向上迭代,最多迭代堆的高度次数,而堆的高度通常为log(n)。因此,插入操作的时间复杂度与堆的高度成正比。

参考图示如下:

参考代码如下:

       public void shiftUp(int child) {
            // 找到child的双亲
            int parent = (child - 1) / 2;
            while (child > 0) {
            // 如果双亲比孩子大,parent满足堆的性质,调整结束
                if (array[parent] > array[child]) {
                    break;
                } else {
            // 将双亲与孩子节点进行交换
                    int t = array[parent];
                    array[parent] = array[child];
                    array[child] = t;
            // 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增
                    child = parent;
                    parent = (child - 1) / 2;
                }
            }
        }

3.2堆的删除

堆的删除操作:将堆中的根节点删除,并保持堆的性质不变。在最大堆中,删除的是具有最大值的根节点;在最小堆中,删除的是具有最小值的根节点。

以下是堆的删除操作的一般步骤:

  1. 将根节点与堆中最后一个节点交换。
  2. 删除堆中的最后一个节点(即原根节点)。
  3. 对新的根节点执行向下过程(Heapify):
    a. 比较当前节点与其子节点的值,并找到值最大(或最小)的子节点。如果有左子节点,其索引为2 * i + 1;如果有右子节点,则索引为2 * i + 2
    b. 如果当前节点的值小于(或大于)其最大(或最小)子节点的值,则交换当前节点与最大(或最小)子节点的值。
    c. 更新当前节点的索引为最大(或最小)子节点的索引,即i的值更新为最大(或最小)子节点的索引。
    d. 重复步骤a至c,直到节点i不再有子节点或其值大于(或小于)其子节点的值。
  4. 直至删除操作完成。

通过以上步骤,删除操作将原根节点移除,并将堆的最后一个节点放置到根的位置,然后通过向下过程将新的根节点下降到合适的位置,以满足堆的性质。

注意:删除操作的时间复杂度为O(log(n)),其中n是堆中的元素数量。这是因为删除操作需要执行一次交换操作,并对新的根节点执行向下过程,最多迭代堆的高度次数,而堆的高度通常为log(n)。因此,删除操作的时间复杂度与堆的高度成正比

参考图示如下:

参考代码如下:

public class MaxHeap {
    private int[] heap;
    private int size;

    public MaxHeap(int capacity) {
        heap = new int[capacity];
        size = 0;
    }

    public void deleteMax() {
        if (size == 0) {
            System.out.println("Heap is empty.");
            return;
        }

        // 将根节点与最后一个节点交换
        int temp = heap[0];
        heap[0] = heap[size - 1];
        heap[size - 1] = temp;

        size--;

        // 执行向下过程
        heapifyDown(0);
    }

    private void heapifyDown(int index) {
        int parent = index;

        while (true) {
            int leftChild = 2 * parent + 1;
            int rightChild = 2 * parent + 2;
            int largest = parent;

            // 比较当前节点与左子节点的值
            if (leftChild < size && heap[leftChild] > heap[largest]) {
                largest = leftChild;
            }

            // 比较当前节点与右子节点的值
            if (rightChild < size && heap[rightChild] > heap[largest]) {
                largest = rightChild;
            }

            // 如果当前节点不是最大值,则交换当前节点与最大子节点的值
            if (largest != parent) {
                int temp = heap[parent];
                heap[parent] = heap[largest];
                heap[largest] = temp;

                // 更新当前节点,继续向下处理
                parent = largest;
            } else {
                break; // 当前节点已经是最大值,停止循环
            }
        }
    }
}

3.3堆模拟实现优先级队列

优先级队列是根据元素的优先级进行排序的数据结构。堆是一种常用的实现优先级队列的数据结构。

堆的操作:
1. 添加元素:将新元素添加到堆的末尾,然后执行向上调整操作,将新元素上移至适当的位置,以满足堆的性质。
2. 删除元素:通常,优先级队列删除的是具有最高(或最低)优先级的元素。在最大堆中,删除根节点;在最小堆中,删除根节点。删除后,将堆的最后一个元素移至根节点的位置,然后执行向下调整操作,将根节点下移至适当的位置,以满足堆的性质。

堆的优势:
1. 堆能够在添加和删除元素时快速维护元素的优先级顺序。
2. 添加和删除元素的时间复杂度为O(log n),其中n是堆中元素的数量。
3. 堆可以使用数组或链表来实现,其中数组实现是最常见和高效的方法。

堆作为一种实现优先级队列的数据结构,具有高效的插入和删除操作,因此在许多应用中得到广泛应用,如任务调度、图算法等。

参考代码如下:

import java.util.*;

public class PriorityQueue<T> {
    private PriorityQueueElement<T>[] heap;  // 存储堆的数组
    private int size;  // 堆的当前大小
    private int capacity;  // 堆的容量

    // 内部类,表示优先级队列中的元素
    private static class PriorityQueueElement<T> {
        private T item;  // 元素值
        private int priority;  // 优先级

        public PriorityQueueElement(T item, int priority) {
            this.item = item;
            this.priority = priority;
        }

        public T getItem() {
            return item;
        }

        public int getPriority() {
            return priority;
        }
    }

    public PriorityQueue() {
        capacity = 10;  // 默认容量为10
        size = 0;
        heap = new PriorityQueueElement[capacity];  // 创建堆数组
    }

    public void enqueue(T item, int priority) {
        if (size == capacity) {  // 如果堆已满,则扩容
            resizeHeap();
        }

        PriorityQueueElement<T> element = new PriorityQueueElement<>(item, priority);
        heap[size] = element;  // 将新元素添加到堆末尾
        size++;
        heapifyUp(size - 1);  // 执行向上调整操作,以维护堆的性质
    }

    public T dequeue() {
        if (isEmpty()) {
            throw new NoSuchElementException("Priority queue is empty.");
        }

        T removedItem = heap[0].getItem();  // 记录被移除的元素
        heap[0] = heap[size - 1];  // 将堆末尾元素移到根节点位置
        size--;
        heapifyDown(0);  // 执行向下调整操作,以维护堆的性质

        return removedItem;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void heapifyUp(int index) {
        int parentIndex = (index - 1) / 2;  // 计算父节点索引
        while (index > 0 && heap[index].getPriority() > heap[parentIndex].getPriority()) {
            swap(index, parentIndex);  // 如果节点优先级大于父节点优先级,交换它们
            index = parentIndex;
            parentIndex = (index - 1) / 2;
        }
    }

    private void heapifyDown(int index) {
        int leftChildIndex = 2 * index + 1;  // 计算左子节点索引
        int rightChildIndex = 2 * index + 2;  // 计算右子节点索引
        int largestIndex = index;

        if (leftChildIndex < size && heap[leftChildIndex].getPriority() > heap[largestIndex].getPriority()) {
            largestIndex = leftChildIndex;  // 如果左子节点优先级大于当前节点优先级,更新最大索引
        }

        if (rightChildIndex < size && heap[rightChildIndex].getPriority() > heap[largestIndex].getPriority()) {
            largestIndex = rightChildIndex;  // 如果右子节点优先级大于当前节点优先级,更新最大索引
        }

        if (largestIndex != index) {
            swap(index, largestIndex);  // 如果最大索引不是当前节点索引,交换它们
            heapifyDown(largestIndex);  // 递归向下调整
        }
    }

    private void swap(int i, int j) {
        PriorityQueueElement<T> temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }

    private void resizeHeap() {
        capacity *= 2;  // 扩大容量
        heap = Arrays.copyOf(heap, capacity);  // 创建新的堆数组
    }
}

我们使用一个数组来存储堆的元素,其中每个元素包含一个item和一个priority表示元素和其优先级。enqueue方法用于将元素加入队列,dequeue方法用于移除并返回具有最高优先级的元素,isEmpty方法用于检查队列是否为空。

在内部,我们使用heapifyUp方法来维护堆的性质,即将新加入的元素上移至适当的位置以满足堆的性质。我们还使用heapifyDown方法来维护堆的性质,在移除元素后将根节点下移至适当的位置。


四.常用接口介绍

4.1 PriorityQueue的特性

Java 集合框架中提供了 PriorityQueue PriorityBlockingQueue 两种类型的优先级队列, PriorityQueue 是线 程不安全的, PriorityBlockingQueue 是线程安全的 ,本文主要介绍 PriorityQueue
关于 PriorityQueue 的使用要注意:

1.使用时必须导入PriorityQueue所在的包,即:

import java . util . PriorityQueue ;

2. PriorityQueue 中放置的 元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException 异常
3. 不能 插入 null 对象,否则会抛出 NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5. 插入和删除元素的时间复杂度为
6. PriorityQueue 底层使用了堆数据结构
7. PriorityQueue 默认情况下是小堆 --- 即每次获取到的元素都是最小的元素
注意: 默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器
参考代码如下:
  // 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
        class IntCmp implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        }
        public class TestPriorityQueue {
            public static void main(String[] args) {
                PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
                p.offer(4);
                p.offer(3);
                p.offer(2);
                p.offer(1);
                p.offer(5);
                System.out.println(p.peek());
            }
        }
此时创建出来的就是一个大堆。

4.2PriorityQueue中插入对象

优先级队列在插入元素时有个要求:插入的元素不能是 null 或者元素之间必须要能够
进行比较 ,为了简单起见,我们只是插入了 Integer 类型,那优先级队列中能否插入自定义类型对象呢?
 class Card {
            public int rank; // 数值
            public String suit; // 花色
            public Card(int rank, String suit) {
                this.rank = rank;
                this.suit = suit;
            }
        }
        public class TestPriorityQueue {
            public static void TestPriorityQueue() {
                PriorityQueue<Card> p = new PriorityQueue<>();
                p.offer(new Card(1, "♠"));
                p.offer(new Card(2, "♠"));
            }
            public static void main(String[] args) {
                TestPriorityQueue();
            }
        }
优先级队列底层使用堆,而向堆中插入元素时,为了满足堆的性质,必须要进行元素的比较,而此时 Card 是没有办 法直接进行比较的,因此抛出异常。

4.3对象的比较

       class Card {
            public int rank; // 数值
            public String suit; // 花色
            public Card(int rank, String suit) {
                this.rank = rank;
                this.suit = suit;
            }
        }
        public class TestPriorityQueue {
            public static void main(String[] args) {
                Card c1 = new Card(1, "♠");
                Card c2 = new Card(2, "♠");
                Card c3 = c1;
                //System.out.println(c1 > c2); // 编译报错
                System.out.println(c1 ==c2); 
                // 编译成功 ----> 打印false,因为c1和c2指向的是不同对象
                //System.out.println(c1 < c2); // 编译报错
                System.out.println(c1 ==c3);
                // 编译成功 ----> 打印true,因为c1和c3指向的是同一个对象
            }
        }
c1 c2 c3 分别是 Card 类型的引用变量,上述代码在比较编译时:
c1 > c2 编译失败
c1== c2 编译成功
c1 < c2 编译失败
从编译结果可以看出, Java 中引用类型的变量不能直接按照 > 或者 < 方式进行比较 。 那为什么 == 可以比较?
因为: 对于用户实现自定义类型,都默认继承自 Object 类,而 Object 类中提供了 equal 方法,而 == 默认情况下调
用的就是 equal 方法 ,但是该方法的比较规则是: 没有比较引用变量引用对象的内容,而是直接比较引用变量的地
,但有些情况下该种比较就不符合题意。
// Object中equal的实现,可以看到:直接比较的是两个引用变量的地址
public boolean equals(Object obj) {
return (this == obj);
}
4.3.1 覆写基类的equals
 public class Card {
            public int rank; // 数值
            public String suit; // 花色
            public Card(int rank, String suit) {
                this.rank = rank;
                this.suit = suit;
            }
            @Override
            public boolean equals(Object o) {
// 自己和自己比较
                if (this == o) {
                    return true;
                }
// o如果是null对象,或者o不是Card的子类
                if (o == null || !(o instanceof Card)) {
                    return false;
                }
// 注意基本类型可以直接比较,但引用类型最好调用其equal方法
                Card c = (Card)o;
                return rank == c.rank
                       && suit.equals(c.suit);
            }
        }
注意: 一般覆写 equals 的套路就是上面演示的
  • 如果指向同一个对象,返回 true
  •  如果传入的为 null,返回 false
  • 如果传入的对象类型不是 Card,返回 false
  • 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
  • 注意下调用其他引用类型的比较也需要 equals,例如这里的 suit 的比较
覆写基类 equal 的方式虽然可以比较,但缺陷是: equal 只能按照相等进行比较,不能按照大于、小于的方式进行 比较

4.3.2基于Comparble接口类的比较
Comparble JDK 提供的泛型的比较接口类,源码实现具体如下:
public interface Comparable<E> {
// 返回值:
// < 0: 表示 this 指向的对象小于 o 指向的对象
// == 0: 表示 this 指向的对象等于 o 指向的对象
// > 0: 表示 this 指向的对象大于 o 指向的对象
int compareTo(E o);
}
对用用户自定义类型,如果要想按照大小与方式进行比较时: 在定义类时,实现 Comparble 接口即可,然后在类 中重写 compareTo 方法。
public class Card implements Comparable<Card> {
            public int rank; // 数值
            public String suit; // 花色
            public Card(int rank, String suit) {
                this.rank = rank;
                this.suit = suit;
            }
            // 根据数值比较,不管花色
            // 这里我们认为 null 是最小的
            @Override
            public int compareTo(Card o) {
                if (o == null) {
                    return 1;
                }
                return rank - o.rank;
            }
            public static void main(String[] args) {
                Card p = new Card(1, "♠");
                Card q = new Card(2, "♠");
                Card o = new Card(1, "♠");
                System.out.println(p.compareTo(o)); // == 0,表示牌相等
                System.out.println(p.compareTo(q)); // < 0,表示 p 比较小
                System.out.println(q.compareTo(p)); // > 0,表示 q 比较大
            }
        }
Compareble java.lang 中的接口类,可以直接使用。
4.3.3基于比较器比较
按照比较器方式进行比较,具体步骤如下:
  • 用户自定义比较器类,实现Comparator接口
  • public interface Comparator<T> {
    // 返回值:
    // < 0: 表示 o1 指向的对象小于 o2 指向的对象
    // == 0: 表示 o1 指向的对象等于 o2 指向的对象
    // > 0: 表示 o1 指向的对象等于 o2 指向的对象
    int compare(T o1, T o2);
    }
    注意: 区分 Comparable Comparator
  • 覆写 Comparator 中的 compare 方法
    import java.util.Comparator;
    class Card {
        public int rank; // 数值
        public String suit; // 花色
        public Card(int rank, String suit) {
            this.rank = rank;
            this.suit = suit;
        }
    }
    class CardComparator implements Comparator<Card> {
    // 根据数值比较,不管花色
    // 这里我们认为 null 是最小的
        @Override
        public int compare(Card o1, Card o2) {
            if (o1 == o2) {
                return 0;
            }
            if (o1 == null) {
                return -1;
            }
            if (o2 == null) {
                return 1;
            }
            return o1.rank - o2.rank;
        }
        public static void main(String[] args) {
            Card p = new Card(1, "♠");
            Card q = new Card(2, "♠");
            Card o = new Card(1, "♠");
    // 定义比较器对象
            CardComparator cmptor = new CardComparator();
    // 使用比较器对象进行比较
            System.out.println(cmptor.compare(p, o)); // == 0,表示牌相等
            System.out.println(cmptor.compare(p, q)); // < 0,表示 p 比较小
            System.out.println(cmptor.compare(q, p)); // > 0,表示 q 比较大
        }
    }

4.3.4 三种方式对比  

集合框架中的 PriorityQueue 底层使用堆结构,因此其内部的元素必须要能够比大小, PriorityQueue 采用了: Comparble和 Comparator 两种方式。
  1. Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接口,并覆写compareTo方法
  2. 用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现Comparator接口并覆写compare方法。

参考代码如下:

// JDK中PriorityQueue的实现:
public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {
// 默认容量
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 内部定义的比较器对象,用来接收用户实例化PriorityQueue对象时提供的比较器对象
    private final Comparator<? super E> comparator;
// 用户如果没有提供比较器对象,使用默认的内部比较,将comparator置为null
    public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
// 如果用户提供了比较器,采用用户提供的比较器进行比较
    public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }
// 向上调整:
// 如果用户没有提供比较器对象,采用Comparable进行比较
// 否则使用用户提供的比较器对象进行比较
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }
// 使用Comparable
    @SuppressWarnings("unchecked")
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }
// 使用用户提供的比较器对象进行比较
    @SuppressWarnings("unchecked")
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }
}


五.堆的应用

5.1常用函数名和功能介绍

参考代码如下:

        static void TestPriorityQueue2() {
            int[] arr = {4, 1, 9, 2, 8, 0, 7, 3, 6, 5};
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
            PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
            for (int e : arr) {
                q.offer(e);
            }
            System.out.println(q.size()); // 打印优先级队列中有效元素个数
            System.out.println(q.peek()); // 获取优先级最高的元素
// 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
            q.poll();
            q.poll();
            System.out.println(q.size()); // 打印优先级队列中有效元素个数
            System.out.println(q.peek()); // 获取优先级最高的元素
            q.offer(0);
            System.out.println(q.peek()); // 获取优先级最高的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
            q.clear();
            if (q.isEmpty()) {
                System.out.println("优先级队列已经为空!!!");
            } else {
                System.out.println("优先级队列不为空");
            }
        }

 注意:以下是JDK 1.8中,PriorityQueue的扩容方式

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
优先级队列的扩容说明:
  • 如果容量小于64时,是按照oldCapacity2倍方式扩容的
  • 如果容量大于等于64,是按照oldCapacity1.5倍方式扩容的
  • 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

5.2堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
  • 升序:建大堆
  • 降序:建小堆

2.利用堆删除思想来进行排序

  • 建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

参考图示如下:


5.3Top-k问题

TOP-K 问题:即求数据集合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决,基本思路如下:

 1. 用数据集合中前K个元素来建堆

  • k个最大的元素,则建小堆
  • k个最小的元素,则建大堆
2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。 
使用 PriorityQueue 创建大小堆,解决 TOPK 问题
//使用比较器创建小根堆
class LessIntComp implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
}
//使用比较器创建大根堆
class GreaterIntComp implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}
public class TestDemo<E> {
//求最小的K个数,通过比较器创建大根堆
    public static int[] smallestK(int[] array, int k) {
        if (k <= 0) {
            return new int[k];
        }
        GreaterIntComp greaterCmp = new GreaterIntComp();
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(greaterCmp);
//先将前K个元素,创建大根堆
        for (int i = 0; i < k; i++) {
            maxHeap.offer(array[i]);
        }
//从第K+1个元素开始,每次和堆顶元素比较
        for (int i = k; i < array.length; i++) {
            int top = maxHeap.peek();
            if (array[i] < top) {
                maxHeap.poll();
                maxHeap.offer(array[i]);
            }
        }
//取出前K个
        int[] ret = new int[k];
        for (int i = 0; i < k; i++) {
            int val = maxHeap.poll();
            ret[i] = val;
        }
        return ret;
    }
    public static void main(String[] args) {
        int[] array = {4, 1, 9, 2, 8, 0, 7, 3, 6, 5};
        int[] ret = smallestK(array, 3);
        System.out.println(Arrays.toString(ret));
    }
}
相关题目: 面试题 17.14. 最小K个数 - 力扣(LeetCode)

总结

总结起来,堆是一种强大而高效的数据结构,在计算机科学中扮演着重要的角色。通过了解堆的定义、性质和操作,我们深入探索了它在算法和数据处理中的应用。堆排序作为一种基于堆的排序算法,为我们提供了一种高效、可靠的排序解决方案。同时,堆还广泛应用于优先级队列、图算法等领域,为我们解决各种实际问题提供了强大的工具。

最后,感谢你阅读这篇关于堆数据结构的博客。希望这篇文章能为你提供有价值的信息,并激发你对堆的兴趣。如果你有任何问题、想法或反馈,欢迎留言与评论区。我期待着与你交流,一起深入探讨堆数据结构以及其他有关计算机科学的话题,愿读者收获满满!同时祝愿读者在未来的学习和实践中取得巨大的成功!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1448237.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java 数据结构】泛型进阶

泛型 1 什么是泛型2 引出泛型2.1 语法 3 泛型类的使用3.1 语法3.2 示例3.3 类型推导(Type Inference) 泛型是如何编译的擦除机制裸类型4 泛型的上界4.1 语法4.2 示例4.3 复杂示例 5 泛型方法5.1 定义语法5.2 示例5.3 使用示例-可以类型推导5.4 使用示例-不使用类型推导 6 通配符…

【电路笔记】-串联电感

串联电感 文章目录 串联电感1、概述2、电感串联示例13、互耦串联电感器4、电感串联示例25、电感串联示例36、总结当电感器以菊花链方式连接在一起并共享公共电流时,它们可以串联连接在一起。 1、概述 这些电感器的互连产生了更复杂的网络,其总电感是各个电感器的组合。 然而…

深度学习从入门到不想放弃-7

上一章的内容 深度学习从入门到不想放弃-6 (qq.com) 今天讲的也算基础(这个系列后来我一寻思,全是基础 ),但是可能要着重说下,今天讲前向计算和反向传播,在哪儿它都永远是核心,不管面对什么模型 前向计算: 有的叫也叫正向传播,正向计算的,有的直接把前向的方法梯度下…

九、优化强度(Optimizing Intensity)

7.Optimizing Intensity 七、优化强度 Researchers have known for decades that there is a relationship between the optimal intensity of attention and the complexity of the task you want to perform. 数十年来&#xff0c;研究者已经知道你想要完成的任务的复杂性与最…

【AI视野·今日CV 计算机视觉论文速览 第299期】Mon, 29 Jan 2024

AI视野今日CS.CV 计算机视觉论文速览 Mon, 29 Jan 2024 Totally 55 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers Annotated Hands for Generative Models Authors Yue Yang, Atith N Gandhi, Greg TurkGAN 和扩散模型等生成模型已经展示了…

【C/C++】2024春晚刘谦春晚魔术步骤模拟+暴力破解

在这个特别的除夕夜&#xff0c;我们不仅享受了与家人的温馨团聚&#xff0c;还被电视机前的春节联欢晚会深深吸引。特别是&#xff0c;魔术师刘谦的精彩表演&#xff0c;为我们带来了一场视觉和心灵的盛宴。在我的博客“【C/C】2024春晚刘谦春晚魔术步骤模拟暴力破解”中&…

OpenCV基础:用Python生成一幅随机的噪声图像

使用Python&#xff1a;生成一幅随机数值的灰度图像&#xff0c;图像大小为1616像素。借助OpenCV库。输出数值&#xff0c;并显示图像。 # -*- coding: utf-8 -*- """ Created on Wed Feb 14 21:49:09 2024author: 李立宗公众号&#xff1a;计算机视觉之光知识…

证明之毕达哥拉斯定理

毕达哥拉斯定理 毕达哥拉斯的著名定理所讲的是&#xff0c;假设一直角三角形的三边长为a、b和c&#xff0c;其中c是斜边长&#xff08;直角所对的边&#xff09;&#xff0c;则 a 2 b 2 c 2 a^2b^2c^2 a2b2c2。这个定理有若干种证明&#xff0c;其中有一种特别简短&#xf…

【黑马程序员】C++封装、对象特性、友元

文章目录 类和对象封装封装的意义struct和class的区别成员属性设置为私有封装练习设计立方体类点和圆的关系 对象的初始化和清理构造函数和析构函数构造函数析构函数 构造函数的分类及调用拷贝构造函数的调用时机构造函数调用规则深拷贝与浅拷贝浅拷贝深拷贝 初始化列表类对象作…

matplotlib雷达图制作具体步骤

此次我们制作的是关于护肤品下的畅销品类雷达图&#xff0c;数据如下&#xff1a; 数据预览&#xff1a; 一、代码展示 import pandas as pd import numpy as np import matplotlib.pyplot as plt plt.rcParams[font.sans-serif][SimHei] adress"D:/pandas练习文件/雷达…

JAVA设计模式之解释器模式详解

解释器模式 1 解释器模式介绍 解释器模式使用频率不算高&#xff0c;通常用来描述如何构建一个简单“语言”的语法解释器。它只在一些非常特定的领域被用到&#xff0c;比如编译器、规则引擎、正则表达式、SQL 解析等。不过&#xff0c;了解它的实现原理同样很重要&#xff0…

零基础学编程怎么入手,中文编程工具构件箱之星空构件用法教程,系统化的编程视频教程上线

零基础学编程怎么入手&#xff0c;中文编程工具构件箱之星空构件用法教程&#xff0c;系统化的编程视频教程上线 一、前言 今天给大家分享的中文编程开发语言工具资料如下&#xff1a; 编程入门视频教程链接 http://​ https://edu.csdn.net/course/detail/39036 ​ 编程…

Java17之使用Lambda表达式对对象集合中指定的字段进行排序

Java17之使用Lambda表达式对对象集合中指定的字段进行排序 文章目录 Java17之使用Lambda表达式对对象集合中指定的字段进行排序1. 集合对象排序1. Java实体类2. 正序排序3.倒序排序 1. 集合对象排序 Java8起可用 List 的 sort 方法进行排序&#xff0c;形参为函数式接口Compara…

C++ //练习 6.12 改写6.2.1节中练习6.10(第188页)的程序,使用引用而非指针交换两个整数的值。你觉得哪种方法更易于使用呢?为什么?

C Primer&#xff08;第5版&#xff09; 练习 6.12 练习 6.12 改写6.2.1节中练习6.10&#xff08;第188页&#xff09;的程序&#xff0c;使用引用而非指针交换两个整数的值。你觉得哪种方法更易于使用呢&#xff1f;为什么&#xff1f; 环境&#xff1a;Linux Ubuntu&#x…

Docker关于conda环境的导出和导入

Docker关于conda环境的导出和导入 1、常用命令&#xff1a;2、环境导出&#xff1a;3、两个服务器之间的文件传输命令&#xff1a;4、环境导入&#xff1a;5、快速进入容器6、其他问题解决记录&#xff1a; 1、常用命令&#xff1a; docker pull -- 从远程仓库将镜像下载至本地…

医疗相关名词,医疗名词整理

1.系统类&#xff1a; HIS Hospital Information System&#xff0c;医院信息系统&#xff0c;在国际学术界已公认为新兴的医学信息学(Medical Informatics)的重要分支。美国该领域的著名教授Morris.Collen于1988年曾著文为医院信息系统下了如下定义&#xff1a;利用电子计算…

[Linux开发工具]项目自动化构建工具-make/Makefile

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1.背景2.依赖关系和依…

error An unexpected error occurred: “https://registry.npm.taobao.org

背景&#xff1a; 想使用yarn命令结果报错 问题原因&#xff1a; 原来证书到期了 http://registry.npm.taobao.org/ 把这个放到浏览器搜索的时候自动换成https://registry.npmmirror.com/ 方案&#xff1a; npm cache clean --forcenpm config set registry https://registry…

Windows 虚拟桌面信息(一)分析注册表

目录 前言 一、理论分析 二、代码实现 总结 本文为原创文章&#xff0c;转载请注明出处&#xff1a; https://blog.csdn.net/qq_59075481/article/details/136110636 前言 Win 10/11 的虚拟桌面微软暂时没有开放接口&#xff0c;有很多信息对开发者是闭塞的&#xff0c;…

Qt QWidget以及各种控件、布局 核心属性(适合入门使用时查询)

目录 1. QWidget核心属性 2. 按钮类控件 2.1 PushButton 核心属性 2.2 RadioButton 核心属性 2.3 CheckBox 和 Tool Button 核心属性 3. 显示类控件 3.1 Label 核心属性 3.2 LCDNumber 核心属性 3.3 ProgressBar 核心属性 3.4 Calendar Widget 核心属性 4. 输入类控…