【数据结构】二叉树 -- 堆

简介: 【数据结构】二叉树 -- 堆

一、堆的概念及结构

如果有一个关键码的集合 K = {k0 , k1 , k2 , … , kn-1} ,把它的所有元素按完全二叉树的顺序存储方式存储在一 个一维数组中 ,并满足: Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=K2i+2) i = 0 , 1 , 2… ,则称为小堆 ( 或大堆) 。(即双亲比孩子的数值小(大)——小(大)堆)将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆只有两种,即大堆和小堆,大堆就是父亲结点数据大于儿子结点数据,小堆则反之。

堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树

2020062310470442.png

二、堆的实现

1、结构的定义

由于是堆的元素按完全二叉树的顺序存储方式存储在一位数组中的,所以堆的结构和顺序表的结构一样。

//符号和结构的声明
#define DEF_SIZE 5
#define CRE_SIZE 2
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* data;
  int size;
  int capacity;
}HP;

2、堆的初识化

堆的初识化和顺序表的初始化一样。

//堆的初始化
void HeapInit(HP* php)
{
  assert(php);
  php->data = (HPDataType*)malloc(sizeof(HPDataType) * DEF_SIZE);
  if (php->data == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  php->size = 0;
  php->capacity = DEF_SIZE;
}

3、堆的插入

堆的插入有两个需要注意的地方:

1、由于堆只会在尾部插入元素,所以我们不需要将 CheckCapacity 单独封装一个函数;

2、由于堆要求在插入元素之后仍保持堆的结构,即保持小根堆/大根堆,所以我们需要对堆进行向上调整,向上调整的过程其实也就是建堆的过程。

//堆的插入--需要保证插入之后仍然保持堆的结构
void HeapPush(HP* php, HPDataType x)
{
  assert(php);
  //检查容量
  if (php->size == php->capacity)
  {
    HPDataType* ptr = (HPDataType*)realloc(php->data, sizeof(HPDataType) * php->capacity * CRE_SIZE);
    if (ptr == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    php->data = ptr;
    php->capacity *= CRE_SIZE;
  }
  //插入元素
  php->data[php->size] = x;
  php->size++;
  //保持堆结构--向上调整
  AdjustUp(php->data, php->size - 1);
}

4、堆的向上调整

这里我们以小根堆为例,如图:假设现在我们已经有了一个小根堆,现在我们往堆中 (堆尾) 插入一个元素,那么可能会出现两种情况:

2020062310470442.png

1、插入的元素大于父节点,此时我们的堆仍保持小根堆结构,所以不需要改动;比如我们往堆中插入30;

2020062310470442.png

2、插入的元素小于父节点;这种情况又可以分为两种:一是插入的节点虽然小于父节点,但是大于父节点的父节点,这种情况我们只需要交换父节点和该节点,使得堆保存小根堆的结构即可,比如我们插入20;二是该节点不仅小于父节点,还小于父节点的父节点,这种情况下我们就需要把该节点不断往上调整,直到把堆调整为小根堆,最坏的情况是该节点被调整为根节点,比如我们插入10;2020062310470442.png

2020062310470442.png

//交换两个节点
void Swap(HPDataType* p1, HPDataType* p2)
{
  assert(p1 && p2);
  HPDataType tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
//向上调整--小根堆
void AdjustUp(HPDataType* data, int child)
{
  assert(data);
  int parent = (child - 1) / 2;  //找到父节点
  while (child > 0)  //当调整到根节点时不再继续调整
  {
    //当子节点小于父节点时交换
    if (data[child] < data[parent])
    {
      Swap(&data[child], &data[parent]);
      //迭代
      child = parent;
      parent = (child - 1) / 2;
    }
    //否则直接跳出循环
    else
    {
      break;
    }
  }
}

如果我们需要建大根堆,只需要把交换的条件修改一下即可。

//当子节点大于父节点时交换
if (data[child] > data[parent])

5、堆的删除

对于堆的删除有明确的规定:我们只能删除堆顶的元素;但是头删之后存在两个问题:

1、顺序表头删需要挪动数据,效率低下;

2、头删之后堆中各节点的父子关系完全被破坏;

对于上面的这些问题,我们有如下解决办法:

1、我们在删除之前先将堆顶和堆尾的元素交换,然后让size–,这样相当于删除了堆顶的元素,且效率达到了O(1);

2、由于我们把堆尾元素交换到了堆顶,堆的结构遭到了破坏,所以设计一个向下调整算法来让保持堆的结构;

//删除堆顶的元素--需要保证删除之后仍然保持堆的结构
void HeapPop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  //首先交换堆顶和堆尾的元素
  Swap(&php->data[0], &php->data[php->size - 1]);
  //删除堆尾的元素
  php->size--;
  //保持堆结构--向下调整
  AdjustDown(php->data, php->size, 0);
}

6、堆的向下调整

堆向下调整的思路和向上调整刚好相反 (我们还是以小根堆为例):1、找出子节点中较小的节点;2、比较父节点与子节点,如果父节点大于子节点则交换两个节点;3、交换之后,原来的子节点成为新的父节点,然后继续 1 2 步骤,直到调整为堆的结构

2020062310470442.png

//向下调整
void AdjustDown(HPDataType* data, int n, int parent)
{
  assert(data);
  int minchild = parent * 2 + 1;
  //当子节点调整到堆尾时结束循环
  while (minchild < n)
  {
    //找出较小的子节点
    if (minchild + 1 < n && data[minchild + 1] < data[minchild])
    {
      minchild += 1;
    }
    //如果父节点大于较小的子节点就交换
    if (data[parent] > data[minchild])
    {
      Swap(&data[parent], &data[minchild]);
      //迭代
      parent = minchild;
      minchild = parent * 2 + 1;
    }
    //否则直接跳出循环
    else
    {
      break;
    }
  }
}

和向上调整类似,如果我们想要调整为大堆,也只需要改变交换条件:

//找出较大的子节点
if (minchild + 1 < n && data[minchild + 1] > data[minchild])
//如果父节点小于较小的子节点就交换
if (data[parent] < data[minchild])

7、取出堆顶的元素

//取堆顶的元素
HPDataType HeapTop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  return php->data[0];
}

8、返回堆的元素个数

//堆的元素个数
int HeapSize(HP* php)
{
  assert(php);
  return php->size;
}

9、判断堆是否为空

//堆的判空
bool HeapEmpty(HP* php)
{
  assert(php);
  return php->size == 0;
}

10、打印堆中的数据

//打印堆中的数据
void HeapPrint(HP* php)
{
  assert(php);
  int i = 0;
  for (i = 0; i < php->size; i++)
  {
    printf("%d ", php->data[i]);
  }
  printf("\n");
}

11、堆的销毁

//堆的销毁
void HeapDestory(HP* php)
{
  assert(php);
  free(php->data);
  php->capacity = php->size = 0;
}

三、完整代码

1、Heap.h

#pragma once
//头文件的包含
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//符号和结构的声明
#define DEF_SIZE 5
#define CRE_SIZE 2
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* data;
  int size;
  int capacity;
}HP;
//函数的声明
//堆的初始化
void HeapInit(HP* php);
//堆的销毁
void HeapDestory(HP* php);
//堆的插入
void HeapPush(HP* php, HPDataType x);
//删除堆顶的元素
void HeapPop(HP* php);
//取堆顶的元素
HPDataType HeapTop(HP* php);
//堆的元素个数
int HeapSize(HP* php);
//堆的判空
bool HeapEmpty(HP* php);
//打印堆中的数据
void HeapPrint(HP* php);

2、Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//堆的初始化
void HeapInit(HP* php)
{
  assert(php);
  php->data = (HPDataType*)malloc(sizeof(HPDataType) * DEF_SIZE);
  if (php->data == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  php->size = 0;
  php->capacity = DEF_SIZE;
}
//堆的销毁
void HeapDestory(HP* php)
{
  assert(php);
  free(php->data);
  php->capacity = php->size = 0;
}
//交换两个节点
void Swap(HPDataType* p1, HPDataType* p2)
{
  assert(p1 && p2);
  HPDataType tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
//向上调整--小根堆
void AdjustUp(HPDataType* data, int child)
{
  assert(data);
  int parent = (child - 1) / 2;  //找到父节点
  while (child > 0)  //当子节点为根节点时循环结束
  {
    //当子节点小于父节点时交换
    if (data[child] < data[parent])
    {
      Swap(&data[child], &data[parent]);
      //迭代
      child = parent;
      parent = (child - 1) / 2;
    }
    //否则直接跳出循环
    else
    {
      break;
    }
  }
}
//堆的插入--需要保证插入之后仍然保持堆的结构
void HeapPush(HP* php, HPDataType x)
{
  assert(php);
  //检查容量
  if (php->size == php->capacity)
  {
    HPDataType* ptr = (HPDataType*)realloc(php->data, sizeof(HPDataType) * php->capacity * CRE_SIZE);
    if (ptr == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    php->data = ptr;
    php->capacity *= CRE_SIZE;
  }
  //插入元素
  php->data[php->size] = x;
  php->size++;
  //保持堆结构--向上调整
  AdjustUp(php->data, php->size - 1);
}
//向下调整
void AdjustDown(HPDataType* data, int n, int parent)
{
  assert(data);
  int minchild = parent * 2 + 1;
  //当子节点调整到堆尾时结束循环
  while (minchild < n)
  {
    //找出较小的子节点
    if (minchild + 1 < n && data[minchild + 1] < data[minchild])
    {
      minchild += 1;
    }
    //如果父节点大于较小的子节点就交换
    if (data[parent] > data[minchild])
    {
      Swap(&data[parent], &data[minchild]);
      //迭代
      parent = minchild;
      minchild = parent * 2 + 1;
    }
    //否则直接跳出循环
    else
    {
      break;
    }
  }
}
//删除堆顶的元素--需要保证删除之后仍然保持堆的结构
void HeapPop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  //首先交换堆顶和堆尾的元素
  Swap(&php->data[0], &php->data[php->size - 1]);
  //删除堆尾的元素
  php->size--;
  //保存堆结构--向下调整
  AdjustDown(php->data, php->size, 0);
}
//取堆顶的元素
HPDataType HeapTop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  return php->data[0];
}
//堆的元素个数
int HeapSize(HP* php)
{
  assert(php);
  return php->size;
}
//堆的判空
bool HeapEmpty(HP* php)
{
  assert(php);
  return php->size == 0;
}
//打印堆中的数据
void HeapPrint(HP* php)
{
  assert(php);
  int i = 0;
  for (i = 0; i < php->size; i++)
  {
    printf("%d ", php->data[i]);
  }
  printf("\n");
}

3、test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
int main()
{
  int a[] = { 27,15,19,18,28,34,65,49,25,37 };
  HP hp;
  //堆的初始化
  HeapInit(&hp);
  //插入元素
  int i = 0;
  int len = sizeof(a) / sizeof(a[0]);
  for (i = 0; i < len; i++)
  {
    HeapPush(&hp, a[i]);
  }
  HeapPrint(&hp);
  //删除堆顶元素
  HeapPop(&hp);
  HeapPrint(&hp);
  //取出堆顶元素
  HPDataType top = HeapTop(&hp);
  printf("%d\n", top);
  //堆排序
  for (i = 0; i < len - 1; i++)
  {
    printf("%d ", HeapTop(&hp));
    HeapPop(&hp);
  }
  //堆的销毁
  HeapDestory(&hp);
  return 0;
}

2020062310470442.png

大家也可以去我的 Gitee 仓库中获取完整代码:Heap/Heap · 野猪佩奇/日常学习 - 码云 - 开源中国 (gitee.com)






相关文章
|
3天前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
18 5
【数据结构】优先级队列(堆)从实现到应用详解
|
14天前
|
存储 机器学习/深度学习
【数据结构】二叉树全攻略,从实现到应用详解
本文介绍了树形结构及其重要类型——二叉树。树由若干节点组成,具有层次关系。二叉树每个节点最多有两个子树,分为左子树和右子树。文中详细描述了二叉树的不同类型,如完全二叉树、满二叉树、平衡二叉树及搜索二叉树,并阐述了二叉树的基本性质与存储方式。此外,还介绍了二叉树的实现方法,包括节点定义、遍历方式(前序、中序、后序、层序遍历),并提供了多个示例代码,帮助理解二叉树的基本操作。
38 13
【数据结构】二叉树全攻略,从实现到应用详解
|
11天前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
11天前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。
|
11天前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。
|
19天前
|
存储 程序员 C语言
堆和栈之间有什么区别
【9月更文挑战第1天】堆和栈之间有什么区别
89 0
|
1月前
|
算法 机器人
【数据结构】什么是堆
【数据结构】什么是堆
31 0
|
1月前
|
算法
【初阶数据结构篇】二叉树算法题
二叉树是否对称,即左右子树是否对称.
|
1月前
|
存储
【初阶数据结构篇】实现链式结构二叉树(二叉链)下篇
要改变root指针的指向,将本来指向根节点的root指针改为空,所以传二级指针(一级指针也可以,只不过在调用完记得把root置为空)。
|
1月前
|
存储 测试技术
【初阶数据结构篇】实现链式结构二叉树(二叉链)上篇
先构建根结点,再对左右子树构建,每次需要时申请一个结点空间即可,否则返回空指针。