二叉树
1.树的概念及结构
1.1 树的概念
树是一种非线性的数据结构,他是由N个有限节点组成的一个具有层次关系的集合。
1.2 树的相关概念
节点的度:一个节点所含有的子树的个数称为节点的度。
叶节点:度为0的节点称为叶节点
父节点:若一个节点有子树,则该节点是子树包含的所有节点的父节点。
子节点:若一个节点有子树,则所有子树包含的节点都是该节点的子节点。
树的度:树中所有节点拥有的最大的度就是树的度。
节点的层次:从根节点开始定义,根节点是第一层,根节点的子节点是第二层。
树的高度或深度:树中节点的最大层次。
祖先:从根到该节点路径上的所有节点都是该节点的祖先。
子孙:以某节点为根的子树中任意节点都是某节点的子孙
森林:由多个不相交的树的集合称为森林。
1.3 树的表示
树在数据结构中既要记录值域,又要记录节点之间的关系。
所以下面的孩子兄弟记录法能很好的表示出树:
typedef int TNDataType;
typedef struct TreeNode
{
TNDataType _data;//节点的数据
TN* child;//指向第一个孩子节点
tn* brother;//指向下一个兄弟节点
}TN;
2.二叉树的概念及结构
2.1 二叉树的概念
二叉树不存在度大于2的节点,由一个根节点加上两颗称为左子树和右子树的二叉树组成。
2.1.1 满二叉树
一个二叉树,如果每一层的节点数都达到了最大值(均为2),则这个二叉树就可以被称作为 “满二叉树” 。换言之,如果一个二叉树的层数为 h,且节点总数是 2^h - 1 ,则他就是一个满二叉树。
2.1.2 完全二叉树
对于深度为 K 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 1 至 n 的结点一一对应时称之为完全二叉树。
2.2 二叉树的性质
-
若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2 的(h-1)次方个结点.
-
若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2的h次方减一 .
-
若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log(n+1).
-
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
若i>0,i位置节点的双亲序号:(i-1)/2 i=0,i为根节点编号,无双亲节点 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
2.3 二叉树的储存结构
二叉树一般可以使用两种结构存储,一种是顺序结构,一种是链式结构。
1.顺序结构
顺序结构就是使用数组存储,一般只适合表示完全二叉树,因为不是完全二叉树的会有空间的浪费,现实中只有堆(完全二叉树)才会使用数组来存储。
二叉树顺序存储在物理上是一个数组,在逻辑上是一个二叉树。
2.链式存储
二叉树的链式存储结构是指,用链表来表示一颗二叉树通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点
左孩子和右孩子所在的链结点的存储地址 。
3.二叉树的顺序结构及实现
3.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2 堆的概念及性质
堆的概念:一个所有元素都按照完全二叉树的顺序存储方式存储在一个数组中的结构,叫做堆,根节点最大的叫做大堆,根节点最小的叫做小堆。
堆的性质:
- 堆中所有节点的值总是不大于或不小于其父节点的值:
- 堆总是一个完全二叉树。
3.3 堆的实现
3.3.1 堆向下调整算法
下面我们可以创建一个数组,这个数组逻辑上可以看做一颗完全二叉树,但还不是一个堆,我们可以通过向下调整算法,从倒数的第一个给叶子节点的节点开始调整,直到调整到根节点,就可以调整成堆。
int a[] = {1,5,3,8,7,6};
//向下调整算法
void AdjustDown(PHDatatye* a, int size, int parent)
{
//断言
assert(a);
size--;
//向下调整
while ((parent * 2 + 1) <= size)
{
int child = parent * 2 + 1;
if ((child + 1) <= size && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
//交换函数,可以自己写一个
SWAP(&a[child], &a[parent]);
parent = child;
}
else
{
break;
}
}
}
3.3.2 堆的插入
先插入一个数到数组 的尾上,在进行向下调整算法,直到满足堆。
3.3.3 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调
整算法。
3.3.4 堆的代码实现
typedef int HPDataType;
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
//实现
void SWAP(PHDatatye* s1, PHDatatye* s2)
{
PHDatatye tmp = *s1;
*s1 = *s2;
*s2 = tmp;
}
void AdjustUp(PHDatatye* a, int child)
{
//按照建造一个大堆来写
assert(a);
//找到当前节点的父亲节点,比较,如果父节点大于当前 节点,停止循环,或者当前节点等于或小于0,停止循环
while (child > 0)
{
int parent = (child - 1) / 2;
//如果当前节点大于父亲节点,则交换节点数据,并更新当前节点位置
if (a[child] >a[parent])
{
SWAP(&a[child], &a[parent]);
child = (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(PHDatatye* a, int size, int parent)
{
assert(a);
size--;
//向下调整
while ((parent * 2 + 1) <= size)
{
int child = parent * 2 + 1;
if ((child + 1) <= size && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
SWAP(&a[child], &a[parent]);
parent = child;
}
else
{
break;
}
}
}
void HeapInit(PH* php)
{
//首先,断言。其次,初始化结构体中的成员变量
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
void HeapDestroy(PH* php)
{
//首先,断言,其次,想办法销毁这个结构体中申请的空间,并将结构体中的成员变量归零
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
void HeapPush(PH* php, PHDatatye x)
{
//首先,断言一下这个结构体的指针是否为空,然后在堆中插入一个x
assert(php);
//插入数据之前先判断这个堆中空间满了没,如果满了,或者这个堆的空间还么开辟,就先开辟下空间
if (php->capacity == php->size)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
int* New = (int*)realloc(php->a, sizeof(int) * newCapacity);
if (New == NULL)
{
perror("realloc failed");
exit(-1);
}
php->a = New;
php->capacity = newCapacity;
}
//开始按照堆的方式插入这个值
php->a[php->size] = x;
php->size++;
//向上调整
AdjustUp(php->a, php->size - 1);
}
void HeapPop(PH* php)
{
assert(php);
assert(php->size > 0);
//先交换两个的值
SWAP(&php->a[(php->size) - 1], &php->a[0]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
PHDatatye HeapTop(PH* php)
{
assert(php);
return php->a[0];
}
bool HeapEmpty(PH* php)
{
assert(php);
return !php->size;
}
int HeapSize(PH* php)
{
assert(php);
return php->size;
}
void HeapPrintf(PH* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//创建堆,两种方法,一种是慢慢想堆中插入数据。第二种是对结构中的数据进行向下调整
void HeapCreate(PH* php, PHDatatye* a, int x)
{
assert(php);
php->a = (PHDatatye*)malloc(sizeof(PHDatatye) * x);
if (php->a == NULL)
{
perror("......");
exit(-1);
}
php->capacity = php->size = x;
memcpy(php->a, a, x * sizeof(PHDatatye));
for (int i = (x - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->a, x, i);
HeapPrintf( php);
}
}
3.4 堆的应用
3.4.1 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1.建堆
升序:建大堆
降序:建小堆
void HeapSort(PHDatatye* array, int size)
{
assert(array);
//把数组转化成大堆
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(array, size, i);
}
//调整顺序
while (size >= 1)
{
//把堆的首尾换一下,然后把第一个向下调整,堆的尺寸减一,不看最后一个
SWAP(&array[0], &array[size - 1]);
size--;
//把第一个向下调整,size是堆的尺寸,
AdjustDown(array, size, 0);
}
}
3.4.2 TOP-K问题
即求数据中前k个最大或最小的元素的,一般情况下数据量都比较大(比如世界500强之类的)。
1.用数据集合中前k个元素来建堆
求前k个最大的元素:建小堆
求前k个最小的元素:建大堆
2.用剩余的元素依次与栈顶元素比较,不满足则替换栈顶元素。
4. 二叉树的链式结构的实现
待补充。。。。。。