目录
一,树的概念和结构
1,树的概念
树是一个非线性的数据结构 ,由n(n>=0)个有限节点组成的具有层次关系的集合,且满足:
1,有且只有一个根节点
2,除根节点外,其他节点有且只有一个父亲节点
3,从根节点到任意一个节点只有一条路径。
4,子树之间不能有交集
任何一棵树都是由根节点和N颗子树构成的,因此可以说树是递归定义的。

2,树的相关概念
结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的为6
叶结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等结点为叶结点
非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等结点为分支结点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点
树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为6
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
树的高度或深度:树中结点的最大层次; 如上图:树的高度为4
堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点
结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;
3,树的表示
保存一个树,既要保存树的值域,又要保存该节点与其他节点的关系 ,而一个节点可能和许多节点产生连接所以不能采用之前链表的那样只使用一个或两个指针,这里我们使用孩子兄弟表示法,进行表示。


二,二叉树的概念和结构
1,二叉树的概念
二叉树是一种树形数据结构 ,其中每个节点至多有两个节点,分别为左子节点 和右子节点。
根节点的左子树和右子树

对于任意的二叉树都是有以下结构组合而成的 :

2,特殊的二叉树
1,满二叉树:一个二叉树,如果树的每一层的节点都达到最大值,则这个树就是满二叉树。
或者说如果一个树有k层,则如果树的结点总数是(2^k) -1,那么这棵树就是满二叉树。
2,完全二叉树:一个二叉树,除了最后一层的节点没有达到最大值,其他层的节点数目均达到最大数,并且最后一层的节点,均是从左到右依次排放的,不能有空缺。
注意:满二叉树其实是一种特殊的完全二叉树。


3,二叉树的性质
1,如果规定根节点所在层为第一层,一颗二叉树有i层,则第i层上最多有2^(i-1)个节点。
2,如果规定根节点所在层为第一层,一颗二叉树有i层,则这颗二叉树最多有(2^i)-1个节点。
3,对于任意一个二叉树,如果度为0的节点(叶子节点)的个数为n0,度为2的节点(分支节点)的个数为n2,则n0 = n2+1。
证明:
4,如果规定根节点所在层为第一层,具有n个节点的满二叉树的深度为log(N+1). (以二为底,N+1的对数)。
5,对于一个具有n个节点的完全二叉树,如果从上到下从左到右从0开始依次对每个结点进行编号,则对于序号为i的结点:
1,i编号为0的结点为根结点,没有双亲结点,若i>0, 则i的双亲结点为:(i-1)/2
2,若(2*i+1) <n,则为左子结点,若(2*i+1)>n,则没有左子结点。
3,若(2*i+2) <n,则为右子结点,若(2*i+2)>n,则没有右子结点。
4,二叉树的存储结构
二叉树的存储可以使用两种结构:一种是顺序结构,另一种是链式结构。
1,顺序存储
顺序存储一般就是使用数组进行存储 ,一般只适用于完全二叉树 ,如果不是完全二叉树就会有空间的浪费**,顺序存储在物理结构上是一个数组,在逻辑结构上是一个二叉树。**


2,链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。


三,堆的概念
堆是一种特殊的完全二叉树,其分为两种 :
大堆:每个节点的值都大于或等于其子节点的值(堆顶为最大值)
小堆:每个节点的值都小于或等于其子节点的值(堆顶为最小值)
注意:这里的堆以及之前提到的栈都是一种数据结构,而在内存区域划分中有三种区域:堆区,栈区,静态区。这两种概念是完全不同的,不要混淆。

四,堆的实现
1,堆基本功能的代码实现
代码链接:https://gitee.com/codelsj-w/test.4.16.c.git
头文件:函数声明 heap.h
cpp
#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;
//堆的初始化
void HPInit(HP* php);
//堆的销毁
void HPDestory(HP* php);
//交换元素
void HPswap(HPDataType* p1, HPDataType* p2);
//向上调整算法--小堆
void AdjustUp(HPDataType* a, int child);
//数据的插入--小堆
void HPPush(HP* php, HPDataType x);
//向下调整算法--小堆
void Adjustdown(HPDataType* a, int n, int parent);
//删除元素--删除栈顶元素
void HPPop(HP* php);
//取出堆顶元素
HPDataType HPTop(HP* php);
//对堆判空
bool HPEmpty(HP* php);
函数的实现:heap.c

cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "heap.h"
//堆的初始化
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
//堆的销毁
void HPDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
//交换元素
void HPswap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向上调整算法--小堆
//这里不适用php,进行操作,为了方便后续进行建堆
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0) //调整的最坏的情况是child节点为根节点
{
//进行交换
if (a[child] < a[parent])
{
HPswap(&a[child], &a[parent]);
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 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
php->a = tmp;
php->capacity = newcapacity;
}
//插入元素
php->a[php->size] = x;
php->size++;
//向上调整算法
AdjustUp(php->a, php->size - 1);
}
//向下调整算法--小堆
void Adjustdown(HPDataType* a, int n, int parent)
{
//先假设较小的孩子是左孩子
int child = parent * 2 + 1;
while (child < n) //当child>= n,就是越界访问,退出循环
{
//计算最小的孩子
if (child + 1 < n && a[child + 1] < a[child]) //防止child+1越界访问
{
//变成右孩子
child++;
}
//进行交换
if (a[parent] > a[child])
{
HPswap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//删除元素--删除栈顶元素
//可以找出最小的元素以及次小的元素,时间复杂度为logN
void HPPop(HP* php)
{
//判空
assert(php);
assert(php->size > 0);
//先交换第一个和最后一个数据
HPswap(&php->a[0], &php->a[php->size - 1]);
//删除最后一个数据
php->size--;
//对第一个元素进行向下调整算法
Adjustdown(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;
}
函数测试:test.c
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "heap.h"
//小堆的实现
void HPtest()
{
//对堆进行初始化
HP hp;
HPInit(&hp);
int arr[] = { 3,4,6,83,32,63,2,7,9,113 };
int sz = sizeof(arr) / sizeof(arr[0]);
//将数组中的元素插入到堆当中
for (int i = 0; i < sz; i++)
{
HPPush(&hp, arr[i]);
}
//将数据按照从小到大进行排序
//while (!HPEmpty(&hp))
//{
// printf("%d ", HPTop(&hp));
// HPPop(&hp);
//}
//printf("\n");
//从小到大取出最小的n个元素
int n = 0;
scanf("%d", &n);
while (n--)
{
printf("%d ", HPTop(&hp));
HPPop(&hp);
}
printf("\n");
HPDestory(&hp);
}
int main()
{
HPtest();
return 0;
}
2,堆排序
堆排序是基于数据结构堆的一种高效的排序方法。
堆排序可以分为两步:1,建队 2,排序、
下面给一个例子:
给定一个数组 a = {8,9,7,2,5,4,1,6}
要求:将数组按照从小到大的顺序进行排序
第一步:建小堆:

方法一:向上调整建小堆
向上调整只需要上方的树都是小堆即可,将数组中的元素从左到右进行向上调整,也就是从树的根节点开始进行调整,这样可以保证每要调整一个节点时,该节点上面的节点都有序。
分析时间复杂度:


方法二:向下调整建小堆
先找到倒数第一个分支节点,进行向下调整,再不断向前找元素进行调整,直到遍历到根节点。

分析时间复杂度:

cpp
//堆排序
void heapsort(int* a,int n)
{
//升序,建大堆
//降序,建小堆
//降序-向上调整算法建小堆
//for (int i = 1; i < n; i++)
//{
// AdjustUp(a, i);
//}
//降序-向下调整算法建小堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
Adjustdown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
HPswap(&a[0], &a[end]);
Adjustdown(a, end, 0);
end--;
}
}
void HPhead2()
{
int ar[] = { 5,6,8,3,2,7,1,4,9 };
int sz = sizeof(ar) / sizeof(ar[0]);
heapsort(ar, sz);
}
int main()
{
//HPtest();
HPhead2();
return 0;
}