二叉树与堆
二叉树
二叉树定义:
每个节点的度小于等于 2 ,即每个节点的子树最多有两颗,且两个字数有左右顺序之分
满二叉树
二叉树的每一层节点数都达到了最大:
完全二叉树
完全二叉树中所存在的节点的位置与满二叉树中一一对应,但在最后一层依次从左往右存储时并未存满:
二叉树的基本性质
性质一:二叉树中第 i 层最多有 2^(i-1) 个节点
求解最多 ===》每一层的节点数都达到存储的最大值(每个节点都保证度为二)
因此,第 i 层最多有节点 : 2^(i-1) 个
性质二:高度为 h 的二叉树最多有 2^h - 1 个节点
求解最多 ===》每一层的节点数都达到存储的最大值(每个节点都保证度为二)
第一层节点最多有 : 2^(1-1) 个
第二层节点最多有 : 2^(2-1) 个
第三层节点最多有 : 2^(3-1) 个
…
第 h 层节点最多有 : 2^(h-1) 个
故:
共有节点数:
2^0 + 2^1+ 2^2 + … +2^(h-1)
=1*(1-2^h) / (1-2)
=2^h -1
性质三:在一棵二叉树中,假如度为零的节点个数为 n0,度为二的节点个数为 n2 ,则有关系式 n0=n2+1
假设一棵二叉树中节点总数为 N ,度为零的节点有n0个,度为二的节点有n2个,度为一的节点有n1个,则有等式 N=n0+n1+n2成立;
因为二叉树中除根节点外,其余节点都有一个双亲,故有 N 个节点的二叉树有 N-1 条边;
度为一的节点有一个孩子(即有一条向下的边),度为零的节点没有孩子(没有向下的边),度为二的节点有两个孩子(有两条向下的边)
因此有等式:N-1=0*n0+1*n1+2*n2
解两个方程式可以得到 n0=n2+1
性质四:具有 N 个总节点的二叉树高度为 [log2(n+1)](向上取整)
假设为满二叉树:
共有 N 个 节点,设高度为 h ,则有:
N=2^0+2^1+2^2+..+2^(h-1)
=2^h - 1
可得 h=log2(N+1)
若不是满二叉树,则高度值计算得到的是一个小数值,故向上取整去接近这个小数值的整数为高度值
性质五:对一颗具有 N 个节点的完全二叉树,对它从上往下、从左往右进行从 1 编号:
(1)1 号节点为根节点;
(2)第 i 个节点的左孩子节点为 2i,2i<n 则为左孩子,否则左孩子不存在;
(3)第 i 个节点的右孩子节点为 2i+1,2i+1<n 则为左孩子,否则左孩子不存在;
注意:
若根节点标记为 0 时,则它的左右孩子节点分别为 2i+1 、2i+2 (2*i+2 < n时,即左右孩子存在时)
节点为 i 的双亲节点为 (i-1)/2
堆
堆是一种完全二叉树结构------更适合使用顺序存储方式来存储数据
定义堆结构为:
typedef int DataType;
typedef struct Heap {
DataType* arr;
int capacity;
int size;
}Heap;
小根堆的定义
如果任意节点的值都小于其孩子节点的值,则称之为小根堆(小堆)
大根堆的定义
如果任意节点的值都大于其孩子节点的值,则称之为大根堆(大堆)
建小堆
已知一个数组,可以对照来建立相应的堆结构:
int arr[]={27,15,19,18,28,34,65,49,25,37};
根据数组画堆结构:
由图可以看出来 ,以 27 为根节点的二叉树的左右子树都为小堆 ,因此需要将 27 向下调整为小堆形式
继续将 27 向下调整:
此时 27 为根的二叉树依旧不满足小根堆,继续向下调整:
此时将 27 调整到了最下方,堆已经调整为小堆形式。
由此可见,调整小堆形式的过程是一个循环向下调整的过程,因此相应代码形式为:
//将以 parent 为根的树调整为小根堆
void Swap(int* a,int* b)
{
int tmp=*a;
*a=*b;
*b=tmp;
}
//向下调整以 parent 为根节点的树
void AdjustDown(int arr[],int size,int parent){
int child=2*parent+1; //记录左孩子节点
while(child < size) { //有两个节点时
//当右孩子节点存在时可以进行调整
if(child + 1 < size && arr[child+1]<arr[child]){
//存在右孩子时,选取较小的孩子
child=child+1;
}
//将小孩子节点与双亲进行比较
if(arr[parent]>arr[child]){
Swap(&arr[parent],&arr[child]); //交换
//大的元素向下调整,将小节点向上调整
//调整之后导致下一层节点为根的树不满足小堆特性,需要继续调整
parent=child;
child=2*parent+1; //根节点从 0 开始标号
}
else{
return ;
}
}
}
因此,如需要将一个不是小根堆的数组结构调整为小堆结构,则需要从最后一个非叶子节点开始进行调整:
最后一个叶子节点 n-1
最后一个叶子节点的双亲节点(最后一个非叶子节点) ((n-1)-1) / 2
Heap* hp;
//核心代码:
//从最后一个非叶子节点开始进行调整小根堆
for(int root=(n-2)/2;root>=0;--root){
AdjustDown(hp->arr,hp->size,root);
}
建大堆
给定一个数组形式:
int arr[]={18,27,28,15,19,34, 25,37,65,49};
据此数组画相应的堆结构:
建大堆,需要从最后一个非叶子节点开始依次向前调整,假如此时需要对某一个 parent (节点值为 15)为根节点的树进行调整大堆:
将 15 向下调整:
此时 15 为根的树依旧不满足大堆特性,继续调整:
对以 parent 为根的树进行调整为大根堆相应代码为:
//将以 parent 节点为根的树调整为大堆
void AdjustDownBig(int* arr, int size, int parent)
{
//将 parent 树调整为大根堆
int child = 2 * parent + 1;
while (child < size)
{
//找较大的孩子节点
if (child + 1 < size && arr[child + 1] > arr[child])
child += 1;
//较大值与双亲进行比较,双亲小则向下调整
if (arr[child] > arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child; //继续向下进行判断
child = 2 * parent + 1;
}
else
return;
}
}
如需要将整个非大根堆数组进行调整为大堆:
Heap* hp;
//核心代码:
//从最后一个非叶子节点开始进行调整为大堆
for(int root=(n-2)/2;root>=0;--root){
AdjustDownBig(hp->arr,hp->size;root);
}
堆的基本操作(以小堆为例)
堆的创建
给定一个数组结构,需要按要求创建堆结构:
void HeadCreate(Heap* hp, DataType* a, int n)
{
//首先需要给堆空间
hp->arr = (DataType*)malloc(sizeof(DataType)*n);
if (NULL == hp->arr)
return;
//假如创建的堆中有 n 个元素,定义堆容量为 n
hp->capacity = n;
//将给定数组中元素拷贝到堆空间中
memcpy(hp->arr, a, sizeof(DataType)*n);
hp->size = n;
//调整为小堆结构
//从倒数第一个非叶子节点进行调整
//倒数第一个叶子节点的双亲,倒数第一个叶子节点 n-1, 它的双亲 ((n-1)-1)/2
for (int root = (n - 2) / 2; root >= 0; root--) {
AdjustDown(hp->arr, hp->size, root);
}
}
堆销毁
void HeapDestroy(Heap* hp)
{
assert(hp);
free(hp->arr); //释放堆空间
hp->arr = NULL;
hp->capacity = hp->size = 0;
}
堆判空
int HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
求堆中元素个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
取堆顶元素
DataType HeapTop(Heap* hp)
{
assert(hp);
return hp->arr[0]; //0号位置为堆顶元素
}
堆中元素的删除
堆元素的删除一般是指删除堆顶元素。
将堆中最后一个元素与堆顶元素进行交换,则堆中元素减一(size–),然后将堆中 size 个元素进行重新调整为小堆元素:
void HeadPop(Heap* hp)
{
//删除堆顶元素
if (HeapEmpty(hp))
return;
Swap(&hp->arr[0], &hp->arr[hp->size - 1]); //将堆顶元素与最后一个元素交换,然后调整
hp->size--;
AdjustDown(hp->arr, hp->size , 0);
}
堆的插入(重点*****)
首先需要判断容量是否足够:不够需扩容,够直接存;
其次,插入元素是在堆底进行的插入,因此需要判断插入的元素是否为最小(即是否需要向上调整);
最后,调整为小堆之后,size++(元素有效个数加一)
//判断堆容量
void CheckCapacity(Heap* hp)
{
if(hp->capacity==hp->size)
{
int newcapacity=2*hp->capacity;
DataType* tmp=(DataType*)malloc(sizeof(DataType)*newcapacity);
if(NULL==tmp)
return;
memcpy(tmp,hp->arr,sizeof(DataType)*hp->size);
free(hp->arr); //释放旧空间
hp->arr=tmp;
hp->capacity=newcapacity;
}
}
//进行元素插入时,是将元素插入到堆底
void HeapPush(Heap* hp,DataType x)
{
assert(hp);
CheckCapacity(hp); //检查容量是否足够
//新元素插入堆底
hp->arr[hp->size++]=x;
//元素插入之后需要检查是否满足大堆/小堆的基本条件,进行相应的向上调整
AdjustUp(hp->arr,hp->size,hp->size-1);
}
//向上调整函数-------小堆
void AdjustUp(DataType arr[],int size,int child)
{
int parent=(child-1)/2; //找需要调整的节点的父母节点
while(chile>=0){
if(arr[child]<arr[parent]){ //(新插入的)孩子节点小于双亲,需要将孩子上移
Swap(&arr[child],&arr[parent]);
child=parent; //继续上一层的调整
parent=(child - 1) / 2;
}
else{
return;
}
}
}
至此,建堆(小堆)的基本操作已经完成,小堆是需要将小元素往上移,大元素向下移;对应大堆的基本操作时,需要将大元素向上小元素向下移。
但是若需求不是很明确,即一段代码想要既能够实现小堆又能够实现大堆时,我们可以使用一个函数指针的方法来实现该操作。
(有任何问题欢迎评论哦~)