【开卷数据结构 】还不会实现堆吗?图文并茂深入理解堆

简介: 【开卷数据结构 】还不会实现堆吗?图文并茂深入理解堆

🌺最大堆与最小堆


🍁最大堆与最小堆的定义


Q:什么是最大堆


A:最大堆是指在树中,如果一个结点有儿子结点,其关键字值都不小于其儿子节点的关键字值。最大堆是一棵完全而二叉树,也是一颗最大树。


Q:什么是最小堆


A:最小堆是指在树中,如果一个结点有儿子结点,其关键字值都不大于其儿子节点的关键字值。最小堆是一棵完全而二叉树,也是一颗最小树。


ab30abaf865723cb8b8865c5fa1a44c0_46ff3d0ebb6c43568e7c813a9154be56.png


注意:最小树的根结点的关键字是树中元素最小的,而最大树的根结点是树中关键字值最大的。


可以发现,最大堆和最小堆十分相似,下文中我们就只研究最大堆。


当我们把最大堆看成抽象数据型时,它非常的简单,仅有以下3个操作:


创建空堆

把新元素插入堆中

从队中删除最大元素


🌺最大堆的操作


🍁最大堆的创建


由于堆是一个完全二叉树,可以使用数组存储来表示。我们可以这样来创建最大堆


💬 代码演示


#define MAX_ELEMENTS 200                        //最大堆大小+1
#define HEAP_FULL(n) (n==MAX_ELEMENTS-1)
#define HEAP_EMPTY(n) (!n)
typedef struct{
  int key;
}element;
element heap[MAX_ELEMENTS];
int n=0;


🍁最大堆的插入


🔺算法分析


最大堆的插入操作可以简单看成是“结点上浮”。当我们在向最大堆中插入一个结点时,我们必须满足完全二叉树的标准,那么被插入结点的位置的是固定的。而且要满足父结点关键字值不小于子结点关键字值,那么我们就需要去移动父结点和子结点的相互位置关系。


可以参考下面的图示进行理解

889079a0935e0c4841dd1c9949b8c165_2614b920d43444df81d5197662734403.png

d1065b450720c79cb83b115a86bc213f_20ac0799cd4f43afa796450ea4dd16c8.png

0bca4df4593a8b02cd0f1c05f4a156bd_7d5d37bfa36641728782840c1bc70ae4.png

4a291f77432b6ae9687f33cdee14c4d4_ba42cd5c03154c9dae48feeec5dd9551.png


💬 代码演示


1)首先检查堆是否满,如果不满,设 i 等于新堆的大小(n+1)。


2)使用 while 循环从最大堆的新叶子结点开始,沿着根结点的路径走。


3)一直到根结点或者位置 i ,使其父结点 i/2 的值不小于要插入的值。


void insert_max_heap(element item ,int *n){
  //将数插入当前大小为 n 的最大堆中
    if(HEAP_FULL(*n)){
      return;
    }
    int i = ++(*n);
    while((i != 1) && (item.key>heap[i/2].key)){
      heap[i] = heap[i/2];
      i/=2;
    }
    heap[i]=item;
}


🍁最大堆的删除


🔺算法分析


最大堆的删除操作,总是从堆的根结点删除元素。同样根元素被删除之后为了能够保证该树还是一个完全二叉树,我们需要来移动完全二叉树的最后一个结点,让其继续符合完全二叉树的定义,从这里可以看作是最大堆最后一个结点的下沉。


可以参考下面的图示进行理解

fb8510bbcf6a4b0a4c7f781bbd3468c9_bbe5fd0b84054ca596e64cbbaebd3d78.png

188ababe4c547ee1deaeb264424996f0_3c5d86d4b4db447098e80fe3059a5c9b.png


9e5614c4ed957acbd6cf95d5e7a64c68_c822645ab5a34c61a10f8d1d3f8f6901.png




现在看来该二叉树虽然是一个完全二叉树,但是它并不符合最大堆的相关定义。我们要在删除完成之后,该完全二叉树依然是最大堆。因此就需要我们来做一些相关的操作。

64d2cfe5666d774425b76bec86fb4a40_6cb581bbceef4218976bc0ad4adbf254.png


43b66d9b35dd81b26dbf01d9a450b958_f6148bf1110f4e95907afa54f939b810.png



💬 代码演示


1)首先删除最大堆的根结点

2)根结点被删除之后,为了能够保证该树还是一个完全二叉树,我们应该移动完全二叉树的最后一个结点,让其继续符合完全二叉树的定义。这里可以看作是最大堆最后一个结点的下沉

3)该二叉树虽然是一个完全二叉树,但是它并不符合最大堆的相关定义,我们在左右结点中选择大的一方和下沉的结点比较,令大的一方上浮。

4)重复步骤 3 ,直到到达叶子结点。

element delete_max_heap(int *n){
  int parent, child;
  element temp, item;
  temp = heap[--*n];
  item = heap[1];
  parent = 1,child=2;
  for(;child <= *n; child = child * 2)
  {
  if( (child < *n) && heap[child].key < heap[child+1].key)
  {// 这一步是为了看当前结点是左子结点大还是右子结点大,然后选择较大的那个子结点
          child++;
      }
      if(temp.key >= heap[child].key)
  {
      break;
        }
      heap[parent] = heap[child];//这就是上图中第二步和第三步中黄色部分操作
      arent = child;// 这其实就是一个递归操作,让parent指向当前子树的根结点
   }
  heap[parent] = temp;
  return item;
}


🌺堆排序


🔺算法分析


通过上文介绍的最大堆的插入算法和删除算法可以直接得到一个时间复杂度为 O(nlogn) 的排序算法。


1)将n个数插入到一个初始为空的堆中,构建出最大堆。

2)整个序列的最大值就是堆顶的根结点。

3)将其与末尾元素进行交换,此时末尾就为最大值

4)将剩余元素重新构造成一个最大堆,这样会得到剩余元素的次小值。

5)如此反复执行,便能得到一个有序序列了。

可以参考下面的图示进行理解


90ee9d2a08ce8fa5078f06c33d44fac7_09f81a546475479ca481b95cfe66c57c.png


根据二叉树的结构与性质,我们可以推理出来最后一个非叶子节点的索引就是 【arr.len / 2 -1】,我们从最后一个非叶子结点开始,从左至右,从下至上进行调整。


这里对非叶子结点进行判断,交换【4,5】


447c9b45facca5d5712ec5138884c03b_962d6312fabb45568ceac9f35138644a.png


对下一个非叶子结点进行判断,交换【5,2】


32b0d98c68c311cd33ecbb28a3651d09_1d5bbcf127c447cdbb3dac6f4fae6dcf.png


交换导致了子根【2,3,4】结构混乱,进行调整,交换【2,4】


bce52060e680b185d4f0e81dedbca5ee_c7ac609191b14651b3817d974b85569c.png


我们构造出来一个最大堆,下面来进行排序,先将顶点元素5与末尾元素2交换位置,此时末尾数字为最大值。


409ca84b1c552b25dcba6a659d6300e5_59904570d2e040dfaffae2b91f471890.png


排除已经确定的最大元素,将剩下元素重新构建最大堆


9b3db24f0f9c9c8803a8fe03e6ccf204_f351c2abcc584577bc6fd348fadaaaa0.png


再将堆顶元素4与末尾元素2进行交换,再重新构建最大堆



d896b07518e4713d79901d01bfc918b6_4830c664f6c84c2795930f767a7a33f1.png


重复上述操作,得到最终排序结果


8cc2613aa7c970eacaa3e0243c68a63d_d653f112b72146f18331fe4a98184916.png


💬 代码演示


void heapSort(int[] arr) {
    //构造最大堆
    heapInsert(arr);
    int size = arr.length;
    while (size > 1) {
        //固定最大值
        swap(arr, 0, size - 1);
        size--;
        //构造大根堆
        heapify(arr, 0, size);
    }
}
//构造最大堆(通过新插入的数上升)
void heapInsert(int[] arr) {
    for (int i = 0; i < arr.length; i++) {
        //当前插入的索引   
  int currentIndex = i;
        //父结点索引
        int fatherIndex = (currentIndex - 1) / 2;
        //如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
        //然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
        while (arr[currentIndex] > arr[fatherIndex]) {
            //交换当前结点与父结点的值
            swap(arr, currentIndex, fatherIndex);
            //将当前索引指向父索引
            currentIndex = fatherIndex;
            //重新计算当前索引的父索引
            fatherIndex = (currentIndex - 1) / 2;
        }
    }
}
//将剩余的数构造成最大堆(通过顶端的数下降)
void heapify(int[] arr, int index, int size) {
    int left = 2 * index + 1;
    int right = 2 * index + 2;
    while (left < size) {
        int largestIndex;
        //判断孩子中较大的值的索引(要确保右孩子在size范围之内)
        if (arr[left] < arr[right] && right < size) 
  {
            largestIndex = right;
        } 
  else 
  {
            largestIndex = left;
        }
        //比较父结点的值与孩子中较大的值,并确定最大值的索引
        if (arr[index] > arr[largestIndex]) 
  {
            largestIndex = index;
        }
        //如果父结点索引是最大值的索引,那已经是最大堆了,则退出循环
        if (index == largestIndex)
   {
            break;
        }
        //父结点不是最大值,与孩子中较大的值交换
        swap(arr, largestIndex, index);
        index = largestIndex;
        left = 2 * index + 1;
        right = 2 * index + 2;
    }
}
    //交换数组中两个元素的值
void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

相关文章
|
1月前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
39 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
79 16
|
2月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
100 1
|
3月前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
69 5
【数据结构】优先级队列(堆)从实现到应用详解
|
2月前
|
存储 算法 调度
数据结构--二叉树的顺序实现(堆实现)
数据结构--二叉树的顺序实现(堆实现)
|
2月前
|
存储 算法 分布式数据库
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
37 0
|
2月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
2月前
【数据结构】大根堆和小根堆
【数据结构】大根堆和小根堆
49 0