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

2024-02-14 19:20

本文主要是介绍“从根到叶:深入理解堆数据结构“,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一.堆的概念及实现

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;  // 默认容量为10size = 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> {@Overridepublic 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;}@Overridepublic 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 是最小的@Overridepublic 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 是最小的@Overridepublic 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置为nullpublic 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 compatibilityif (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);elsesiftUpComparable(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> {@Overridepublic int compare(Integer o1, Integer o2) {return o1 - o2;}
}
//使用比较器创建大根堆
class GreaterIntComp implements Comparator<Integer> {@Overridepublic 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.chinasem.cn/article/709373

相关文章

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-

深入理解RxJava:响应式编程的现代方式

在当今的软件开发世界中,异步编程和事件驱动的架构变得越来越重要。RxJava,作为响应式编程(Reactive Programming)的一个流行库,为Java和Android开发者提供了一种强大的方式来处理异步任务和事件流。本文将深入探讨RxJava的核心概念、优势以及如何在实际项目中应用它。 文章目录 💯 什么是RxJava?💯 响应式编程的优势💯 RxJava的核心概念

如何通俗理解注意力机制?

1、注意力机制(Attention Mechanism)是机器学习和深度学习中一种模拟人类注意力的方法,用于提高模型在处理大量信息时的效率和效果。通俗地理解,它就像是在一堆信息中找到最重要的部分,把注意力集中在这些关键点上,从而更好地完成任务。以下是几个简单的比喻来帮助理解注意力机制: 2、寻找重点:想象一下,你在阅读一篇文章的时候,有些段落特别重要,你会特别注意这些段落,反复阅读,而对其他部分