【数据结构初阶】第七篇——二叉树的顺序结构及实现(堆的向下,向上调整算法)

简介: 【数据结构初阶】第七篇——二叉树的顺序结构及实现(堆的向下,向上调整算法)

二叉树的顺序结构


普通二叉树是不适合用数组来存储的,因为可能会导致大量的空间浪费。而完全二叉树更适合使用顺序结构存储。

image.png

堆的概念及结构


堆的概念

堆:如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足ki<=k2i+1且ki<=k2i+2(或满足ki>=k2i+1且ki>=k2i+2),其中i=0,1,2...,则称该集合为堆。

小堆:将根节点最小的堆叫做小堆,也叫作最小堆或小根堆。

大堆:将根节点最大的堆叫做大堆,也叫最大堆或大跟堆。

堆的性质:

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

堆总是一颗完全二叉树。

堆的结构

image.png

堆的向下调整算法


现在我们给出一个数组,逻辑上可以看作是一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。

image.png

但是,使用向下调整算法需要满足一个前提:


   若将其调整成小堆,那么根节点的左右子树必须都为小堆。


   若将其调整成大堆,那么根结点的左右子树必须都为大堆。


向下调整算法的基本思想(这里以小堆为例):


1.从根节点处开始,选出左右孩子中值较小的孩子。


2.让小的孩子与其父亲进行比较。


若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,知道调整到叶子节点为止。


若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。

//交换函数
void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
//堆的向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{
  //child记录左右孩子中值较小的孩子的下标
  int child = 2 * parent + 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 = 2 * parent + 1;
    }
    else//已成堆
    {
      break;
    }
  }
}

 使用堆的向下调整算法,最坏的情况下就是一直交换,需要循环的次数为:h-1(h为树的高度),而 h = log2(N+1)(N为树的总结点数).所以堆的向下调整算法的时间复杂度为:O(logN) 。

  上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?

 答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。

代码:

  //建堆
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(php->a, php->size, i);
  }

堆的向上调整算法


当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法。

image.png

向上调整算法的基本思想(以建小堆为例):

1.将目标结点与其父结点比较。

2.若目标结点的值比其父节点的值小,则交换目标节点与其父节点的位置,并将原目标节点的父节点当作新的目标节点继续进行向上调整。若目标节点的值比其父节点的值大,则停止向上调整,此时该树已经是小堆了.

代码如下:

//交换函数
void Swap(HPDataType* x, HPDataType* y)
{
  HPDataType tmp = *x;
  *x = *y;
  *y = tmp;
}
//堆的向上调整(小堆)
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;
    }
  }
}

堆的实现


初始化堆


  首先,必须创建一个堆类型,该类型中需包含堆的基本信息:存储数据的数组,堆中元素的个数以及当前堆的最大容量。

typedef int HPDataType;//堆中存储数据的类型
typedef struct Heap
{
  HPDataType* a;//用于存储数据的数组
  int size;//记录堆中已有元素个数
  int capacity;//记录堆的容量
}HP;

堆的初始化和顺序表很相似基本上什么都不用做,只要指针置空,大小和容量置为0即可。

void HeapInit(HP* hp)
{
  assert(hp);
  hp->a=NULL;
  hp->capacity=hp->size=0;

销毁堆


为了避免内存泄漏,使用完动态开辟的内存空间后都要及时释放该空间,所以,一个用于释放内存空间的函数是必不可少的。

//销毁堆
void HeapDestroy(HP* php)
{
  assert(php);
  free(php->a);//释放动态开辟的数组
  php->a = NULL;//及时置空
  php->size = 0;//元素个数置0
  php->capacity = 0;//容量置0
}

打印堆


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

堆的插入


数据插入时是插入到数组的末尾,即树形结构的最后一层的最后一个结点,所以插入数据后我们需要运用堆的向上调整算法对堆进行调整,使其在插入数据后仍然保持堆的结构。

//堆的插入
void HeapPush(HP* php, HPDataType x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    HPDataType* tmp = (HPDataType*)realloc(php->a, 2 * php->capacity*sizeof(HPDataType));
    if (tmp == NULL)
    {
      printf("realloc fail\n");
      exit(-1);
    }
    php->a = tmp;
    php->capacity *= 2;
  }
  php->a[php->size] = x;
  php->size++;
  //向上调整
  AdjustUp(php->a, php->size - 1);
}

堆的删除


堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个节点,再对堆进行一次向下调整。


原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为O ( N ) O(N)O(N)。若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为O ( log ⁡ ( N ) ) O(\log(N))O(log(N))

//堆的删除
void HeapPop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  Swap(&php->a[0], &php->a[php->size - 1]);//交换堆顶和最后一个结点的位置
  php->size--;//删除最后一个结点(也就是删除原来堆顶的元素)
  AdjustDown(php->a, php->size, 0);//向下调整
}

获取堆顶的数据


获取堆顶的数据,即返回数组下标为0的数据

//获取堆顶的数据
HPDataType HeapTop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  return php->a[0];//返回堆顶数据
}

获取堆的数据个数


获取堆的数据个数,即返回堆结构体中的size变量

//获取堆中数据个数
int HeapSize(HP* php)
{
  assert(php);
  return php->size;//返回堆中数据个数
}

堆的判空


堆的判空,即判断堆结构体中的size变量是否为0

//堆的判空
bool HeapEmpty(HP* php)
{
  assert(php);
  return php->size == 0;//判断堆中数据是否为0
}

建堆的时间复杂度


image.png

相关文章
|
存储 算法 Java
算法系列之数据结构-二叉树
树是一种重要的非线性数据结构,广泛应用于各种算法和应用中。本文介绍了树的基本概念、常见类型(如二叉树、满二叉树、完全二叉树、平衡二叉树、B树等)及其在Java中的实现。通过递归方法实现了二叉树的前序、中序、后序和层次遍历,并展示了具体的代码示例和运行结果。掌握树结构有助于提高编程能力,优化算法设计。
426 10
 算法系列之数据结构-二叉树
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
499 12
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
262 10
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
647 3
|
6月前
|
机器学习/深度学习 算法 机器人
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
634 0
|
6月前
|
数据采集 分布式计算 并行计算
mRMR算法实现特征选择-MATLAB
mRMR算法实现特征选择-MATLAB
411 2
|
7月前
|
传感器 机器学习/深度学习 编解码
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
344 3
|
7月前
|
存储 编解码 算法
【多光谱滤波器阵列设计的最优球体填充】使用MSFA设计方法进行各种重建算法时,图像质量可以提高至多2 dB,并在光谱相似性方面实现了显著提升(Matlab代码实现)
【多光谱滤波器阵列设计的最优球体填充】使用MSFA设计方法进行各种重建算法时,图像质量可以提高至多2 dB,并在光谱相似性方面实现了显著提升(Matlab代码实现)
243 6
|
6月前
|
机器学习/深度学习 算法 机器人
使用哈里斯角Harris和SIFT算法来实现局部特征匹配(Matlab代码实现)
使用哈里斯角Harris和SIFT算法来实现局部特征匹配(Matlab代码实现)
313 8
|
6月前
|
机器学习/深度学习 算法 自动驾驶
基于导向滤波的暗通道去雾算法在灰度与彩色图像可见度复原中的研究(Matlab代码实现)
基于导向滤波的暗通道去雾算法在灰度与彩色图像可见度复原中的研究(Matlab代码实现)
355 8

热门文章

最新文章