【数据结构初阶】七、非线性表里的二叉树(堆的实现 -- C语言顺序结构)

=========================================================================

相关代码gitee自取

C语言学习日记: 加油努力 (gitee.com)

 =========================================================================

接上期

【数据结构初阶】六、线性表中的队列(链式结构实现队列)-CSDN博客

 =========================================================================

                     

1 . 非线性表里的 树(Tree)

树的概念及结构:

            

树的概念

是一种非线性的数据结构
它是 n(n>=0) 个有限节点组成一个具有层次关系的集合
把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
( jie点线性结构 -- 结点  ;非线性结构 -- 节点 )

               

树的结构

  • 有一个特殊的节点,称为根节点,根节点没有前驱节点
                 
  • 除根节点外其余节点分成 M(M>0) 互不相交的集合 T1 、T2 、……Tm ,
    其中每一个集合 Ti(1<= i <= m) 又是一棵结构与树类似的子树
    每棵子树的根节点有且只有一个前驱节点可以有零个或多个后继节点
                   
  • 递归定义
                 
  • 树形结构中,子树之间不能有交集否则就不是树形结构
图示:

                

树的其它相关概念:

(有些概念对应意思不好理解,可以结合看下列示例图的例子)

概念名称对应意思下图中例子
节点的度一个节点含有子树个数 称为 该节点的度节点A的度为6
🔺叶节点
(终端节点)
度为0节点 称为 叶节点节点BCH……为叶节点

🔺分支节点
(非终端节点)

度不为0节点 称为 分支节点节点DEF……为分支节点
🔺父节点
(双亲节点)
一个节点含有子节点
则这个节点 称为 其子节点的父节点
节点A节点B的父节点
🔺子节点
(孩子节点)
一个节点含有的子树的根节点 称为 该节点的子节点节点B节点A的子节点
兄弟节点具有相同父节点的节点 互称为 兄弟节点节点BC是兄弟节点
树的度一棵树中最大的节点的度 称为 树的度下图中树的度为6
🔺节点的层次从根开始定义起
第一(零)层根的子节点第二(一)层
以此类推建议从第一层起算空树可用零层表示
节点A所在层为第一层,
节点B所在层为第二层……
🔺树的高度
(树的深度)
树中节点的最大层次下图中树的高度(深度)为4
堂兄弟节点父节点(双亲节点)在同一层的节点 互称为 堂兄弟节点HI互为兄弟节点
🔺节点的祖先从根到该节点上的所有的分支节点 都是 祖先节点A是所有节点的祖先
🔺子孙以某节点为根的子树中任一节点
都称为 该节点的子孙
所有节点都是节点A的子孙
森林m(m>0)棵互不相交的树组成的集合 称为 森林
示例图:

                     

                    


                    

树的存储(表示):

                  

树的表示方式:

  • 树结构相对线性表就比较复杂了,要存储表示起来比较麻烦了,
    既要保存值域也要保存节点和节点之间的关系
                      
  • 实际中有很多种表示方式如:
    双亲表示法孩子表示法孩子双亲表示法以及孩子兄弟表示法等,
    我们这里就简单了解其中最常用孩子兄弟表示法
    树结构的最优设计

                          

(左)孩子(右)兄弟表示法结构:

                     //树中各节点数据域存储数据的类型:typedef int DataType;//(左)孩子(右)兄弟表示法:struct TreeNode{//节点中的数据域:DataType data;//第一个孩子节点 -- 最左边的孩子节点:struct TreeNode* firstchild;//指向该节点下一个兄弟节点 -- 右边的兄弟节点:struct TreeNode* nextbrother;};

                

这个表示法本质是用链表实现树:
  • firstchild 指针 -- 相当于链表头指针找到当前节点下一层的头节点
                    
  • nextbrother 指针 -- 相当于链表遍历指针,可以往后遍历该层节点
                     
使用示例图:

               

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

2 . 树中的 二叉树(Binary Tree)

二叉树的概念及结构:

            

二叉树的概念

一棵二叉树节点的一个有限集合该集合满足

  • 或者为空
  • 或者由一个根节点加上两棵别称为左子树右子树二叉树组成

             

二叉树的结构

  • 二叉树不存在度大于2的节点
    ( 所以节点的度可能是 0 即空树,也可能是 1 或 2 )

                     
  • 二叉树的子树左右之分次序不能颠倒,因此二叉树是有序树
图示:

               

注意:

对于任意二叉树
都是由以下几种情况复合而成

                     

                    


                    

特殊的二叉树

                   

满二叉树

一个二叉树,如果每一个层的节点数都达到最大值,则这个二叉树就是满二叉树
也就是说,如果一个二叉树的高度 k ,且节点总数2^k - 1,则它就是满二叉树

图示:

                     

                     

完全二叉树

完全二叉树效率很高的数据结构,完全二叉树是满二叉树引出来的概念
对于高度 K 的,有 n 个节点二叉树
当且仅当每一个节点与高度为 K 满二叉树中编号从 1  n 的节点一一对应时,
称之为完全二叉树
          

简单理解:

假设树的高度K , K-1 "满"的
最后一层不一定满至少有一个节点),且树从左到右连续
该树即完全二叉树

                  

注:

满二叉树一种特殊的完全二叉树

完全二叉树的最后一层为满是满二叉树

所以假设完全二叉树的高度为 K ,
节点总数最高2^K - 1  ;节点总数最少(最后一层只有一个节点)是 2^(K - 1) 

                 

图示:

                     

                    


                    

二叉树的存储结构

                 

二叉树一般可以使用两种结构存储
一种顺序结构,一种链式结构

              

顺序结构

  • 顺序结构存储就是使用 数组 来存储
    任意位置通过下标可以找到父亲或者孩子,数组中下标为0的元素为根节点

                   
  • 使用数组进行存储一般只适合表示完全二叉树满二叉树),
    因为不是完全二叉树的话会有空间的浪费
                      
  • 现实使用中只有  才会使用数组来存储
                            
  • 二叉树顺序存储物理上一个数组,在逻辑上一颗二叉树
                        
顺序结构(数学)规律:
  • 通过父节点下标找到子节点下标):
    左节点  ==》  leftchild  =  parent * 2 + 1
    右节点  ==》  rightchild  =  parent * 2 + 2

                   
  • 通过子节点下标找到父节点下标):
    父节点  ==》 parent  = (child - 1) / 2
    通过观察可以发现左节点下标都是奇数右节点下标都是偶数
    (偶数-1) / 2 向下取整得到偶数父节点下标
    所以一个公式即可求得父节点
                   
  • 该规律只适用于完全二叉树,因为完全二叉树从左到右是连续的
    非完全二叉树也可以使用,但因为节点不一定是连续的
    所以数组中有些位置不存储元素,导致空间浪费
图示:

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

3 . 完全二叉树中的 堆(Heap)

堆的概念及结构

          

堆的概念

如果有一个关键码的集合  K = \left \{ k_{0}, k_{1}, k_{2}, ...,k_{n-1}\right \} ,
它的所有元素完全二叉树的顺序存储方式存储在一个一维数组并满足

  • _{}^{}K_{i} \leqslant K_{2*i+1}  K_{i} \leqslant K_{2*i+2}

    ( K_{i}\geqslant K_{2*i+1} K_{i}\geqslant K_{2*i+2}

    其中 i = 012……

称为小堆大堆
  • 根节点最大的堆叫做最大堆大根堆 -- 大堆
  • 根节点最小的堆叫做最小堆小根堆 -- 小堆

              

简单理解:

小堆 -- 各父节点值总是小于等于其子节点值各子节点值总是大于等于其父节点值
大堆 -- 各父节点值总是大于等于其子节点值各子节点值总是小于等于其父节点值

              

堆的结构

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

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

4 . 堆的实现

使用二叉树的顺序结构实现堆

  • 普通的二叉树不适合用数组顺序结构来存储,因为会存在大量的空间浪费
                 
  • 完全二叉树适合使用顺序结构存储
    所以现实中通常把一种完全二叉树使用顺序结构的数组来存储

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

(详细解释在图片的注释中,代码分文件放下一标题处)

                             

实现具体功能前的准备工作

  • 堆头文件中包含之后所需头文件
                  
  • 定义节点值(堆底层数组元素)的类型
                  
  • 定义堆类型
图示

            

            

---------------------------------------------------------------------------------------------

            

HeapInit函数 -- 对堆类型中的成员进行初始化

  • assert断言接收的堆类型指针不为空
                   
  • 堆根节点指针置为NULL
    堆当前节点个数置为0
    堆当前开辟的空间单位置为0
图示

            

            

---------------------------------------------------------------------------------------------

            

HeapDestroy函数 -- 对堆类型进行销毁

  • assert断言接收的堆类型指针不为空
                   
  • 释放堆类型中以a为头开辟的动态空间
                  
  • 堆当前节点个数堆当前开辟的空间单位置为0
图示

            

            

---------------------------------------------------------------------------------------------

            

HeapPrint函数 -- 打印堆中各节点值

  • assert断言接收的堆类型指针不为空
                   
  • 使用for循环循环打印
图示

            

            

---------------------------------------------------------------------------------------------

            

Swap函数 -- 在向上向下调整操作中互换节点位置

  • 向上调整AdjustUp内部函数)和向下调整AdjustDown内部函数
    推插入节点HeapPush函数)和堆删除节点HeapPop操作中的重要步骤
    向上调整操作向下调整操作需要用到Swap函数交换两节点值
                   
  • 该函数实现很简单,创建一个临时变量配合调换两节点值即可
图示

            

            

---------------------------------------------------------------------------------------------

            

AdjustUp内部函数
--
用于在插入节点后(HeapPush函数)向上调整该节点在堆中的位置

  • 通过上面顺序结构中讲到的公式
    通过子节点下标child找到其父节点下标parent
                   
  • 使用while循环进行向上调整
图示

            

            

---------------------------------------------------------------------------------------------

            

(难)HeapPush函数 -- 在堆类型中插入一个节点

  • assert断言接收的堆类型指针不为空
                   
  • 断言后要判断是否需要扩容需要则进行扩容操作检查是否扩容成功
                  
  • 空间足够后将插入节点放入堆
    插入后堆当前节点个数size++
                    
  • 插入节点后调用 向上调整AdjustUp内部函数
    对插入节点在堆中的位置进行调整
    使得插入后整体还是一个堆大堆或小堆
图示

            

            

---------------------------------------------------------------------------------------------

            

HeapInitArray函数 -- 接收一个数组将其初始化为一个堆底层数组

  • 实现向上调试AdjusuUp内部函数后,
    可以运用其来建堆建大堆还是小堆取决于AdjustUp函数内的条件设置),
    相比前面HeapInit初始化函数
    该函数会直接为堆开辟动态空间,并放入各节点值
                   
  • assert断言接收的堆类型指针不为空
    assert断言数组a不为空
                  
  • 开辟动态空间检查开辟情况
                    
  • 堆当前节点个数置为n
    堆当前开辟的空间单位置为n
                        
  • 数组a元素拷贝作为堆底层数组元素
                   
  • 循环向上调整堆中的节点底层数组元素进行建堆
图示

            

            

---------------------------------------------------------------------------------------------

            

AdjustDown内部函数
--
用于在删除根节点后(HeapPush函数)向下调整堆,使其还是一个堆

  • 通过上面顺序结构中讲到的公式
    通过父节点下标parent找到其左子节点下标child

                   
  • 使用while循环循环向下调整堆以小堆为例
                  
  • while循环中,先找出当前两个子节点中的较小子节点下标
                    
  • while循环中找到较小节点下标后
    判断是否需要向下调整执行相应操作
图示

            

            

---------------------------------------------------------------------------------------------

            

(难)HeapPop函数 -- 删除堆类型根节点(删除当前堆中最值)

  • assert断言接收的堆类型指针不为空
    assert断言堆节点个数底层数组元素个数不为0
                   
  • 使用交换节点Swap函数将根节点值和尾节点值交换
    将移至尾部的原根节点删除
                  
  • 使用向下调整AdjustDown内部函数对堆进行向下调整
图示

            

            

---------------------------------------------------------------------------------------------

            

HeapTop函数 -- 获取并返回推根节点值

  • assert断言接收的堆类型指针不为空
    assert断言堆节点个数底层数组元素个数不为0

                   
  • 直接返回根节点值
图示

            

            

---------------------------------------------------------------------------------------------

            

HeapEmpty函数 -- 判断堆类型是否为空

  • assert断言接收的堆类型指针不为空
                   
  • 如果堆节点个数底层数组元素个数为0则说明堆为空
图示

            

            

---------------------------------------------------------------------------------------------

            

总体测试:

            

            

---------------------------------------------------------------------------------------------

            

HeapSort排序函数(难)
--
使用堆中向下调整的操作对普通数组进行排序(升序或降序)

  • 建堆
    将要排序的数组看做一棵完全二叉树
    使用循环向下调整操作从倒数第一个父节点非叶子节点开始向下调整
    调整完成后数组就会符合堆的性质(这里以小堆为例)
    升序 -- 建大堆        ;        降序 -- 建小堆
                   
  • 排序
    使用while循环进行排序
    最小值小堆根节点)和尾元素进行交换

    除最小值的其它值看做堆,进行向下调整选出次大小值
    调整好最尾的一个值再循环调整下一个
图示

                

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                

5 . 对应代码

Heap.h -- 头文件

#pragma once//在堆头文件中包含之后所需头文件:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>//定义节点值(堆底层数组元素)的类型:
typedef int HPDataType;//定义堆类型:
typedef struct Heap
{//因为底层是用顺序结构实现,//所以该类型和顺序表类似://指向堆节点的指针://(指向堆底层数组的首元素)HPDataType* a;//堆当前节点个数://(堆底层数组的元素个数)int size;//堆当前开辟的空间单位://(堆底层数组动态开辟的空间单位)int capacity;}HP; //堆类型Heap重命名为HP//堆初始化函数1 -- 对堆类型中的成员进行初始化
//接收 堆类型指针(php)
void HeapInit(HP* php);//堆销毁函数 -- 对堆类型进行销毁
//接收 堆类型指针(php)
void HeapDestroy(HP* php);//打印堆函数 -- 打印堆中各节点值
//接收 堆类型指针(php)
void HeapPrint(HP* php);//节点位置互换函数 -- 在向上向下调整操作中互换节点位置:
//接收要互换位置的两个节点的指针(p1 和 p2)
void Swap(HPDataType* p1, HPDataType* p2);//堆插入函数 -- 在堆类型中插入一个节点
//接收 堆类型指针(php)和插入节点的值(x)
void HeapPush(HP* php, HPDataType x);//堆初始化函数2 -- 接收一个数组将其初始化为一个堆底层数组
//接收 堆类型指针(php)、数组首元素地址(a)、数组元素个数(n)
void HeapInitArray(HP* php, int* a, int n);//堆删除函数 -- 删除堆类型根节点(删除当前堆中最值)
//接收 堆类型指针(php)
void HeapPop(HP* php);//堆顶函数 -- 获取并返回推根节点值
//接收 堆类型指针(php)
HPDataType HeapTop(HP* php);//判空函数 -- 判断堆类型是否为空
//接收 堆类型指针(php)
bool HeapEmpty(HP* php);

            

            

---------------------------------------------------------------------------------------------

                

Heap.c -- 实现文件

#define _CRT_SECURE_NO_WARNINGS 1//包含堆头文件:
#include "Heap.h"//堆初始化函数1 -- 对堆类型中的成员进行初始化
// (一开始不给值,之后使用函数插入值形成堆)
//接收 堆类型指针(php)
void HeapInit(HP* php)
{//assert断言接收的堆类型指针不为空://(确保有成功创建堆类型可以被初始化)assert(php);//将堆根节点指针置为NULL:php->a = NULL;//将堆当前节点个数置为0:php->size = 0;//将堆当前开辟的空间单位置为0:php->capacity = 0;
}//堆销毁函数 -- 对堆类型进行销毁
//接收 堆类型指针(php)
void HeapDestroy(HP* php)
{//assert断言接收的堆类型指针不为空://确保有堆类型可以被销毁assert(php);//释放堆类型中以a为头开辟的动态空间:free(php->a);//将堆当前节点个数和堆当前开辟的空间单位置为0:php->a = NULL;php->size = php->capacity = 0;
}//打印堆函数 -- 打印堆中各节点值
//接收 堆类型指针(php)
void HeapPrint(HP* php)
{//assert断言接收的堆类型指针不为空://(确保有成功创建堆类型可以被打印)assert(php);//使用for循环循环打印:for (int i = 0; i < php->size; i++)//size有多少个节点就打印多少个节点值:{//通过下标打印节点值:printf("%d ", php->a[i]);}//换行:printf("\n");
}//节点位置互换函数 -- 在向上向下调整操作中互换节点位置:
//接收要互换位置的两个节点的指针(p1 和 p2)
void Swap(HPDataType* p1, HPDataType* p2)
{//创建一个临时变量配合调换两节点值:HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}//在内部实现一个不对外声明的向上调整函数AdjustUp
//第一个参数接收 堆底层数组的首元素地址
//第二个参数接收 刚刚插入数组元素(子节点)的下标
void AdjustUp(HPDataType* a, int child)
{//使用之前讲到的公式,//通过子节点下标找到其父节点下标:int parent = (child - 1) / 2;//只要child还大于0就继续向上调整//需要向上层调整的话,上层节点的下标是比较小的//所以child不断调整就会越来越小while (child > 0){//小堆中的向上调整:if (a[child] < a[parent])//小堆中子节点值比父节点值还小://大堆中则把条件改为://if (a[child] > a[parent])  //大堆的向上调整{//那就需要两者调换位置:Swap(&a[child], &a[parent]); //值替换成功后,下标也要进行调整://child移到上层后,下标为parent下标:child = parent;//child移到该层后,还可能是再上层父节点的子节点//可能还需要再向上调整,最坏的情况是移到成为堆的根节点://再获得新父节点的下标:parent = (parent - 1) / 2;}else//如果小堆中子节点值已经大于等于父节点值了//即符合小堆的条件了{//那就不用进行向上调整,break退出循环:break;}}
}//堆插入函数 -- 在堆类型中插入一个节点
//接收 堆类型指针(php)和插入节点的值(x)
void HeapPush(HP* php, HPDataType x)
{//assert断言接收的堆类型指针不为空://确保有堆类型可以插入节点assert(php);//断言后要检查是否需要扩容:if (php->size == php->capacity)//堆当前节点个数 == 开辟空间单位//说明插入节点前需先进行扩容:{//创建变量存储新容量单位:int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;//因为capacity初始化为0,所以可以使用三目操作符进行增容://ps->capacity == 0 ? 4 : ps->capacity * 2//如果为0则直接增容到4,不为0则增容2倍//开辟动态空间:HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);//这里直接使用realloc函数进行动态空间开辟//如果realloc函数接收头指针(该函数第一个参数)为空,//那么它的作用相当于malloc函数//对开辟空间进行检查:if (tmp == NULL)//返回空指针,开辟失败:{//打印错误信息:perror("realloc fail");//终止程序:exit(-1);}//成功开辟空间后,将开辟空间的头指针赋给a:php->a = tmp;//将新容量单位赋给原容量单位:php->capacity = newCapacity;}//在堆中进行插入操作://堆:
//物理结构是个数组;逻辑结构是个树
//树就没有头插和尾插的概念了
//节点具体插入哪个位置要看情况
//只要插入值足够大或足够小,就可以一直往上一层替换
//节点插入堆后要保持还是一个堆,是从左到右连续的树//不论什么情况,先默认插入最尾部://(即堆底层数组的末尾)//因为数组下标从0开始,//所以size即末尾插入节点下标:php->a[php->size] = x;//插入后堆当前节点个数size++:php->size++;//此时如果插入节点足够大(或足够小),//则刚好满足大堆(或小堆)的条件,//但如果不满足,就要将插入节点向树的上层调整,//且之后还会再执行该操作,//所以这里可以在文件内部定义一个向上调整函数AdjustUp//并进行调用:AdjustUp(php->a, php->size-1);//第一个参数接收 堆底层数组的首元素地址//第二个参数接收 刚刚插入数组元素(子节点)的下标// php->size-1 -- 前面插入节点后size++了// 所以这里的下标应-1 ,得到插入元素下标
}//堆初始化函数2 -- 接收一个数组将其初始化为一个堆底层数组
// (给你底层数组的各个值,再向上调整形成堆)
//接收 堆类型指针(php)、数组首元素地址(a)、数组元素个数(n)
void HeapInitArray(HP* php, int* a, int n)
{//assert断言断言接收的堆类型指针不为空://(确保有成功创建堆类型可以被初始化)assert(php);//assert断言数组a不为空:assert(a);//开辟动态空间:php->a = (HPDataType*)malloc(sizeof(HPDataType*) * n);//对开辟空间进行检查:if (php->a == NULL)//返回空指针,开辟失败:{//打印错误信息:perror("realloc fail");//终止程序:exit(-1);}//将堆当前节点个数置为n:php->size = n;//将堆当前开辟的空间单位置为n:php->capacity = n;//将数组元素拷贝作为堆底层数组元素:memcpy(php->a, a, sizeof(HPDataType) * n);//再循环向上调整堆中的节点(底层数组元素)进行建堆 :for (int i = 1; i < n; i++){AdjustUp(php->a, i);}
}//在内部实现一个不对外声明的向下调整函数AdjustDown
//第一个参数接收 堆底层数组的首元素地址
//第二个参数接收 堆节点个数(底层数组元素个数)
//第三个参数接收 调换位置后的根节点(原尾节点)下标
void AdjustDown(HPDataType* a, int n, int parent)
{//(小堆中)向下调整需要获得比父节点parent小的子节点:// (该子节点要和其父节点调换位置)//可以先假设左子节点是比父节点小的://获得根节点的左子节点(第一个子节点)下标:int child = parent * 2 + 1;//最坏情况:根节点成为叶子(没有子节点可以替换了)while (child < n)//左子节点下标如果>=堆节点个数(底层数组元素个数)//说明已经超出了数组范围,此时父节点已成为叶子无法再调换{//如果我们上面假设是错的:if (child + 1 < n  &&  a[child + 1] < a[child])//大堆中则把条件改为://if (child + 1 < n  &&  a[child + 1] > a[child]) //大堆的向下调整// child+1 即右子节点的下标/*	左右子节点都存在才用比较出较小节点:	防止不存在的右子节点越界:child + 1 < n上面只要左子节点在数组就能进入while循环而左子节点可能就是数组最后一个元素了可能会出现左子节点存在而右子节点不存在的情况所以要在右子节点存在的情况下比较出较小节点/没有右子节点直接使用左子节点。*///如果右子节点比左子节点小:{//就要获得右子节点(较小子节点)下标:++child; //自增后即右子节点下标}//到这里就获得较小节点的下标://(不关心是左子节点还是右子节点,只要值小的节点)//(小堆中)如果较小节点比父节点还小if (a[child] < a[parent])//大堆中则把条件改为://if (a[child] > a[parent]) //大堆的向下调整{//那就需要调换两者位置:Swap(&a[child], &a[parent]);//调换两节点值后,下标也要刷新:parent = child;//此时新的子节点下标为:child = parent * 2 + 1;}else//如果较小节点已经比父节点大了:{//那就符合小堆的条件,没必要调换:break; //退出循环}}
}//堆删除函数 -- 删除堆类型根节点(删除当前堆中最值)
//接收 堆类型指针(php)
void HeapPop(HP* php)
{//assert断言接收的堆类型指针不为空://确保有堆类型可以删除根节点assert(php);//assert断言堆节点个数(底层数组元素个数)不为0//确保有节点可以删除:assert(php->size > 0);/*如果进行挪动覆盖第一个位置的根节点会导致剩下的节点关系乱套(兄弟变父子)最终结果可能不一定是堆,而且数组中挪动覆盖效率本来就不高所以可以将根节点和尾节点交换位置再进行尾删,就实现了删除根节点且数组中尾删效率较高,*///将根节点值和尾节点值交换:Swap(&php->a[0], &php->a[php->size - 1]);//	&php->a[0] :根节点值//	&php->a[php->size - 1] :尾结点值//再将移至尾部的原根节点删除:--php->size; //直接元素个数size-1即可/*此时堆的左右子树的结构没有变化左右子树依然是小堆(大堆)有了这个前提就可以进行向下调整:因为前面根节点和尾节点交换位置所以现在的根节点(原尾结点)可能就要比起子节点大了树整体不符合小堆条件,所以要进行向下调整(注:向上调整的前提是前面节点符合堆的条件)向下调整操作:成为根节点后,跟其两个子节点比较如果子节点比较小,则互换位置互换位置后再跟下一层子节点比较如果子节点比较小,则互换位置以此类推,最坏情况是到成为叶子为止(小堆中大的值往下移,小的值往上走)*///所以这里可以在文件内部定义一个向下调整函数AdjustDown//并进行调用:AdjustDown(php->a, php->size, 0);//根节点在底层数组中下标是0/** 该函数的意义和示例:该函数的意义是删除第一小的值(小堆中)再找到第二小的值,以此类推,可以依次找到数组的最小值示例:“该城市排名前10的蛋糕店” 小堆根元素就第一好的店,使用该函数后找到第二好的店,以此类推……*//** 该函数时间复杂度:向下或向上调整操作最好情况执行1次,最坏情况执行树的高度次即时间复杂度为 O(logN),10亿个值只需调整30多次 -- 很优秀( log -- 默认以2为底 )*/}//堆顶函数 -- 获取并返回推根节点值
//接收 堆类型指针(php)
HPDataType HeapTop(HP* php)
{//assert断言接收的堆类型指针不为空://确保有堆类型可以获取根节点值assert(php);//assert断言堆节点个数(底层数组元素个数)不为0//确保堆类型中有节点:assert(php->size > 0);//直接返回根节点值:return php->a[0];
}//判空函数 -- 判断堆类型是否为空
//接收 堆类型指针(php)
bool HeapEmpty(HP* php)
{//assert断言接收的堆类型指针不为空://确保有堆类型可以获取根节点值assert(php);//如果堆节点个数(底层数组元素个数)为0则说明堆为空:return php->size == 0;//php->size = 0 -- 成立返回true,堆为空//php->size = 0 -- 不成立返回false,堆不为空 
}

            

            

---------------------------------------------------------------------------------------------

                

Test.c -- 测试文件

#define _CRT_SECURE_NO_WARNINGS 1//包含堆头文件:
#include "Heap.h"//测试函数 -- 未排序
void Test0()
{//创建一个数组创建要放入堆中的各节点值int a[] = { 65,100,70,32,50,60 };//计算数组长度:int alength = sizeof(a) / sizeof(int);//创建堆类型变量:HP hp;//使用HeapInit函数进行初始化:HeapInit(&hp);//使用for循环将数组各值放入堆类型变量中:for (int i = 0; i < alength; i++){//堆类型变量hp在还没有HeapPush之前只是完全二叉树//在使用HeapPush函数插入并排序后就成了堆,//		所以建堆的方式之一就是一直push://使用HeapPush函数插入节点:HeapPush(&hp, a[i]);}printf("当前堆:> ");//使用HeapPrint函数打印堆:HeapPrint(&hp);//使用HeapEmpty函数判断堆是否为空:while (!HeapEmpty(&hp))//不为空就获取堆根节点值:{//使用HeapTop函数获取堆根节点值并打印:printf("当前堆根节点值:> %d\n", HeapTop(&hp));//(小堆中)使用HeapPop函数删除当前根节点,// 并将第二小的节点设置为新的根节点:HeapPop(&hp);}//使用HeapDestroy函数进行销毁:HeapDestroy(&hp);
}测试函数 -- 进行排序
//
堆排序函数(优化前) -- 利用堆类型对普通数组进行排序(升序或降序)
第一个参数:	接收堆底层数组的首元素地址
第二个参数:	堆底层数组的元素个数
//void HeapSort(int* a, int n)
//{
//	//创建堆类型变量:
//	HP hp;
//
//	//使用HeapInit函数进行初始化:
//	HeapInit(&hp);
//
//	//使用for循环将数组各值放入堆类型变量中:
//	for (int i = 0; i < n; i++)
//	{
//		//堆类型变量hp在还没有HeapPush之前只是完全二叉树
//		//在使用HeapPush函数插入并排序后就成了堆,
//		//		所以建堆的方式之一就是一直push:
//
//		//使用HeapPush函数插入节点:
//		HeapPush(&hp, a[i]);
//	}
//
//	//printf("当前堆:> ");
//	使用HeapPrint函数打印堆:
//	//HeapPrint(&hp);
//
//	int i = 0;
//	//使用HeapEmpty函数判断堆是否为空:
//	while (!HeapEmpty(&hp))
//		//不为空就获取堆根节点值:
//	{
//		//(小堆中)依次将堆类型的根节点值赋给数组a,
//		//运用堆实现将数组从小到大排序:
//		a[i++] = HeapTop(&hp);
//
//		//(小堆中)使用HeapPop函数删除当前根节点,
//		// 并将第二小的节点设置为新的根节点:
//		HeapPop(&hp);
//	}
//
//	//使用HeapDestroy函数进行销毁:
//	HeapDestroy(&hp);
//}这种排序的缺陷:1. 得先有一个堆的数据结构2. 空间复杂度的消耗
//void Test1()
//{
//	//创建一个堆底层数组 -- 节点值随机;未进行排序
//	int a[] = { 65,100,70,32,50,60 };
//
//	//计算数组长度:
//	int alength = sizeof(a) / sizeof(int);
//
//	printf("运用堆排序前的数组:> ");
//	//运用堆排序后前的数组:
//	for (int i = 0; i < alength; i++)
//	{
//		printf("%d ", a[i]);
//	}
//	printf("\n");
//	
//	//使用HeapSort函数对未进行排序的堆底层数组进行排序:
//	HeapSort(a, alength);
//
//	printf("运用堆排序后的数组:> ");
//	//运用堆排序后的数组:
//	for (int i = 0; i < alength; i++)
//	{
//		printf("%d ", a[i]);
//	}
//
//}测试函数 -- 进行排序
//
堆排序函数(优化后) -- 利用堆类型的思路对普通数组进行排序(升序或降序)
第一个参数:	接收堆底层数组的首元素地址
第二个参数:	堆底层数组的元素个数
//void HeapSort(int* a, int n)
//{	
//	//之前是使用HeapPush函数创建堆类型后,
//	//再运用到普通数组的排序中,
//	//那我们也可以不用堆,
//	//直接使用HeapPush函数的思路将普通数组建成堆:
//	
//  //向上调整建堆 -- 时间复杂度 -- O(N*logN)
//	//将数组看做完全二叉树,再进行建堆:
//	//在AdjustUp中可设置为是大堆调整还是小堆调整
//	for (int i = 1; i < n; i++)
//		//从数组第二个元素(下标为1)开始排序,第一个元素默认为根节点,
//		//使用AdjustUp函数后再进行进一步调整:
//	{
//		//之前是在堆类型的操作中使用了AdjustUp调整函数
//		//这里在普通数组上也可以使用AdjustUp调整函数
//		//只是堆类型需要扩容,而普通数组不需要扩容而已
//		//把普通数组按大堆或小堆的方式进行调整:
//		AdjustUp(a, i);
//	}
//
//	//现在首元素就相当于(小堆)根节点,即最小值
//	//要想再选出次小的值,只能把剩下的数据看做堆
//	//但是父子关系全乱了,剩下数据也不一定还是堆
//	//可以再重新建堆,但是代价太大了
//
//	//还有一个思路:升序(从小到大)用大堆
//	//把数组建成大堆,把最大值选出来后,把最大值和最尾值交换
//	//这样升序中(从小到大)也排好了一个值,即最大值放尾部
//	//再把除最大值的其它值看做堆,进行向下调整,选出次大值
//	//放倒数第二个位置,循环这套操作 -- 时间复杂度NlogN
//	//(同样的道理,如果是降序(从大到小)就用小堆)
//
//	//以降序(从大到小)用小堆为例:
//
//	//先获得数组尾元素下标:
//	int end = n - 1;
//	//使用while循环进行排序:
//	while (end > 0)
//		//数组尾元素下标大于说明至少还有两个元素,继续排序:
//	{
//		//把最小值(小堆根节点)和尾元素进行交换:
//		Swap(&a[0], &a[end]);
//
//		//把除最小值的其它值看做堆,进行向下调整,选出次大小值:
//		AdjustDown(a, end, 0);
//		//end即是除最小值的其它值(数组前n-1个值)
//
//		//这样就调整好了最尾的一个值,end-1循环调整下一个:
//		end--;
//	}
//  
//}
//
//void Test2()
//{
//	//创建一个堆底层数组 -- 节点值随机;未进行排序
//	//int a[] = { 75,65,100,32,50,60 };
//	int a[] = { 2,3,5,7,4,6,8 };
//
//	//计算数组长度:
//	int alength = sizeof(a) / sizeof(int);
//
//	printf("运用堆排序前的数组:> ");
//	//运用堆排序后前的数组:
//	for (int i = 0; i < alength; i++)
//	{
//		printf("%d ", a[i]);
//	}
//	printf("\n");
//
//	//使用HeapSort函数对未进行排序的堆底层数组进行排序:
//	HeapSort(a, alength);
//
//	printf("运用堆排序后的数组:> ");
//	//运用堆排序后的数组:
//	for (int i = 0; i < alength; i++)
//	{
//		printf("%d ", a[i]);
//	}
//
//}//测试函数 -- 进行排序
//堆排序函数(再再优化) -- 只用向下调整对普通数组进行排序(升序或降序)
//第一个参数:	接收堆底层数组的首元素地址
//第二个参数:	堆底层数组的元素个数
void HeapSort(int* a, int n)
{//向下调整建堆 -- 时间复杂度:O(N)//将要排序的数组看做一棵完全二叉树//从倒数第一个父节点(非叶子节点)开始向下调整:for (int i = (n-1-1)/2; i >= 0; i--)//(n-1-1)/2 -- n-1是最后叶子的下标,该叶子-1再/2(公式)//找到其父节点(倒数第一个父节点),调整好该父节点后i--//再调整上一父节点,直到向下调整到根节点{AdjustDown(a, n, i); //从i位置开始向下调整}//建堆最坏情况下执行次数:T(N) = N - log(N+1)//因此:建堆的时间复杂度为O(N)//开始排序:以降序(从大到小)用小堆为例://先获得数组尾元素下标:int end = n - 1;//使用while循环进行排序:while (end > 0)//数组尾元素下标大于说明至少还有两个元素,继续排序:{//把最小值(小堆根节点)和尾元素进行交换:Swap(&a[0], &a[end]);//把除最小值的其它值看做堆,进行向下调整,选出次大小值:AdjustDown(a, end, 0);//end即是除最小值的其它值(数组前n-1个值)//这样就调整好了最尾的一个值,end-1循环调整下一个:end--;}}void Test3()
{//创建一个堆底层数组 -- 节点值随机;未进行排序//int a[] = { 75,65,100,32,50,60 };int a[] = { 2,3,5,7,4,6,8 };//计算数组长度:int alength = sizeof(a) / sizeof(int);printf("运用堆排序前的数组:> ");//运用堆排序后前的数组:for (int i = 0; i < alength; i++){printf("%d ", a[i]);}printf("\n");//使用HeapSort函数对未进行排序的堆底层数组进行排序:HeapSort(a, alength);printf("运用堆排序后的数组:> ");//运用堆排序后的数组:for (int i = 0; i < alength; i++){printf("%d ", a[i]);}}//主函数:
int main() 
{//Test0();//Test1();//Test2();Test3();return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/153225.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【计算机网络】高级IO初步理解

文章目录 1. 什么是IO&#xff1f;什么是高效 IO? 2. IO的五种模型五种IO模型的概念理解同步IO与异步IO整体理解 3. 阻塞IO4. 非阻塞IOsetnonblock函数为什么非阻塞IO会读取错误&#xff1f;对错误码的进一步判断检测数据没有就绪时&#xff0c;返回做一些其他事情完整代码myt…

黑马JVM总结(二十九)

&#xff08;1&#xff09;语法糖-重写桥接 &#xff08;2&#xff09;语法糖-匿名内部类 &#xff08;3&#xff09;类加载-加载 类加载可以分为3个阶段&#xff0c;加载、连接、初始化 我们知道java类编译成字节码以后&#xff0c;运行呢需要类加载器把类的字节码加载到方法…

git的基本使用

git地址 https://git-scm.com/ git首次安装必须设置签名代码&#xff0c;否则无法提交代码 git init git status 14 idea中连接gitee file-setting中安装gitee插件&#xff0c;安装gitee插件后可以在version control中看到gitee&#xff0c;点击 添加gitee账号&#xff…

Kafka和RabbitMQ的对比

Rabbitmq比kafka可靠&#xff0c;kafka更适合IO高吞吐的处理&#xff0c;比如ELK日志收集 Kafka和RabbitMq一样是通用意图消息代理&#xff0c;他们都是以分布式部署为目的。但是他们对消息语义模型的定义的假设是非常不同的。 a) 以下场景比较适合使用Kafka。如果有大量的事…

聊聊MySQL的聚簇索引和非聚簇索引

文章目录 1. 索引的分类1. 存储结构维度2. 功能维度3. 列数维度4. 存储方式维度5. 更新方式维度 2. 聚簇索引2.1 什么是聚簇索引2.2 聚簇索引的工作原理 3. 非聚簇索引&#xff08;MySQL官方文档称为Secondary Indexes&#xff09;3.1 什么是非聚簇索引3.2 非聚簇索引的工作原理…

Win10系统打开组策略编辑器的两种方法

组策略编辑器是Win10电脑中很实用的工具&#xff0c;它可以帮助用户管理和设置计算机的安全性、网络连接、软件安装等各种策略。但是&#xff0c;很多新手用户不知道打开Win10电脑中组策略编辑器的方法步骤&#xff0c;下面小编给大家介绍两种简单的方法&#xff0c;帮助打开快…

Gitlab+Jenkins自动化部署,解放双手

项目打包 ​ 在部署项目前需要对源码进行打包&#xff0c;一个简单的SpringBoot项目默认是打包为jar包&#xff0c;也就是在pom.xml中的<packaging>jar</packaging>方式&#xff0c;当然也会有一些打包成war包方式&#xff0c;使用外置的Tomcat应用服务器部署war包…

Python装饰器(一次搞清楚)

最重要的情绪管理是要明白&#xff0c;没有一种情绪是不应该的 一、简单装饰器 Python装饰器是一种语法糖&#xff0c;用于在不改变原有函数代码的情况下&#xff0c;为函数添加额外的功能。装饰器本质上是一个函数&#xff0c;它接收一个函数作为参数&#xff0c;并返回一个新…

【python】exec()内置函数释义

【python】exec内置函数释义 官方释义样例注意事项拓展感谢及参考博文 官方释义 官方Python API文档镇楼 exec(object, globalsNone, localsNone, /, *, closureNone) 支持动态执行 Python 代码&#xff0c; object 必须是字符串或者代码对象。 需要特别注意以下两点&#xff…

css自学框架之面板

面板是我们开发中经常用到&#xff0c;也就是页面版面中一块一块的板块&#xff0c;效果如下图&#xff1a; 一、css代码 .myth-panel {background-color: var(--white);border: solid 1px transparent;}.myth-panel .myth-panel-header {border-bottom: solid 1px transpar…

Play Beyond:Sui让优秀的游戏变得更好

自问世以来&#xff0c;视频游戏就紧随着文化产业发展。从Pong和Space Invaders的时代到Animal Crossing和Among Us&#xff0c;伟大的游戏总有能力吸引玩家&#xff0c;并推动娱乐产业发展。根据Grand View Research的数据&#xff0c;全球视频游戏市场在2022年估计为2170.6亿…

JavaScript进阶 第一天笔记

JavaScript 进阶 - 第1天 学习作用域、变量提升、闭包等语言特征&#xff0c;加深对 JavaScript 的理解&#xff0c;掌握变量赋值、函数声明的简洁语法&#xff0c;降低代码的冗余度。 理解作用域对程序执行的影响能够分析程序执行的作用域范围理解闭包本质&#xff0c;利用闭包…

核货宝:服装店收银系统如何选择?收银系统选购指南!

对于各行各业而言&#xff0c;收银系统都是必备的工具。特别是对于像服装店这样的零售门店来说&#xff0c;选择一套适合的收银系统尤为重要。在选择收银系统时&#xff0c;有一些关键的技巧需要注意&#xff0c;以达到软硬件合理搭配、节省开支的目的。下面将分享四个选购服装…

《视觉 SLAM 十四讲》第 7 讲 视觉里程计1 【如何根据图像 估计 相机运动】【特征点法】

github源码链接V2 文章目录 第 7 讲 视觉里程计17.1 特征点法7.1.1 特征点7.1.2 ORB 特征FAST 关键点 ⟹ \Longrightarrow ⟹ Oriented FASTBRIEF 描述子 7.1.3 特征匹配 7.2 实践 【Code】本讲 CMakeLists.txt 7.2.1 使用 OpenCV 进行 ORB 的特征匹配 【Code】7.2.2 手写 O…

智慧茶园:茶厂茶园监管可视化视频管理系统解决方案

一、方案背景 我国是茶叶生产大国&#xff0c;茶叶销量全世界第一。随着经济社会的发展和人民生活水平的提高&#xff0c;对健康、天然的茶叶产品的消费需求量也在逐步提高。茶叶的种植、生产和制作过程工序复杂&#xff0c;伴随着人力成本的上升&#xff0c;传统茶厂的运营及…

怒刷LeetCode的第25天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;闭合为环 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;动态规划 方法二&#xff1a;组合数学 方法三&#xff1a;递归 方法四&#xff1a;数学公式 第三题 题目来源 题目内容 解决方法 …

TCP原理特性详解

文章目录 可靠传输机制1.确认应答2.超时重传2.连接管理1.三次握手2.四次挥手 传输效率1.滑动窗口2.流量控制3.拥塞控制4.延时应答5.捎带应答 面向字节流粘包问题 TCP异常情况 可靠传输机制 可靠性&#xff1a;即发送方知道数据是发送成功了&#xff0c;还是失败了。 1.确认应答…

如何精细化管理嵌入式软件项目?ACT汽车电子与软件技术周演讲回顾

2023 ACT汽车电子与软件技术周已于8月18日在中国上海落下帷幕。展会现场&#xff0c;龙智技术支持部负责人、Atlassian认证专家叶燕秀与龙智技术工程师邱洁玉共同为观众带来了主题为“更好、更快、更安全&#xff1a;嵌入式开发中的最佳实践与工具链构建”的演讲&#xff0c;分…

elasticsearch内存占用详细分析

内存占用 ES的JVM heap按使用场景分为可GC部分和常驻部分。 可GC部分内存会随着GC操作而被回收&#xff1b; 常驻部分不会被GC&#xff0c;通常使用LRU策略来进行淘汰&#xff1b; 内存占用情况如下图&#xff1a; common space 包括了indexing buffer和其他ES运行需要的clas…

想要精通算法和SQL的成长之路 - 恢复二叉搜索树和有序链表转换二叉搜索树

想要精通算法和SQL的成长之路 - 恢复二叉搜索树和有序链表转换二叉搜索树 前言一. 恢复二叉搜索树二. 有序链表转换二叉搜索树 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 恢复二叉搜索树 原题链接 首先&#xff0c;一个正常地二叉搜索树在中序遍历下&#xff0c;遍历…