数据结构--二叉树的顺序实现(堆实现)

简介: 数据结构--二叉树的顺序实现(堆实现)

引言

在计算机科学中,二叉树是一种重要的数据结构,广泛应用于各种算法和程序设计中。本文将探讨二叉树的顺序实现,特别是堆的实现方式。

一、树

1.1树的概念与结构

树是⼀种⾮线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做

树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。

有⼀个特殊的结点,称为根结点,根结点没有前驱结点。

除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、……、Tm ,其中每⼀个集合 Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有 0 个或多个后继。因此,树是递归定义的。

树形结构中,⼦树之间不能有交集,否则就不是树形结构

⾮树形结构:

总结:

⼦树是不相交的(如果存在相交就是图了,图以后得课程会有讲解)

除了根结点外,每个结点有且仅有⼀个⽗结点

⼀棵N个结点的树有N-1条边

1.2树的基本术语

  • ⽗结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点; 如上图:A是B的⽗结点
  • ⼦结点/孩⼦结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点; 如上图:B是A的孩⼦结点
  • 结点的度:⼀个结点有⼏个孩⼦,他的度就是多少;⽐如A的度为6,F的度为2,K的度为0
  • 树的度:⼀棵树中,最⼤的结点的度称为树的度; 如上图:树的度为 6
  • 叶⼦结点/终端结点:度为 0 的结点称为叶结点; 如上图: BCHI... 等结点为叶结点
  • 分⽀结点/⾮终端结点:度不为 0 的结点; 如上图: DEFG... 等结点为分⽀结点
  • 兄弟结点:具有相同⽗结点的结点互称为兄弟结点(亲兄弟); 如上图: BC 是兄弟结点
  • 结点的层次:从根开始定义起,根为第 1 层,根的⼦结点为第 2 层,以此类推;
  • 树的⾼度或深度:树中结点的最⼤层次; 如上图:树的⾼度为 4
  • 结点的祖先:从根到该结点所经分⽀上的所有结点;如上图: A 是所有结点的祖先
  • 路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列;⽐如A到Q的路径为: A-E-J-Q;H到Q的路径H-D-A-E-J-Q
  • ⼦孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是A的⼦孙
  • 森林:由 m(m>0)棵互不相交的树的集合称为森林;

1.3树的表示法

孩⼦兄弟表⽰法:

树结构相对线性表就⽐较复杂了,要存储表⽰起来就⽐较⿇烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表⽰⽅式如:双亲表⽰法,孩⼦表⽰法、孩⼦双亲表⽰法以及孩⼦兄弟表⽰法等。我们这⾥就简单的了解其中最常⽤的孩⼦兄弟表⽰法

struct TreeNode
 {
   struct Node* child;  // 最左边的孩子结点
   struct Node* brother;  // 指向其右边的下一个兄弟结点
   int data;        // 结点中的数据域
};

1.4树形结构实际运⽤场景

⽂件系统是计算机存储和管理⽂件的⼀种⽅式,它利⽤树形结构来组织和管理⽂件和⽂件夹。在⽂件系统中,树结构被⼴泛应⽤,它通过⽗结点和⼦结点之间的关系来表⽰不同层级的⽂件和⽂件夹之间的关联。

二、二叉树

2.1二叉树的概念与结构

二叉树(binary tree) 是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二” 的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。

总结:

1. ⼆叉树不存在度⼤于 2 的结点

2. ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树

注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的

2.2二叉树的基本术语

  • 根节点(root node) :位于二叉树顶层的节点,没有父节点。
  • 叶节点(leaf node):没有子节点的节点,其两个指针均指向 None
  • 节点所在的 层(level) :从顶至底递增,根节点所在层为 1 。
  • 节点的度(degree) :节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
  • 二叉树的高度(height) :从根节点到最远叶节点所经过的边的数量。
  • 节点的深度(depth) :从根节点到该节点所经过的边的数量。
  • 节点的高度(height) :从距离该节点最远的叶节点到该节点所经过的边的数量。

2.3特殊二叉树

1.完美二叉树(满二叉树)

完美二叉树(perfect binary tree) 所有层的节点都被完全填满。在完美二叉树中,叶节点的度

0 ,其余所有节点的度都为 2 ;若树的高度为 ,则节点总数为 2 ℎ+1 − 1 ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。

2. 完全二叉树

完全二叉树(complete binary tree) 只有最底层的节点未被填满,且最底层节点尽量靠左填充。

2.4二叉树的存储结构

⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构。

2.4.1顺序结构

顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树 ,因为不是完全⼆叉树会有空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。

2.3.2 链式结构

⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址 。链式结构⼜分为 ⼆叉链 三叉链 。(红⿊树等会⽤到三叉链。)

三、实现顺序结构二叉树

⼀般堆使⽤顺序结构的数组来存储数据,堆是⼀种特殊的⼆叉树,具有⼆叉树的特性的同时,还具备其他的特性。

3.1堆的概念与结构

堆是一种完全二叉树,具有以下性质:

  • 最大堆:每个节点的值都大于或等于其子节点的值。
  • 最小堆:每个节点的值都小于或等于其子节点的值。

堆常用于实现优先队列,因为它能有效地支持插入和删除操作。

总结

堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值;

堆总是⼀棵完全⼆叉树。

3.2堆的实现

我们以小堆为例进行实现:

3.2.1存储结构

二叉树的顺序存储通常使用数组来实现。对于一个节点在数组中的索引 i,可以通过以下方式找到其父节点和子节点的索引:

  • 父节点(i - 1) / 2(取整)
  • 左子节点2 * i + 1
  • 右子节点2 * i + 2
typedef struct Heap
{
  DataType *arr;
  int size;
  int capacity;
}HP;

这里可以看到堆的底层结构和动态顺序表一样。

3.2.2相关操作

1.初始化

//初始化
void HPInit(HP* p)
{
  assert(p);
  p->arr = NULL;
  p->size = p->capacity = 0;
}

2.销毁

//销毁
void HPDestory(HP* p)
{
  assert(p);
  if (p->arr)
  {
    free(p->arr);
  }
  p->arr = NULL;
  p->size = p->capacity = 0;
}
Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}

3.向上调整

💡 向上调整算法

先将元素插⼊到堆的末尾,即最后⼀个孩⼦之后

插⼊之后如果堆的性质遭到破坏,将新插⼊结点顺着其双双亲往上调整到合适位置即可

//堆的向上调整
void AdjustUp(DataType* arr, int child)
{
  int parent = (child - 1) / 2;//父节点
  while(child>0)//注意child可能会被调整到根节点,这时候就不能再调整了
  {
    if (arr[child] < arr[parent])//如果条件语句不成立,就说明堆已经成型
    {
      Swap(&arr[child], &arr[parent]);
      child = parent;
      parent = (child - 1) / 2;//循环以上步骤
    }
    else
    {
      break;
    }
  }
}

4.堆的插⼊

将新数据插⼊到数组的尾上,再进⾏向上调整算法,直到满⾜堆。

//插入
void HPPush(HP* p, DataType x)
{
  assert(p);
  if (p->size == p->capacity)//判断空间是否足够
  {
    //扩容
    int NewCap = p->capacity == 0 ? 4 : 2 * p->capacity;
    DataType* tmp = (DataType*)realloc(p->arr, NewCap * sizeof(DataType));
    if (tmp == NULL) 
    {
      perror("realloc fail!");
      exit(1);
    }
    p->arr = tmp;
    p->capacity = NewCap;
  } 
  p->arr[p->size] = x;
  AdjustUp(p->arr, p->size);
  ++p->size;
}

5.向下调整法

💡 向下调整算法

将堆顶元素与堆中最后⼀个元素进⾏交换

删除堆中最后⼀个元素

将堆顶元素向下调整到满⾜堆特性为⽌

向下调整算法有⼀个前提:左右⼦树必须是⼀个堆,才能调整。

//堆的向下调整
void AdjustDwon(DataType* arr, int parent, int n)
{
  int child = parent * 2 + 1;//左孩子
  while (child < n) 
  {
    //寻找左右孩子最小的那个
    if (child + 1 < n && arr[child] > arr[child + 1])
    {
      child++;
    }
    //这里和向上调整就一样了,比较,交换,循环
    if (arr[child] < arr[parent])
    {
      Swap(&arr[child], &arr[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}

6.堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进⾏向下调整算法。

//删除,出堆顶数据
void HPPop(HP* p)
{
  assert(p && p->size);
  Swap(&p->arr[0], &p->arr[p->size - 1]);
  --p->size;
  AdjustDwon(p->arr, 0, p->size);
}

7.判空

//判空
bool HPEmpty(HP* p)
{
  assert(p);
  return p->size == 0;
}

8.返回堆顶元素

//返回堆顶元素
DataType HPTop(HP* p)
{
  assert(p && p->size);
  return p->arr[0];
}

四、完整代码

Heap.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
//定义堆的结构--数组
typedef int DataType;
typedef struct Heap//小堆为例
{
  DataType *arr;
  int size;
  int capacity;
}HP;
//初始化
void HPInit(HP* p);
//销毁
void HPDestory(HP* p);
//插入
void HPPush(HP* p,DataType x);
//删除,出堆顶数据
void HPPop(HP* p);
//判空
bool HPEmpty(HP* p);
//返回堆顶元素
DataType HPTop(HP* p);

Heap.c

#define _CRT_SECURE_NO_WARNINGS
#include"Heap.h"
//初始化
void HPInit(HP* p)
{
  assert(p);
  p->arr = NULL;
  p->size = p->capacity = 0;
}
//销毁
void HPDestory(HP* p)
{
  assert(p);
  if (p->arr)
  {
    free(p->arr);
  }
  p->arr = NULL;
  p->size = p->capacity = 0;
}
Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
//堆的向上调整
void AdjustUp(DataType* arr, int child)
{
  int parent = (child - 1) / 2;//父节点
  while(child>0)//注意child可能会被调整到根节点,这时候就不能再调整了
  {
    if (arr[child] < arr[parent])//如果条件语句不成立,就说明堆已经成型
    {
      Swap(&arr[child], &arr[parent]);
      child = parent;
      parent = (child - 1) / 2;//循环以上步骤
    }
    else
    {
      break;
    }
  }
}
//插入
void HPPush(HP* p, DataType x)
{
  assert(p);
  if (p->size == p->capacity)//判断空间是否足够
  {
    //扩容
    int NewCap = p->capacity == 0 ? 4 : 2 * p->capacity;
    DataType* tmp = (DataType*)realloc(p->arr, NewCap * sizeof(DataType));
    if (tmp == NULL) 
    {
      perror("realloc fail!");
      exit(1);
    }
    p->arr = tmp;
    p->capacity = NewCap;
  } 
  p->arr[p->size] = x;
  AdjustUp(p->arr, p->size);
  ++p->size;
}
//堆的向下调整
void AdjustDwon(DataType* arr, int parent, int n)
{
  int child = parent * 2 + 1;//左孩子
  while (child < n) 
  {
    //寻找左右孩子最小的那个
    if (child + 1 < n && arr[child] > arr[child + 1])
    {
      child++;
    }
    //这里和向上调整就一样了,比较,交换,循环
    if (arr[child] < arr[parent])
    {
      Swap(&arr[child], &arr[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
//删除,出堆顶数据
void HPPop(HP* p)
{
  assert(p && p->size);
  Swap(&p->arr[0], &p->arr[p->size - 1]);
  --p->size;
  AdjustDwon(p->arr, 0, p->size);
}
//判空
bool HPEmpty(HP* p)
{
  assert(p);
  return p->size == 0;
}
//返回堆顶元素
DataType HPTop(HP* p)
{
  assert(p && p->size);
  return p->arr[0];
}

main.c

#define _CRT_SECURE_NO_WARNINGS
#include"Heap.h"
void test01()
{
  HP hp;
  HPInit(&hp);
  int arr[] = { 17,20,10,13,19,15 };
  for (int i = 0; i < 6; i++)
  {
    HPPush(&hp, arr[i]);
    }
  //HPPop(&hp);
  while (!HPEmpty(&hp))
  {
    printf("%d-", HPTop(&hp));
    HPPop(&hp);
  }
  HPDestory(&hp);
}
int main()
{
  test01();
  return 0;
}

五、总结

通过顺序实现的方式,我们可以高效地存储和操作二叉树。堆作为一种特殊的二叉树,提供了快速的插入和删除操作,非常适合优先队列的实现。在许多应用场景中,如任务调度、图算法等,堆结构都发挥着重要作用。

希望本文能够帮助你理解二叉树的顺序实现及堆的基本操作。继续探索数据结构的世界,会发现更多有趣的应用和挑战!


相关文章
|
22天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
18天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2566 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
14天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
16天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
18天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1561 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
1天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
20天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
885 14
|
15天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
655 7
|
9天前
|
Docker 容器
|
1天前
|
存储 人工智能 弹性计算
产品技术能力飞跃,阿里云E-HPC荣获“CCF 产品创新奖”!
9月24日,在中国计算机学会举办的“2024 CCF 全国高性能计算学术年会”中,阿里云弹性高性能计算(E-HPC)荣获「 CCF HPC China 2024 产品创新奖」。这也是继 2022 年之后,阿里云E-HPC 再次荣获此奖项,代表着阿里云在云超算领域的持续创新结果,其产品能力和技术成果得到了业界的一致认可。