【初阶数据结构】——堆的引入和实现二叉树

简介: 【初阶数据结构】——堆的引入和实现二叉树

前言


上篇文章简单介绍树,讲解了最基本的二叉树,以及二叉树使用数组存储的顺序结构和使用链表存储的链式结构两种存储方式,今天就引入堆来实现二叉树。


一、二叉树的顺序结构及实现


1.1二叉树的顺序结构


普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而满二叉树和完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

e18e98311715483898d54c9ce63f6f9f.png


1.2堆的结构


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


堆的性质:


堆中某个节点的值总是不大于或不小于其父节点的值。

大堆:任何父亲大于等于孩子

小堆:任何父亲小于等于孩子

堆总是一棵完全二叉树。

8b9d61eb67c84e7fa35b9e167b5bb09e.png


二、堆的实现


2.1堆向上调整算法(堆的插入)


我们直到在数组中插入数据是在末尾插入,那么用堆来表示就是在有效数据下面做孩子或者父亲,依次插入数据和上面的父亲结点作比较,如果父亲大了就将父亲和孩子互换,一直换到度也就是第一个结点就形成小堆,反之则形成大堆。

240f66354c21403a9d66f4c2df75b8fc.png


2.2堆向下调整算法(堆的删除)


我么以上面建的小堆为例如果我们删除第一个元素,按照惯例将后面的元素前移,就会形成新的堆但是新的堆不一定是我们的大堆或者小堆上面的情况纯属巧合,通过观察我们可以发现去掉第一个元素形成的左右子树依然是小堆,我们不妨将第一个元素和最后一个元素互换位置,这样最小的元素就在最后,指针前移就可以做到删除,然后第一个位置的两个子树都是小堆再从两个小堆的堆顶选出最小的交换,重复操作又可以是一个小堆了。


a45b383e78c14bda832a2cb79085465e.png


2.3建堆的时间复杂度


因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

e696086bef314924be2f119c50a6f2db.png


2.4堆的创建


typedef int HPDatatype;
typedef struct Heap
{
  HPDatatype* a;
  int size;
  int capacity;
}HP;


还是使用动态开辟空间,来实现;


2.5堆的初始化和空间的销毁


void HPInit(HP* php)
{
  assert(php);
  php->a = NULL;
  php->size = php->capacity = 0;
}


和顺序表一样,将指针置空和将容量,size置零 。

//销毁空间
void HPDes(HP* php)
{
  assert(php);
  free(php->a);
  php->size = php->capacity = 0;
}

使用free库函数释放动态开辟的空间,最后将容量和size置零。


2.6堆的插入


void HPPush(HP* php, HPDatatype x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    HPDatatype* tmp = (HPDatatype*)realloc(php->a, sizeof(HPDatatype)*newcapacity);
    if (tmp == NULL)
    {
      perror("realloc failed");
      exit(-1);
    }
    php->a = tmp;
    php->capacity = newcapacity;
  }
  php->a[php->size] = x;
  php->size++;
  HPadjustUp(php->a, php->size-1);
}


和顺序表的形式差不多,进入函数判断空间是否足够,不够的话动态开辟新的空间,开辟不成功的话打印错误码并退出,成功的话插入有效数据,size++,然后使用向上调整函数调整成堆 。


向上调整函数


调整函数就是上面的堆的向上调整算法,运用父亲和孩子的下标关系调整。

void HPadjustUp(HPDatatype* a, int child)
{
  //找到父亲
  int parent = (child - 1) / 2;
  //根为0  当和根交换后child为0
  while (child > 0)
  {
    //当child小时和父亲交换 建成小堆
    //当child大时和父亲交换 建成大堆
    if (a[parent] > a[child])
    {
      swap(&a[parent], &a[child]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}

4440fc54447a43e89539228f2b75697d.png


交换函数


void swap(HPDatatype* x, HPDatatype* y)
{
  HPDatatype tmp = *x;
  *x = *y;
  *y = tmp;
}


取地址防止出函数创建的变量销毁,导致交换失败。


2.7堆的删除


void HPpop(HP* php)
{
  assert(php);
  assert(php->size);
  //先将头和尾交换  左右子树依然是完整的小/大堆,再从两个子堆中找出最大/小值;
  swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  HPadjustDown(php->a, php->size, 0);
}

进入函数先判断,如果size为0是会造成越界,再将头和尾交换,size减减,最后使用堆的向下调整算法调整成小堆。


向下调整函数


void HPadjustDown(HPDatatype* a, int n, int parent)
{
  //假设左孩子最小
  int child = parent * 2 + 1;
  while (child < n)
  {
    if (child + 1 < n && a[child + 1] < a[child])
    {
      //假设失败 右孩子小
      ++child;
    }
    else
    {
      if (a[child] < a[parent])
      {
        swap(&a[child], &a[parent]);
        parent = child;
        child = parent * 2 + 1;
      }
      else
      {
        break;
      }
    }
  }
}

a45b383e78c14bda832a2cb79085465e.png


在这个函数中我们使用了一个假设法我们也不知道子堆的堆顶那个数据最小,但是两者是连续的先假设一个然后进行判断 ;这里一定要注意child的取值范围(child<n),防止越界。


2.8堆的打印、取值、判空


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


因为我们创建的是数组,遍历整个数组就可以。

HPDatatype HPtop(HP* php)
{
  assert(php);
  assert(php->size > 0);
  return php->a[0];
}


这里要注意size要大于0,当size为0是代表空。

bool HPempty(HP* php)
{
  assert(php);
  return php->size == 0;
}


当size为0时代表空,0==0返回true。


三、完整代码


#define _CRT_SECURE_NO_WARNINGS 67
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int HPDatatype;
typedef struct Heap
{
  HPDatatype* a;
  int size;
  int capacity;
}HP;
//初始化
void HPInit(HP* php)
{
  assert(php);
  php->a = NULL;
  php->size = php->capacity = 0;
}
//销毁空间
void HPDes(HP* php)
{
  assert(php);
  free(php->a);
  php->size = php->capacity = 0;
}
void swap(HPDatatype* x, HPDatatype* y)
{
  HPDatatype tmp = *x;
  *x = *y;
  *y = tmp;
}
//
void HPadjustUp(HPDatatype* a, int child)
{
  //找到父亲
  int parent = (child - 1) / 2;
  //根为0  当和根交换后child为0
  while (child > 0)
  {
    //当child小时和父亲交换 建成小堆
    //当child大时和父亲交换 建成大堆
    if (a[parent] > a[child])
    {
      swap(&a[parent], &a[child]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
void HPPush(HP* php, HPDatatype x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    HPDatatype* tmp = (HPDatatype*)realloc(php->a, sizeof(HPDatatype)*newcapacity);
    if (tmp == NULL)
    {
      perror("realloc failed");
      exit(-1);
    }
    php->a = tmp;
    php->capacity = newcapacity;
  }
  php->a[php->size] = x;
  php->size++;
  HPadjustUp(php->a, php->size-1);
}
void HPPrint(HP* php)
{
  for (int i = 0; i < php->size; i++)
  {
    printf("%d ", php->a[i]);
  }
  printf("\n");
}
void HPadjustDown(HPDatatype* a, int n, int parent)
{
  //假设左孩子最小
  int child = parent * 2 + 1;
  while (child < n)
  {
    if (child + 1 < n && a[child + 1] < a[child])
    {
      //假设失败 右孩子小
      ++child;
    }
    else
    {
      if (a[child] < a[parent])
      {
        swap(&a[child], &a[parent]);
        parent = child;
        child = parent * 2 + 1;
      }
      else
      {
        break;
      }
    }
  }
}
void HPpop(HP* php)
{
  assert(php);
  assert(php->size);
  //先将头和尾交换  左右子树依然是完整的小/大堆,再从两个子堆中找出最大/小值;
  swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  HPadjustDown(php->a, php->size, 0);
}
HPDatatype HPtop(HP* php)
{
  assert(php);
  assert(php->size > 0);
  return php->a[0];
}
bool HPempty(HP* php)
{
  assert(php);
  return php->size == 0;
}
int main()
{
  HP hp;
  HPInit(&hp);
  int a[] = { 65,100,70,32,50,60 };
  for (int i = 0; i < sizeof(a) / sizeof(int); i++)
  {
    HPPush(&hp, a[i]);
  }
  HPPrint(&hp);
  while (!HPempty(&hp))
  {
    printf("%d ", HPtop(&hp));
    HPpop(&hp);
  }
  HPDes(&hp);
  return 0;
}


b62a6d688ada4d4da984550d4881f4bb.png

最后的执行结果我们可以发现利用堆的删除可以实现排序,这就是我们下篇文章的内容利用堆实现排序,敬请期待!!!

相关文章
|
1月前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
39 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
1月前
|
机器学习/深度学习 存储 算法
数据结构实验之二叉树实验基础
本实验旨在掌握二叉树的基本特性和遍历算法,包括先序、中序、后序的递归与非递归遍历方法。通过编程实践,加深对二叉树结构的理解,学习如何计算二叉树的深度、叶子节点数等属性。实验内容涉及创建二叉树、实现各种遍历算法及求解特定节点数量。
81 4
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
76 16
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
131 8
|
2月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
100 1
|
2月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
31 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
2月前
|
存储 算法 搜索推荐
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
这篇文章主要介绍了顺序存储二叉树和线索化二叉树的概念、特点、实现方式以及应用场景。
35 0
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
|
存储 算法
数据结构与算法学习十六:树的知识、二叉树、二叉树的遍历(前序、中序、后序、层次)、二叉树的查找(前序、中序、后序、层次)、二叉树的删除
这篇文章主要介绍了树和二叉树的基础知识,包括树的存储方式、二叉树的定义、遍历方法(前序、中序、后序、层次遍历),以及二叉树的查找和删除操作。
31 0
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
186 9