目录
一.树的概念
注意,树的同一层中不能有关联,否侧就不是树了,就变成图了,例如:
由于B和C存在关联,这就不是树了。
有关树的一些重要概念:
二、二叉树
1.二叉树的概念
二叉树由一个根节点加上两棵别称为左子树和右子树的二叉树组成
它具有两个特点:不存在度大于2的节点;子树有左右之分,次序不能颠倒,故二叉树是有序树
各类型二叉树集合:
2.特殊类型的二叉树
满二叉树:所有非叶子节点都有两个子节点,且所有叶子节点都在同一层
完全二叉树:除了最后一层,每一层都是满的,且最后一层的叶子节点都尽可能靠左排列
二叉排序树:左子树所有节点的值小于根节点的值,右子树所有节点的值大于根节点的值,且左右子树也分别是二叉排序树
3.二叉树的性质
(1) 对于具有n个节点的完全二叉树,如果按照从上到下从左到右的顺序对所有节点从0开始编号,则序号为i的节点有:其双亲节点序号为(i-1)/2;其左孩子节点序号为i*2+1,右孩子节点序号为i*2+2,注意i*2+1和i*2+2要小于n,合法性
(2)规定根节点层数为1,具有n个节点的满二叉树深度为h=log2(n+1)
(3)规定根节点层数为1,第i层最大节点数为2^(i-1)
(4)规定根节点层数为1,深度为h的二叉树的最大节点数为2^h-1
4.二叉树存储的结构
分为顺序存储结构和链式存储结构,其中用顺序存储结构来实现完全二叉树就是接下来重点介绍的堆。
链式存储:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
顺序存储:顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺****序存储在物理上是一个数组,在逻辑上是一颗二叉树。
三、堆
1.堆的概念
堆可以看作一种特殊类型的完全二叉树,分为大堆和小堆,根节点最大的称为大堆,根节点最小的称为小堆。
大堆:谁大谁当爹,但兄弟间无绝对大小关系
小堆:谁小谁当爹,但兄第间无绝对大小关系
2.堆的实现
升序:建大堆
降序:建小堆
接下来给出建小堆的代码实现:
Heap.h
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<stdbool.h>
typedef int HDataType;
typedef struct Heap
{
HDataType* arr;
int size;
int capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* obj);
// 堆的插入
void HeapPush(Heap* obj, HDataType x);
// 堆的删除
void HeapPop(Heap* obj);
// 取堆顶的数据
HDataType HeapTop(Heap* obj);
// 堆的数据个数
int HeapSize(Heap* obj);
// 堆的判空
bool HeapEmpty(Heap* obj);
// 堆的销毁
void HeapDestroy(Heap* obj);
Heap.c
#include"Heap.h"
void HeapInit(Heap* php)
{
php->capacity = php->size = 0;
php->arr = NULL;
}
//扩容
void Exp(Heap* obj)
{
if (obj->size == obj->capacity)
{
int new_capeccity = obj->capacity == 0 ? 4 : obj->capacity * 2;
HDataType* tem = (HDataType*)realloc(obj->arr, sizeof(HDataType) * new_capeccity);
if (tem == NULL)
{
assert("realloc");
}
obj->arr = tem;
obj->capacity = new_capeccity;
}
}
//交换
void Swap(HDataType* child, HDataType* parent)
{
HDataType tem = *child;
*child = *parent;
*parent = tem;
}
//向上调整
//这里假设是小堆进行调整
//如果是大堆,将if处改为大于号即可
void UpAdjust(HDataType* p,int child)
{
//通过计算找父母
HDataType parent = (child - 1) / 2;
while (child > 0)
{
if (p[child] < p[parent])
{
//交换
Swap(&p[child], &p[parent]);
//交换后,将parent的位置给给child,继续计算下一个parent
child = parent;//把父母给孩子
parent = (child - 1) / 2;//得到新的父母
}
else
{
break;
}
}
}
//插入
//先向堆尾插入元素,再根据大堆还是小堆向上调整
void HeapPush(Heap* obj,HDataType x)
{
assert(obj);
Exp(obj);
obj->arr[obj->size] = x;
obj->size++;
//这个时候我们需要向上
UpAdjust(obj->arr, obj->size - 1);
}
//向下调整
//这里假设是小堆
//如果是大堆,将两个if处改为大于号即可
void DownAdjust(HDataType* p,int n,int parent)
{
//计算出左孩子
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && p[child + 1] < p[child]) {//如果右儿子小于左儿子,直接++到右儿子的位置
++child;//child+1<n是为了避免越界访问,因为每次算出的是左孩子,堆尾不一定是右孩子
}
if (p[child] < p[parent])//如果child<parent,就交换,要把小的往上走
{
//这边操作一样,算法不同
Swap(&p[child], &p[parent]);
parent = child;//把孩子给父母
child = parent * 2 + 1;//得到新的孩子
}
else
{
break;
}
}
}
//删除堆顶元素
//这里是小堆,删的实际是最小元素
void HeapPop(Heap* obj)
{
assert(obj);
assert(obj->arr);
assert(obj->size > 0);//堆内无元素则不存在删的问题
//1.先交换堆顶和堆尾的元素,避免中间节点之间关系全部混乱
Swap(&obj->arr[0], &obj->arr[obj->size - 1]);
//2.删
obj->size--;
//3.这里我们假设的是小堆,而当前交换后显然不是小堆,故向下调整,将堆中次小元素调到堆顶
DownAdjust(obj->arr, obj->size, 0);
}
//获取堆顶元素
HDataType HeapTop(Heap* obj)
{
assert(obj);
return obj->arr[0];
}
//获取堆中数据的个数
int HeapSize(Heap* obj)
{
assert(obj);
return obj->size;
}
//判空
bool HeapEmpty(Heap* obj)
{
assert(obj);
return obj->size == 0;
}
//堆的销毁
void HeapDestroy(Heap* obj)
{
assert(obj);
assert(obj->arr);
free(obj->arr);
obj->arr = NULL;
obj->capacity = obj->size = 0;
}
test.c
cl:以上代码和顺序表的实现是很相似的,最大区别是堆独有的特点,也就是建堆,堆尾插入元素时进行向上调整,堆顶删除元素时进行向下调整,这两步是最关键的算法,是保证堆的特点(大小堆)的关键。