树与二叉树
一、树的概念和结构
1.1 树的概念
树是一种非线性的数据结构,它是由有限个节点组成的一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树,也就是说他是根朝上而叶子朝下的,如图:
树的相关描述:
节点的度: 一个节点含有的子树的个数称为该节点的度
树的度: 树的所有节点的度中最大的称为该树的度
叶节点或终端节点: 度为0的节点称为叶节点
非终端节点或分支节点: 度不为0的节点
双亲节点或父节点: 若一个节点含有子节点,则称这个节点为其子节点的
父节点或双亲节点
孩子节点或子节点: 一个节点含有的子树的根节点称为该节点的子节点
兄弟节点: 具有相同父节点的节点互称为兄弟节点
节点的层次: 从根节点定义起,根节点为第一层根节点的子节点为第二层,以此类推。
树的高度或深度: 树中节点的最大层次
堂兄弟节点: 双亲节点在同一层次的节点互称为堂兄弟
节点的祖先: 从根节点到该节点所经分枝上的所有节点。
子孙: 以某节点为根的子树中的所有节点都称为该节点的子孙
森林: 由n(n>0)颗互不相交的树的集合称为森林。
其中树有几个特点需要注意:
- 每颗树都有一个特殊的结点叫根节点,根节点没有前驱节点
- 除根结点外,其余结点被分成M(M>0)个互不相交的集合,其中每一个集合又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
- 因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
1.2 树的表示
二、二叉树
2.1二叉树的概念和结构
一个二叉树是节点的一个有限集合该集合:
- 或者为空
- 由一个根节点加上两颗别称为左子树和右子树的二叉树组成
- 二叉树不存在度大于二的节点
- 二叉树的子树有左右之分,次序不可颠倒,因此二叉树是有序树
注意
对于任意的二叉树都是由以下几种情况复合而成的:
特殊的二叉树:
- 满二叉树: 一个二叉树的每一层节点数都达到最大值
- 完全二叉树: 除了可能的最后一层外,所有层的节点数都达到了最大个数。如果最后一层不是完全填满的,那么该层节点都尽可能地靠左排列。满二叉树是特殊的完全二叉树即最后一层是填满的。
三、顺序结构二叉树(堆)
3.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。
(需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。)
- 如图,可以看到非完全二叉树用数组存储时,许多下标对应的空间并没有存储到相应的值,如果树的体系够大可能会造成巨大的空间浪费
3.2 堆的概念与结构:
将一个关键码的集合K = { k1,k2 , k3,…,k(n-1) },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
堆的本质就是数组也就是顺序表,堆只不过将数组或顺序表以二叉树的关系表示了出来。
堆的性质:
- 堆中的某个节点的值总是不大于或不小于其父节点的值;
- 堆是一颗完全二叉树。
小根堆:以任意一节点为父节点其子节点的值都大于父节点,所以在小堆中根节点最小。
大根堆:以任意一节点为父节点其子节点的值都小于父节点,所以在大堆中根节点最大。
3.3 堆的创建(向上向下调整算法)
当我们得到一个数组或顺序表时如何将其建成一个堆呢?现在我们通过算法将其构建成一个堆:
int arr[]={5,7,6,9,4,3,8,1};
// 下标:0 1 2 3 4 5 6 7
向上调整建堆:
步骤:
- 从左向右挨个插入
- 每插入一个与其父节点比较调整一次
- 调整后再与新的父节点比较若不符合条件再调整,直到根节点为止
以创建一个大堆为例:
实际上在实现向上调整建堆时我们不用再新建一个新数组用于数据的插入,在原数组中就可以。关键是找到相应子节点对应的父节点,也就是其中所蕴含的二叉树关系,值得庆幸的是我们通过数组下标就可以解决:
经过观察我们发现下标关系有:
- child=parent * 2+1 (计算左子节点)
- child=parent*2+1+1(计算右子节点 )
- parent= (child-1)/2
因此我们通过对数组的遍历就可以用向上调整进行建堆:
void HeapSort(int* a, int n)//n为数组元素的多少
{
// 向上调整建堆 O(N*logN)
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;//找到其所对应的父节点
//while (parent >= 0)
while (child > 0)
{
if (a[child] < a[parent])//小的向上调整说明建小堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向上调整
向下调整建堆:
向下调整建堆的核心是确保调整节点的左右子树分别已经成堆,但是当我们拿到一个数组时只能将其看作一个完全二叉树而不是堆,怎么来构建呢?
这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点就可以调整成堆。
举个例子:
int a[]={1,5,3,8,7,6};
需要注意的是:
- 每一次发生元素交换的时候,都需要递归调用重新构造堆的结构此时就要对破环原有堆序性的部分重新构造堆,如上图第四步调整下来的1破坏原有堆序性需要再次调整。
在实际代码实现向下调整建堆的时候我们可以先找到倒数的第一个非叶子节点的下标,然后对其向下调整后再- -,一直调整到根节点就可以完成自下而上向下调整建堆。我们发现,倒数第一个非叶子节点总是最后一个节点的父节点。所以:
- parent = (size-1-1)/2
这里size为数组元素的多少,parent为倒数第一个非叶子节点的下标。
代码实现如下:
void CreatHeap(int* a, int n)
{
// 向下调整建堆 O(N)
for (int i = (n-1-1)/2; i >= 0; i--)//下标依次-1,直到调整到根节点
{
AdjustDown(a, n, i);
}
}
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;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
3.4建堆复杂度(向上向下调整建堆的比较)
向下调整建堆的时间复杂度的计算:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明。
所以向下调整建堆的时间复杂度是O(N),类似的向上调整建堆的时间复杂度是O(N*log N),在时间复杂度上看向下调整建堆的效率更高,我们再使用堆这个数据结构是向下调整算法也利用地更多。
3.5 堆的代码实现
堆的本质就是数组或顺序表,所以代码实现堆与顺序表别无二致:
//Heap.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
//两数据的交换
void Swap(HPDataType* p1, HPDataType* p2);
//向上调整
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int n, int parent);
//堆的初始化
void HPInit(HP* php);
//堆的销毁
void HPDestroy(HP* php);
//堆的插入
void HPPush(HP* php, HPDataType x);
//删除堆顶数据
void HPPop(HP* php);
//返回堆顶数据
HPDataType HPTop(HP* php);
//判空
bool HPEmpty(HP* php);
//Heap.c
#include"Heap.h"
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&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 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");//申请空间失败了
return;
}
//成功
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;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// logN
void HPPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&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;
}
3.6 堆的应用(堆排序)
堆排序字面意思就是对数组中的元素进行排序,总共分为两个步骤:
1.建堆:
- 升序:建大堆
- 降序:建小堆
2.利用堆删除的思想来进行排序
代码实现:
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 > 0)
{
Swap(&a[0], &a[end]);//与末尾数据交换
AdjustDown(a, end, 0);//对新数组建堆
--end;
}
}