【数据结构】—堆详解(手把手带你用C语言实现)

简介: 【数据结构】—堆详解(手把手带你用C语言实现)

❤️什么是堆?

       堆是一种基于树结构的数据结构,它是一棵二叉树,具有以下两个特点:

1. 堆是一个完全二叉树,即除了最后一层,其他层都是满的,最后一层从左到右填满。

2. 堆中每个节点都满足堆的特性,即父节点的值要么等于或者大于(小于)子节点的值。

    堆的分类

       堆一般分为两类:大堆和小堆。大堆中,父节点的值大于或等于子节点的值,而小堆中,父节点的值小于或等于子节点的值。堆的主要应用是在排序和优先队列中。

        以下分别为两个堆(左大堆,右小堆):



💛堆的实现思路

       使用什么实现?

       逻辑结构如上, 然而这仅仅是我们想像出来的而已,而实际上的堆的物理结构是一个完全二叉树通常是用数组实现的。如下:

        对此,这就要引申出一个问题?我们该如何分辨父节点以及子节点呢?如下:

      怎么分辨父节点以及子节点?

      通常我们的数组下标为“0”处即为根节点,也就是说我们一定知道一个父节点!并且我们也有计算公式一个来计算父节点以及子节点先记住,后面实现有用!!!也就是说我们也可以通过公式从每一个位置计算父节点以及子节点!如下:

     

                       

                       

       总体实现思路

       先建立一个结构体,由于堆的结构实际上是一颗完全二叉树,因此我们的结构跟二叉树一样即可!接着,想想我们的堆需要实现的功能?构建、销毁、插入、删除、取堆顶的数据、取数据个数、判空。(⊙o⊙)…基本的就这些吧哈~                                                        


       接着按照   定义函数接口->实现各个函数功能->测试测试->收工(-_^) o(* ̄▽ ̄*)ブ      

💜堆的实现

       结构体以及接口的实现

typedef int HPDataType;
typedef struct Heap
{
  HPDataType* _a;
  int _size;
  int _capacity;
}Heap;
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

 💯堆的两种建堆方式(调整方法)究极无敌重要!!!

      在实现以上的接口之前,我们必须必须要知道堆的两种建堆方式!!!

      并且仅仅通过调整两种建堆方式的<和>符号我们就可以轻易控制大小堆,具体看代码注释!

       建堆有两种方式,分别是自底向上建堆以及自顶向下建堆。具体简介如下:


       1. 自底向上建堆:自底向上建堆是指按照原始数组顺序依次插入元素,然后对每个插入的元素执行向上调整的操作,使得堆的性质保持不变。这种方法需要对每个元素都进行调整操作,时间复杂度为 O(nlogn)。


       2. 自顶向下建堆:自顶向下建堆是指从堆顶开始,对每个节点执行向下调整操作,使得堆的性质保持不变。这种方法需要从根节点开始递归向下调整,时间复杂度为 O(n)。因此,自顶向下建堆的效率比自底向上建堆要高。

 以上两种建堆方式 实际上是基于两种调整方法,接下来将详细介绍:

      向上调整方法  

       堆的向上调整方法将新插入的节点从下往上逐层比较,如果当前节点比其父节点大(或小,根据是大根堆还是小根堆),则交换这两个节点。一直向上比较,直到不需要交换为止。这样可以保证堆的性质不变。

具体步骤如下:


       1.将新插入的节点插入到堆的最后一位。


       2.获取该节点的父节点的位置,比较该节点与其父节点的大小关系。


       .如果该节点比其父节点大(或小,根据是大根堆还是小根堆),则交换这两个节点。


       4.重复步骤2-3,直到不需要交换为止,堆的向上调整完成。


       堆的向上调整的时间复杂度为O(logn),其中n为堆的大小。


       一图让你了解~(以大堆为例)

       实现如下:

void swap(HPDataType* s1, HPDataType* s2)
{
  HPDataType temp = *s1;
  *s1 = *s2;
  *s2 = temp;
}
void Adjustup(HPDataType* a, int child)//向上调整
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] > a[parent])//建大堆,小堆则<
    {
      swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
 向下调整方法

       堆的向下调整方法是指将某个节点的值下放至其子节点中,以维护堆的性质的过程。

       假设当前节点为 i,其左子节点为 2i+1,右子节点为 2i+2,堆的大小为 n

      则向下调整的步骤如下:

  1. 从当前节点 i 开始,将其与其左右子节点中较小或较大的节点比较,找出其中最小或最大的节点 j。
  2. 如果节点 i 小于等于(或大于等于,取决于是最小堆还是最大堆)节点 j,则说明它已经满足堆的性质,调整结束;;否则,将节点 i 与节点 j 交换位置,并将当前节点 i 更新为 j。
  1. 重复执行步骤 1 和步骤 2,直到节点 i 没有子节点或已经满足堆的性质。

       一图让你了解~(以大堆为例)

        实现如下:

void swap(HPDataType* s1, HPDataType* s2)
{
  HPDataType temp = *s1;
  *s1 = *s2;
  *s2 = temp;
}
void Adjustdown(HPDataType* a, int n, int parent)//向下调整
{
  int child = parent * 2 + 1;
  while (child < n)
  {
    if (child + 1 < n && a[child + 1] > a[child])//找出两个孩子中较大的那个,此为大堆,如果要实现小堆则 改 >
    {
      ++child;
    }
    if (a[child] > a[parent])//此为大堆,如果要实现小堆则 改 >
    {
      swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}

    堆的构建

void HeapCreate(Heap* hp, HPDataType* a, int n)
{
  assert(hp);
  assert(a);
  hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
  if (!hp->_a)
  {
    perror("malloc fail");
    exit(-1);
  }
  hp->_capacity = hp->_size = n;
  //将a中的元素全部转移到堆中
  memcpy(hp->_a, a, sizeof(HPDataType) * n);
  //建堆
  for (int i = 1; i < n; i++)
  {
    Adjustup(hp->_a, i);//按向上调整,此建立大堆
  }
}

    本文的构建方法是通过传递一个数组以及传递一个数组大小来构建的,里面包括了堆结构体的初始化操作,基本的建堆方式则是通过向上调整方法建堆。


      堆的销毁

void HeapDestory(Heap* hp)
{
  assert(hp);
  free(hp->_a);
  hp->_a = NULL;
  hp->_capacity = hp->_size = 0;
}

 就正常的销毁操作?大家应该都懂(确信) (o°ω°o)


       堆的插入

void HeapPush(Heap* hp, HPDataType x)
{
  assert(hp);
  if (hp->_capacity == hp->_size)//扩容
  {
    int newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
    HPDataType* new = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity);
    if (!new)
    {
      perror("realloc fail");
      exit(-1);
    }
    hp->_a = new;
    hp->_capacity = newcapacity;
  }
  hp->_a[hp->_size++] = x;
  Adjustup(hp->_a, hp->_size - 1);
}

实现是对于堆的空间进行判断,不够则是扩容操作,当然也有初始化的意思,接着是通过向上调整的方式插入操作。


       ⭕️堆的删除 (较重要)

void HeapPop(Heap* hp)//先将最后一个数与堆顶交换,然后再让size--,再进行向下调整
{
  assert(hp);
  swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
  hp->_size--;
  Adjustdown(hp->_a, hp->_size, 0);
}

       进行删除操作,我们当然是删除堆顶啦,这个删除操作先将最后一个数与堆顶交换,然后再让size--,再进行向下调整。

一图让你了解~


       取堆顶的数据

HPDataType HeapTop(Heap* hp)//取堆顶
{
  assert(hp);
  assert(hp->_size > 0);
  return hp->_a[0];
}

堆的数据个数

int HeapSize(Heap* hp)//堆大小
{
  assert(hp);
  return hp->_size;
}

 堆的判空

int HeapEmpty(Heap* hp)//判堆空
{
  assert(hp);
  return hp->_size==0;
}

💚总体代码

       pile.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 01
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* _a;
  int _size;
  int _capacity;
}Heap;
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

    pile.c

#include"pile.h"
void swap(HPDataType* s1, HPDataType* s2)
{
  HPDataType temp = *s1;
  *s1 = *s2;
  *s2 = temp;
}
void Adjustup(HPDataType* a, int child)//向上调整
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] > a[parent])//建大堆,小堆则<
    {
      swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
void Adjustdown(HPDataType* a, int n, int parent)//向下调整
{
  int child = parent * 2 + 1;
  while (child < n)
  {
    if (child + 1 < n && a[child + 1] > a[child])//找出两个孩子中较大的那个,此为大堆,如果要实现小堆则 改 >
    {
      ++child;
    }
    if (a[child] > a[parent])//此为大堆,如果要实现小堆则 改 >
    {
      swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
  assert(hp);
  assert(a);
  hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
  if (!hp->_a)
  {
    perror("malloc fail");
    exit(-1);
  }
  hp->_capacity = hp->_size = n;
  //将a中的元素全部转移到堆中
  memcpy(hp->_a, a, sizeof(HPDataType) * n);
  //建堆
  for (int i = 1; i < n; i++)
  {
    Adjustup(hp->_a, i);//按向上调整,此建立大堆
  }
}
void HeapDestory(Heap* hp)
{
  assert(hp);
  free(hp->_a);
  hp->_a = NULL;
  hp->_capacity = hp->_size = 0;
}
void HeapPush(Heap* hp, HPDataType x)
{
  assert(hp);
  if (hp->_capacity == hp->_size)//扩容
  {
    int newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
    HPDataType* new = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity);
    if (!new)
    {
      perror("realloc fail");
      exit(-1);
    }
    hp->_a = new;
    hp->_capacity = newcapacity;
  }
  hp->_a[hp->_size++] = x;
  Adjustup(hp->_a, hp->_size - 1);
}
void HeapPop(Heap* hp)//先将最后一个数与堆顶交换,然后再让size--,再进行向下调整
{
  assert(hp);
  swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
  hp->_size--;
  Adjustdown(hp->_a, hp->_size, 0);
}
HPDataType HeapTop(Heap* hp)//取堆顶
{
  assert(hp);
  assert(hp->_size > 0);
  return hp->_a[0];
}
int HeapSize(Heap* hp)//堆大小
{
  assert(hp);
  return hp->_size;
}
int HeapEmpty(Heap* hp)//判堆空
{
  assert(hp);
  return hp->_size==0;
}

 test.c

#include"pile.h"
void test()
{
  Heap hp;
  int arr[] = { 1,6,2,3,4,7,5 };
  HeapCreate(&hp, arr, sizeof(arr) / sizeof(arr[0]));
  //HeapPush(&hp, 10);
  printf("%d\n", HeapSize(&hp));
  while (!HeapEmpty(&hp))
  {
    printf("%d %d \n", HeapTop(&hp), HeapSize(&hp));
    HeapPop(&hp);
  }
  printf("%d\n", HeapSize(&hp));
  HeapDestory(&hp);
  HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
  printf("\n");
}
int main()
{
  test();
  return 0;
}

测试结果:


                   感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!

相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
55 1
|
2月前
|
存储 算法 搜索推荐
【趣学C语言和数据结构100例】91-95
本文涵盖多个经典算法问题的C语言实现,包括堆排序、归并排序、从长整型变量中提取偶数位数、工人信息排序及无向图是否为树的判断。通过这些问题,读者可以深入了解排序算法、数据处理方法和图论基础知识,提升编程能力和算法理解。
56 4
|
2月前
|
存储 机器学习/深度学习 搜索推荐
【趣学C语言和数据结构100例】86-90
本文介绍并用C语言实现了五种经典排序算法:直接插入排序、折半插入排序、冒泡排序、快速排序和简单选择排序。每种算法都有其特点和适用场景,如直接插入排序适合小规模或基本有序的数据,快速排序则适用于大规模数据集,具有较高的效率。通过学习这些算法,读者可以加深对数据结构和算法设计的理解,提升解决实际问题的能力。
49 4
|
2月前
|
存储 算法 数据处理
【趣学C语言和数据结构100例】81-85
本文介绍了五个经典算法问题及其C语言实现,涵盖图论与树结构的基础知识。包括使用BFS求解单源最短路径、统计有向图中入度或出度为0的点数、统计无向无权图各顶点的度、折半查找及二叉排序树的查找。这些算法不仅理论意义重大,且在实际应用中极为广泛,有助于提升编程能力和数据结构理解。
54 4
|
2月前
|
算法 数据可视化 数据建模
【趣学C语言和数据结构100例】76-80
本文介绍了五种图论算法的C语言实现,涵盖二叉树的层次遍历及广度优先搜索(BFS)和深度优先搜索(DFS)的邻接表与邻接矩阵实现。层次遍历使用队列按层访问二叉树节点;BFS利用队列从源节点逐层遍历图节点,适用于最短路径等问题;DFS通过递归或栈深入图的分支,适合拓扑排序等场景。这些算法是数据结构和算法学习的基础,对提升编程能力和解决实际问题至关重要。
54 4
|
2月前
|
存储 算法 vr&ar
【趣学C语言和数据结构100例】71-75
本文介绍了五个C语言数据结构问题及其实现,涵盖链表与二叉树操作,包括按奇偶分解链表、交换二叉树左右子树、查找节点的双亲节点、计算二叉树深度及求最大关键值。通过递归和遍历等方法,解决了理论与实际应用中的常见问题,有助于提升编程能力和数据结构理解。
47 4
|
2月前
|
存储 算法 C语言
【趣学C语言和数据结构100例】66-70
本书《趣学C语言和数据结构100例》精选了5个典型的数据结构问题及C语言实现,涵盖链表与数组操作,如有序集合的集合运算、有序序列表的合并、数组中两顺序表位置互换、三递增序列公共元素查找及奇偶数重排。通过详细解析与代码示例,帮助读者深入理解数据结构与算法设计的核心思想,提升编程技能。
37 4
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
74 5
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
67 1
|
2月前
|
存储 机器学习/深度学习 算法
【趣学C语言和数据结构100例】61-65
本文介绍了五个关于C语言和数据结构的经典问题及其实现方法,涵盖查找链表共同后缀、删除重复节点、重新排列链表元素、合并有序链表以及特定条件下的链表排序。每个问题通过具体的算法设计,不仅展示了链表操作的灵活性,还强调了效率优化的重要性。通过这些问题的探讨,读者可以深入理解链表的基本操作和高级应用,提升解决实际问题的能力。
59 4