数据结构与算法——第五节 树和堆

简介: 其按照我们想要的方式排列了出来。由于我们在向下调整算法里的if给的是 a[child + 1] > a[child]和a[child] > a[parent]才交换,那么我们得到的就是一个大堆。

目录


树的定义及相关定义


树的定义


树的节点的相关概念


树的表示法(存储法)


1、双亲表示法:


2、孩子表示法


3、孩子兄弟表示法:


二叉树的概念及相关概念


二叉树的概念


二叉树的特点:


特殊的二叉树


1、斜树:


2、满二叉树


3、完全二叉树


3、二叉树的性质


4、二叉树的存储


顺序存储


链式存储


5、二叉树的遍历方法


1、前序遍历


2、中序遍历


3、后续遍历


4、层序遍历


堆(优先队列)


堆的概念:


堆的性质(两条):


堆的实现


堆的向下调整算法


堆的创建


堆排序


堆的模拟实现


函数1:void HeapInit(HP* php,HPDataType* a, int n);  //初始化函数


函数2:void HeapDestroy(HP* php);//销毁堆


函数3: void HeapPrint(HP* php)//打印堆


函数4:void AdjustDown(int* a, int n, int parent);//向下调整


函数5:void AdjustUp(int* a, int n, int child);//向上调整


函数6:void HeapPush(HP* php, HPDataType x);//插入


函数7:void HeapPop(HP* php); //头删


函数8:HPDataType Heaptop(HP* php);//取出堆顶上的元素


其他函数:


今天,我们的任务主要是讲解树的有关内容。树和堆。


首先,我们来花点时间,把树有关的所有的概念性的东西全部介绍一遍。


树的定义及相关定义

树的定义

树是n个节点的有限集。n=0时称为空树。


在任何一棵非空树中:


(1)有且仅有一个特定的称为根。


(2)当n > 1时,其余节点可分为m(m>0)个互不相交的有限集T1、T2...Tm,其中每一个集合本身又是一棵树,并且称为根的子树。(以上来源于《大话数据结构》,在此鸣谢)

image.png



如图,这就是一棵树。


由上面可以看出,我们在定义树的时候,就用到了递归的概念。


反正对于树而言,结点之间不可以形成闭合的回路。


一定要注意,树的结点直接是不可以形成闭合的回路的!!!形成回路的那就是图了。


树的节点的相关概念

我们知道,树的结点一定是包含着一个数据域和若干个指向其子树的分支。


1、度:


节点拥有的子树个称之为树的度。


例如,我们刚刚上面的那张图片:A的度就是2;B的度也是2 ;D的度为0.


其中度为0的结点称为树的叶子


2、孩子、父(双)亲、兄弟和子孙


官方的说法是这样的:结点的子树的根称为该结点的孩子。相应地,该结点称为孩子的父(双)亲。二同一双亲的孩子之间互相成为兄弟;


以某结点为根的子树中的任一结点都成为该结点的子孙。


而反过来该结点称为其子树的祖先。


像上图中,A为B和C的父(双)亲;B为A的孩子;B和C互为兄弟;


3、树的度


树的度,也叫树的层次,它意为:从根节点开始,根为第一层,根的孩子为第二层,若某结点在第i层,那么其子树的根就是在i+1层。


像上图所示,该树的度为4.


4、森林


森林是m个不相交的树的集合,其中m>=0;对于每一个树而言,其子树的集合即为森林。


如上图,以B和C为根结点的子树就构成了森林。


image.png


(重影是由于文件类型转换的时候存在些问题,我也是没有办法了......)


(为方便理解,下面还有好多这么长的动画呦👉)


树的表示法(存储法)

对于树,我们有顺序表示法和链式表示法两种,我们在下面会渗透。


1、双亲表示法:

由于除了根节点之外,其余的每个结点,它不一定有孩子,但是它们一定有且仅有一个父(双)亲。


根据这样一个规律,我们假设用一组连续的空间来存储树的结点(说白了,就是用数组存储)。当然也可以不用数组存储;


而在每个结点中,再设一个指针指向双亲结点中的其他位置。


就是说,每一个结点除了知道自己在哪里以外,还知道它的双亲在哪里。


我们还是用上述的树来举例:


A到G它们的下标分别从0到6,那么我们就可以用如下的方式来表示各个节点之间的关系:


其中,我们规定根结点A的指针域为-1,就是说,我们规定其双亲的下标为-1.



数据域
指针域
结点A x -1
结点B x 0
结点C x 0
结点D x 1
结点E x 1
结点F x 2
结点G x 2


当然,这样表示也是有缺点的。就是我们只能够从孩子往双亲去遍历。


如果我们想要从双亲遍历到孩子呢?


那就必须要遍历整棵树了。很麻烦。


当然,我们也有能够稍微改进一点的办法,就是在每一个结点记住了其双亲的同时,再记住其第一个孩子。但是这样,就又有点不像双亲表示法了,而更像我们后面所说的孩子表示法了。


2、孩子表示法

我们接下来就介绍孩子表示法。对于一棵树的每一个结点,我们分别计算出每个结点的度的大小,然后取其最大的结点,每个结点就开辟出这么多个指针域,分别表示每一个孩子。如果孩子不够,就用空指针表示。比如:

(该图节选自《大话数据结构》,在此鸣谢)


(注:本图与上面的例子无关)


那么这样的方法显然也是有缺陷的。


很显然,当结点之间的极差很大的时候,我们浪费了很多空间。


那有没有什么更好的方法呢?


我们可以将其改进成:先专门开辟一块空间用来存放结点的度,然后再去按照结点的度取分配空间。


就像这样:



(该图节选自《大话数据结构》,在此鸣谢)


(注:本图与上面的例子无关)


但是这样也有缺点:每个结点大小并不一样,开辟的时候有困难。


又单独开辟了一个数据域用于存放树的度,增加了维护负担。


再想,有没有更好的方法?


我们结合着上述方法的缺点,可以考虑用下面这种方法:孩子兄弟表示法。


3、孩子兄弟表示法:

该方法的主要思路,就是我们通过双亲取找到它的第一个孩子,然后再通过第一个孩子去找到剩下的孩子。


如图:



(该图节选自《大话数据结构》,在此鸣谢。)


(emmm之所以节选这么多,是因为我发现该书上有好多都有现成的,我比较懒,就不另画了)


(不过本文章的内容绝不是个别书籍的照搬照抄呦)


(注:本图与上面的例子无关)


这样表示的话,这样的话,就给查找某个结点带来了方便。


不过,也不是很好,它也是只能找孩子,找双亲也还是比较麻烦。当然改进的方法,可以再增加一个parent指针域来解决这个问题。不过在这里,就不细谈了。


二叉树的概念及相关概念

二叉树的概念

二叉树是树的一种 特殊形式,特殊在什么地方呢?就是它的每个结点的度的最大值只能为2。


它要么为空集,要么由一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树的二叉树组成。


二叉树的特点:


1. 每个结点最多有两棵子树,即二叉树不存在度大于2的

2. 二叉树的子树有左右之分,其子树的次序不能颠倒。


上述的第一点不再赘述,第二点的意思是树的子树分为左子树和右子树,右子树和左子树的顺序不能颠倒。右子树就是右子树,左子树就是左子树。


特殊的二叉树

1、斜树:

顾名思义就好了。就是指一个树是斜的。


注意,这里的斜着的指的一定是这样的:

image.png



就是它们只有左子树,或者只有右子树才可以。


2、满二叉树

就是指所有的分支都存在左子树和右子树,并且所有叶子都在同一层。


就比如,我们最先开始举的例子就是一颗满二叉树。就是这个:

image.png



也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 。


则它就是满二叉树。


3、完全二叉树

完全二叉树是效率很高的数据结构,


这个理解着有点门槛,官方的定义是:对一个具有n个结点的二叉树一层一层从左到右编号,如果每一个结点的编号和它们在满二叉树中像这种方式排编的结点的编号相同,那么,该树就是完全二叉树。


翻译成人话:1、将一颗二叉树一层一层从上到下、从左到右排序;


                    2、将和该二叉树深度相同的满二叉树按照相同方式排序。


                   3、对比该二叉树的结点和对应的满二叉树的结点。如果该二叉树的每一个结点的编号,和在满二叉树中的编号相同,那么该树就是完全二叉树。


对于完全二叉树而言:


叶子结点只能出现在后两层。

最下一层的叶子一定集中在左部连续位置

如果倒数第二层有叶子结点,那么一定在树的右部的连续位置

如果某结点的度为1,那么它一定是左节点。

同结点数的二叉树,完全二叉树的深度最小。

要注意的是满二叉树是一种特殊的完全二叉树


3、二叉树的性质

若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点.

若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1.

对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1

若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=Log2(n+1). (ps:Log2(n+1)是log以2为底,n+1为对数)

对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点

2. 若2i+1=n否则无左孩子

3. 若2i+2=n否则无右孩子

4、二叉树的存储

我们上面说了树的存储,而对于二叉树这么一个特殊的结构来说,我们也分为顺序结构和链式结构。


有了上面的基础,接下来的内容应该就很好理解了:


顺序存储

先将一颗二叉树的所对应的完全二叉树画出来,然后依次排序,将它们存储到一个数组当中,如果二叉树的某个结点不存在,就用NULL来表示。


举一个简单的例子:

image.png



就比如这么一颗二叉树,存储方式如下图:

image.png



这种存储方式实际上缺点是很明显的,就是对于极端情况(比如右斜树),它会浪费大量的空间。


所以,这种方法也只适用一般的二叉树。


链式存储

那么对于链式存储,一般都叫二叉链表。


这里的存储和前面的普通的树是一样的,只不过其只主要开辟指向左右两边的空间就可以了。


就像下面的代码(模拟树的结点)

struct BinaryTreeNode
{
    struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    struct BinTreeNode* _pRight; // 指向当前节点右孩子
    BTDataType _data; // 当前节点值域
}


当然,这样的方法还是向上面所说的那样有弊端——无法访问它的双亲。


我们考虑改进方法,可以考虑用三叉链表:


就像这样:

struct BinaryTreeNode
{
    struct BinTreeNode* _pParent; // 指向当前节点的双亲
    struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    struct BinTreeNode* _pRight; // 指向当前节点右孩子
    BTDataType _data; // 当前节点值域
};


5、二叉树的遍历方法

遍历方法我们有四种,我们今天模拟实现三种,原因是还有一种直接用顺序结构就出来了。


那么是哪四种呢?它们分别是前序遍历、中序遍历、后序遍历和层序遍历。


我们用模拟实现的方式来实现一下这些方法。


我们首先创建树结点:


在这里,我们只是定义了左节点和右节点,就是说,我们用到的是二叉链表。

#include<stdio.h>
#include<stdlib.h>
typedef int BTDataType;
typedef struct BinaryTreeNode
{
  struct BinaryTreeNode* left;
  struct BinaryTreeNode* right;
  BTDataType data;
}BTNode;


我们还是先完善一下,给一个创建节点的函数:

BTNode* CreateTreeNode(BTDataType x)
{
  BTNode* node = (BTNode*)malloc(sizeof(BTNode)); //开辟
  node->data = x;                       //给上数据域
  node->left = NULL;                     
  node->right = NULL;                   //左右指针先置空
  return node;                          //返回这个创建的结点
}


好。


那么我们先用这个函数,创建那么几个结点:

int main()
{
  BTNode* A = CreateTreeNode('A');
  BTNode* B = CreateTreeNode('B');
  BTNode* C = CreateTreeNode('C');
  BTNode* D = CreateTreeNode('D');
  BTNode* E = CreateTreeNode('E');
  BTNode* F = CreateTreeNode('F');
  BTNode* G = CreateTreeNode('G');
  A->left = B;
  A->right = C;
  B->left = D;
  B->right = E;
  C->left = F;
  C->right = G;
  return 0;
}



这样的话,我们就构建出来了棵树。

image.png



1、前序遍历

若二叉树为空,则返回空。它的原则是,先访问遍历根节点,然后再前序遍历左子树,再前序遍历右子树。


我们还是以刚刚上面的例子,接着说:


来看下面的动画:


image.png


这个动画结束后,应该就不再需要我的啰嗦了。


代码实现一下:


注意到我们刚刚在定义树的时候、定义前序遍历的时候都是用递归来去实现的。


所以我们的代码也考虑用递归来去实现。


顺着刚刚的代码来写:


前序遍历:

void PrevOrder(BTNode* root)
{
  if (root == NULL)
  {
  //printf("NULL ");  //这个可要可不要,如果为了便于理解,可以要。
  return;
  }
  printf("%c ", root->data);
  PrevOrder(root->left);
  PrevOrder(root->right);
}


这样,我们所遍历出来的就是:

image.png



可以看出,其所遍历的顺序恰好就是我们上面所画的那种顺序。


2、中序遍历

有了前序遍历,中序遍历和后序遍历就像多米诺骨牌一样,好理解很多了。


中序遍历,就是先访问根的左子树,再访问根,最后访问右子树。


我们刚刚的前序遍历是先访问根,再访问左子树,最后访问右子树的;


                而现在的中序遍历是将访问根和访问左子树的顺序交换一下。


我们还可以从二者的代码上面来对比:

void InOrder(BTNode* root)
{
  if (root == NULL)
  {
  printf("NULL ");
  return;
  }
  InOrder(root->left);
  printf("%c ", root->data);
  InOrder(root->right);
}


可以看出和上面的区别:printf的位置不同了,而printf实际就是访问根节点的顺序。就是说,我先递归,再打印。在递归。


这个不难理解,就是有点绕。


笔者这里真的是懒得画了,读者有兴趣可以自己动手调式试试,代码和思路都已经给出了。


3、后续遍历

还是和前序、中序去对比,前序是先访问根节点,中序是在中间的位置访问根节点,那么后序就是在最后的位置去访问根节点。


我们这里就不再去做过多的赘述了。


我们可以再来把代码对比一下:

void PostOrder(BTNode* root)
{
  if (root == NULL)
  {
  printf("NULL ");
  return;
  }
  PostOrder(root->left);
  PostOrder(root->right);
  printf("%c ", root->data);
}


可以看到,递归什么的都是一模一样的,不一样的仅仅是printf的位置,即访问根节点的位置。


后续遍历的根节点的访问(也就是printf的位置)是在最后。


4、层序遍历

这个遍历就比较简单了。就是一层一层地去遍历,就和我们刚刚的用顺序结构来存储树的方式差不多。所以,如果我们采用这种方式去遍历, 那我们就完全可以用顺序结构存储,不再向上面用二叉链表。这样的话,遍历这一颗树就相当于遍历数组啦!!!哈哈。


关于树的遍历方式,我们就介绍到这里。


各位宝贝们,我们现在洋洋洒洒地说了那么多,实际上,硬菜才刚刚开始。


我们下面来说堆(优先队列)


堆(优先队列)

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。


而完全二叉树更适合使用顺序结构存储。


现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。


需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事。一个是数据结构,一个是操作系统中管理内存的一块区域分段。


那么,说了半天,什么叫堆?


堆的概念:

堆是一棵顺序存储的完全二叉树。


其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。


其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。


举例来说,对于n个元素的序列{R0, R1, ... , Rn}当且仅当满足下列关系之一时,称之为堆:


(1) Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)


(2) Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)


其中i=1,2,…,n/2向下取整;


(节选自静默虚空的博客排序六 堆排序 - 静默虚空 - 博客园 (cnblogs.com),在此鸣谢~~)


堆的性质(两条):

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

堆总是一棵完全二叉树。

image.png


(小根堆示例)


image.png


(大根堆示例)


堆的实现

在此之前,我们先来介绍一种算法:堆的向下调整算法


堆的向下调整算法

我们以小根堆为例,对于这样一个堆,如果只有根节点的位置不满足堆的性质,即一棵树,根节点的左子树是小堆,右子树也是小堆,整棵树不是小堆,那么用向下调整算法,就可以让整棵树变成小堆。


那么我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。


再强调一遍:我们这个算法有一个前提,那就是左右子树都得是堆。


那向下调整算法到底是怎么一回事呢?


举个例子吧来说明吧:

image.png



那么如果是想要调整为大根堆呢?


一样的方法。


这种方法,是我们接下来进行一系列其他“活动”的核心。


我们来说一下代码:


这个函数有三个参数,分别是数组a,数组的元素个数n,要调整的双亲的下标parent

void swap(int* p1, int* p2)
{
  int temp = *p1;
  *p1 = *p2;
  *p2 = temp;
} //一个交换函数
void AdjustDown(int* a, int n, int parent)
{
  int child = parent * 2 + 1;   //左孩子结点的下标为双亲的下标+1
  while (child < n)             //如果左孩子仍然在结点总数范围内
  {
        //我们先默认左孩子比右孩子要小
  if (child + 1 < n && a[child + 1] < a[child])  //但是如果右孩子在结点总数范围之内并且 
                                                       //右孩子比左孩子还小
  {
    ++child;                                  //那就让child指到右孩子的头上
  }
  if (a[child] < a[parent])                     //如果双亲比孩子要大
  {
    swap(&a[child], &a[parent]);              //那就交换
    parent = child;                           //然后让孩子当成新的双亲
    child = parent * 2 + 1;                   //计算出新的孩子,继续下一轮的计算
  }
  else          
  {
    break;                                    //如果双亲不比孩子要大,那就直接跳出
  }
  }
}



该讲解的地方笔者都已经很详细地讲解到了。


堆的创建

如何把一个普通的数组变成一个堆的形式呢?


还是举一个小例子:


int a[] = {1,3,5,7,2,4,0};

先将其按照完全二叉树的方式排列开来:

image.png



那么我们应当怎样操作才能使得其变成一个堆呢?


我们以小堆为例来看:总体来说,就是:从后往前来依次用向下调整算法。


首先,对0用向下调整算法,它就是它自己一个人,所以不用动。4,2,7同理。


接着,对5用向下调整算法(注意我们这里所说的是以该结点为根节点的(子)树),可以得到这样一棵树(如左下图):

image.png



然后,再对3用向下调整算法。得到这样一棵树(如右上图)


此时,我们发现,1的左边是小堆,右边是小堆,那么我们紧接着,我们再对1用向下调整算法,就 得到了最终的小堆。


注意到,对于一个孩子而言,它减1除以2得到的就是它的双亲的下标(因为这里是整除,所以我们就不需要分类讨论了)即parent = (child-1)/2。


那我们就直接这样好啦


int main()
{
  int a[] = { 1,3,5,7,2,4,0 };
  int n = sizeof(a) / sizeof(a[0]);//建堆
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)//n-1是小标,再减一意为算出parent
  {
  AdjustDown(a, n, i);
  }
  return 0;
}


循环个这么多次,然后对这些结点依次向下调整,最终,我们就得到了一个小堆。


有兴趣可以自己将a再打印出来,这里我们就不再做过多的赘述。


我们来算一算它的这样去建堆的时间复杂度是多少。


对于一个堆,它最坏的情况下,需要调整的次数为:


f = 1*(h-1)+2*(h-2)+(2^2)*(h-3)+...+(2^(h-1))*1 (通项为每一层需要调整的次数*每一层的结点数)


然后用错位相减法,得出其算法的时间复杂度为O(N)。(这个应该能算好吧,要不然你高一的数学老师该来找你了哈哈)


堆排序

还是刚刚那么一个数组


int a[] = {1,3,5,7,2,4,0};

我用堆排,将其排成降序,其算法的思路是这样的:


文字表述版:


1、首先,建堆。(我们这里建小根堆,具体原因我们后面再说)


2、交换第一个数(最小的数)和最后一个数(因为我们要排降序),并且这样可以最大限度地保留原有的树形结构,不用再去 建堆了。


3、将最后一个数舍弃(指不在下一次排序的考虑范围内)


重复步骤2和3。


图示版:

image.png



没有画完,累死我了,我都要画晕了。。。后面的以此类推就可以了,主体的思路已经呈现出来了。


代码版:

void HeapSort(int* a, int n)
{
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
  AdjustDown(a, n, i);   //先去建堆
  }
  int end = n - 1;           //最后一个结点的下标
  while (end)                 
  {
  swap(&a[0], &a[end]); //交换第一个位置和最后一个位置
  AdjustDown(a, end, 0); //向下调整
  end--;               //最后一个数拿掉
  }
}


我们运行测试一下:

void HeapSort(int* a, int n)
{
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
  AdjustDown(a, n, i);
  }
  int end = n - 1;
  while (end)
  {
  swap(&a[0], &a[end]);
  AdjustDown(a, end, 0);
  end--;
  }
}
int main()
{
  int a[] = { 1,3,5,7,2,4,0 };
  HeapSort(a, sizeof(a) / sizeof(int));
  for (int i = 0; i < sizeof(a) / sizeof(int); i++)
  {
  printf("%d ", a[i]);
  }
  return 0;
}



如图所示:



那么该算法的时间复杂度是多少呢?


答案是N*log(N).为什么?我们现在不说,等下一章十大排序再说。


堆的模拟实现

好啦,最后一个任务。模拟实现一棵树。弄完我们就下班。


右了上面的基础,这次应该很快了。


首先, 我们这样去创建一个结点:


要求有一个数组去存储,然后要有数组的大小和容量。

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<string.h>
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;
  int size;
  int capacity;
}HP;


我们计划实现这些函数:

void HeapInit(HP* php,HPDataType* a, int n);
void HeapDestroy(HP* php);
void HeapPrint(HP* php);
void AdjustDown(int* a, int n, int parent);
void AdjustUp(int* a, int n, int child);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType Heaptop(HP* php);
int HeapSize(HP* php);
bool HeapEmpty(HP* php);
void swap(int* p1, int* p2);


具体是干什么用的,想写了这么多,英文都是一样的,不说也应该知道了。


不过我们还是会一个一个介绍:


函数1:void HeapInit(HP* php,HPDataType* a, int n);  //初始化函数

void HeapInit(HP* php, HPDataType* a, int n)
{
  assert(php);
  php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);//先开辟n个空间
  if (php->a == NULL)
  {
  printf("malloc fail\n");
  exit(-1);
  }
  memcpy(php->a, a, sizeof(HPDataType) * n);             //把a的数据全部拷贝过来
  php->size = n;                     
  php->capacity = n;                                    //容量和大小均赋值为n
  for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
  {
  AdjustDown(php->a, php->size, i);                 //向下调整建堆
  }
}


函数2:void HeapDestroy(HP* php);//销毁堆

由于是连续开辟的,那么我们直接free一下就可以了

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


函数3: void HeapPrint(HP* php)//打印堆

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


函数4:void AdjustDown(int* a, int n, int parent);//向下调整

这个不说了,说过了,说烂了。

void swap(int* p1, int* p2)
{
  int temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}
void AdjustDown(int* 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;
  }
  }
}



函数5:void AdjustUp(int* a, int n, int child);//向上调整

向上调整是某一个结点往上去找,这个时候,我们传的是child,然后和它的parent去比较。

void AdjustUp(int* a, int n, int child)
{
  int parent;
  while (child > 0)
  {
  parent = (child - 1) / 2;
  if (a[child] > a[parent])
  {
    swap(&a[child], &a[parent]);
    child = parent;
  }
  else
  {
    break;
  }
  }
}

这个也就不说了。



函数6:void HeapPush(HP* php, HPDataType x);//插入

void HeapPush(HP* php, HPDataType x)
{
  if (php->size == php->capacity)
  {
  HPDataType* tmp = (HPDataType*)realloc(php->a, php->capacity * 2 * 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, php->size - 1); //然后向上调整
}


函数7:void HeapPop(HP* php); //头删

void HeapPop(HP* php)
{
  assert(php);
  assert(php->size > 0);
  swap(&php->a[php->size - 1], &php->a[0]);
  php->size--;
  AdjustDown(php->a, php->size, 0);
}


函数8:HPDataType Heaptop(HP* php);//取出堆顶上的元素

HPDataType Heaptop(HP* php)
{
  assert(php);
  assert(php->size > 0);
  return php->a[0];        //直接return就可以了
}


其他函数:

最后两个我们放一起讲:

int HeapSize(HP* php)
{
  assert(php);
  return php->size;   //返回size
}
bool HeapEmpty(HP* php)
{
  assert(php);
  return php->size == 0;   //判断是否为空,是返回1,不是返回0
}


我们来将这些接口用一下试试看:

#include"tree.h"
int main()
{
  int a[] = { 15,18,28,34,65,19,49,68,37,27 };
  int n = sizeof(a) / sizeof(a[0]);
  HP hp;
  HeapInit(&hp, a, n);      //初始化
  HeapPrint(&hp);           //打印
  HeapPush(&hp, 8);         //插入
  HeapPrint(&hp);           //打印
  HeapPush(&hp, 96);        //插入
  HeapPrint(&hp);           //打印
    HeapDestroy(&hp);
  return 0;
}


得到运行截图:



可以看出,


其按照我们想要的方式排列了出来。由于我们在向下调整算法里的if给的是 a[child + 1] > a[child]和a[child] > a[parent]才交换,那么我们得到的就是一个大堆。


当然,如果你想玩更多的花样,可以自行下去调试。


有关树的内容就介绍到这里吧~~~我们下节——十大排序算法再见。


目录
相关文章
|
24天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
30 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
5天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
26 5
|
26天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
63 16
|
21天前
|
算法
树的遍历算法有哪些?
不同的遍历算法适用于不同的应用场景。深度优先搜索常用于搜索、路径查找等问题;广度优先搜索则在图的最短路径、层次相关的问题中较为常用;而二叉搜索树的遍历在数据排序、查找等方面有重要应用。
26 2
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
72 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
72 1
|
2月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
24 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
2月前
|
Java C++
【数据结构】探索红黑树的奥秘:自平衡原理图解及与二叉查找树的比较
本文深入解析红黑树的自平衡原理,介绍其五大原则,并通过图解和代码示例展示其内部机制。同时,对比红黑树与二叉查找树的性能差异,帮助读者更好地理解这两种数据结构的特点和应用场景。
33 0
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆