首页 > 其他分享 >一文掌握堆(Heap)全貌:原理深度解析、动态演示核心操作与实际应用场景

一文掌握堆(Heap)全貌:原理深度解析、动态演示核心操作与实际应用场景

时间:2024-03-31 18:00:34浏览次数:434  
标签:index 演示 全貌 largerChild int 建堆 heap 节点 Heap

参考动画:从堆的定义到优先队列、堆排序

建议配合动画食用

为什么叫堆呢?

“堆”这个词在数据结构的上下文中通常指的是一种特定的树形数据结构,其命名来源于它的特性和应用。在这种结构中,父节点和子节点之间存在特定的排序关系,这类似于物理世界中堆积的物体——较大或较重的物体(在堆排序中对应于较大或较小的值)位于顶部,而较小或较轻的物体则位于下方。

1、堆必须是完全二叉树

完全二叉树是一种特殊的二叉树,其中每个层级(除了可能的最后一个层级)都是完全填满的,并且最后一个层级的所有节点都尽可能地向左排列。这种结构使得堆可以用数组有效地表示,无需使用指针,节省空间。

由此可以推出:对于堆的父节点i,其子节点为2*I+1和2*i+2

2、堆序性

堆序性是指在堆中每个节点都必须满足特定的顺序关系。对于最大堆,任一节点的值都大于或等于其子节点的值。对于最小堆,则任一节点的值都小于或等于其子节点的值。
在这里插入图片描述

堆的基本操作

上滤和下滤(上浮与下沉)

  • 上滤(上浮): 在添加新元素或调整堆时使用,新元素添加到堆的末尾,然后与其父节点比较,如果不满足堆的性质,则与父节点交换,这一过程持续进行,直至恢复堆的性质。
  • 下滤(下沉): 在移除堆顶元素或调整堆时使用,将末尾元素移动到堆顶,然后与其子节点比较,如果不满足堆的性质,则与子节点中符合条件的那个交换,这一过程持续进行,直至恢复堆的性质。

代码:以大根堆为例
上浮操作

void shifup(vector<int>& heap,int index){
     while(index>0){
         int parent=(index-1)/2;
         if(heap[index]<=heap[parent]) break;//对于大根堆
         else swap(heap[index],heap[parent]);
         index=parent;
     }
}



下沉操作

void shifDown(vector<int>& heap,int index,int heapSize){
   int leftChild,rightChild,largerChild;
   while((leftChild=2*index+1)<heapSize){//循环时判断孩子节点是否在堆的范围内
        rightChild=leftChild+1;
        largerChild=leftChild;
        if(rightChild<heapSize&&heap[rightChild]>heap[leftChild]){//判断左孩子还是右孩子大    
        largerChild=rightChild;
        }
        if(heap[index]>=heap[largerChild]) break;//大根堆
        swap(heap[index],heap[larferChild]);
        index=largerChild;
   }
}

建堆

对于一个乱序的数组,如何建堆呢:
顶上滤顺序遍历,下下沉倒序遍历

自顶向下

方法:(以大根堆为例)
1、插入堆;
2、上滤
代码:

void siftUp(vector<int>& heap, int index) {
    while (index > 0) {
        int parent = (index - 1) / 2;
        if (heap[index] <= heap[parent]) break;
        swap(heap[index], heap[parent]);
        index = parent;
    }
}

void buildHeapTopDown(vector<int>& nums) {
    for (int i = 0; i < nums.size(); i++) {
        siftUp(nums, i);
    }
}

自下而上

1、先将所有元素建堆;
2、然后对每个父节点进行下滤操作;
3、先从第二层开始,最后在对根结点

为什么从 (n/2) - 1 开始: 在堆中,一个节点的子节点是 2*i + 12*i + 2i 是父节点的索引)。因此,对于数组中的最后一个元素 n - 1,其父节点的索引是 (n - 1 - 1) / 2 = (n/2) - 1

void siftDown(vector<int>& heap, int index, int heapSize) {
    int leftChild, rightChild, largerChild;
    while ((leftChild = 2 * index + 1) < heapSize) {
        rightChild = leftChild + 1;
        largerChild = leftChild;
        if (rightChild < heapSize && heap[rightChild] > heap[leftChild]) {
            largerChild = rightChild;
        }
        if (heap[index] >= heap[largerChild]) break;
        swap(heap[index], heap[largerChild]);
        index = largerChild;
    }
}

//这里是直接有一个完整的数组了
void buildHeapBottomUp(vector<int>& nums) {
    int n = nums.size();
    for (int i = (n - 2) / 2; i >= 0; i--) {
        siftDown(nums, i, n);
    }
}


总结

自下而上建堆方法的优势在于它利用了堆的完全二叉树特性,从底层开始逐步构造子堆,确保了每个子树自身都已经是一个合法的堆,然后再逐步合并这些小堆。这样,在处理父节点时,其子节点已经是有序的,只需要做少量必要的调整即可。因此,相比于自顶向下插入每个元素再逐一上滤的方法,自下而上的建堆方法更加高效。

简而言之:

自顶向下建堆

  • 适用场景: 动态插入场景,如逐步加入新元素。
  • 操作: 逐个将元素插入堆中,并通过上滤操作维护堆的性质。
  • 时间复杂度: 大约为 O(nlogn),其中 n 是数组元素的数量。这是因为每次插入操作的时间复杂度为 O(logn),并且需要进行 n 次插入。

自下而上建堆

  • 适用场景: 静态数组,当已有一个完整的数组并希望将其转化为堆。
  • 操作: 从最后一个非叶子节点开始,逐次对每个节点执行下滤操作,将整个数组转化为堆。
  • 时间复杂度: O(n)。这种方法比自顶向下建堆更高效,因为它更好地利用了已有的堆性质,尤其是在树的较低层。

应用

优先队列

C++优先队列探秘:原理揭示与最小堆自定义实现指南

堆排序

堆排序算法中的关键决策:大根堆与小根堆的选择及其权衡考量

标签:index,演示,全貌,largerChild,int,建堆,heap,节点,Heap
From: https://blog.csdn.net/weixin_67247533/article/details/137176064

相关文章