1.树的概念及结构
1.1树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
注意:树型结构中,子树之间不能有交集。
1.2树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
1.3树的表示
树的结构相对来说比较复杂,我们想要存储表示出来,既要保存值,又要保存节点与节点之间的关系。实际上树的表示方式比较多,我们这里就介绍一下最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* firstChild;//第一个孩子
struct Node* nextBrother;//下一个兄弟
DataType data;//节点的值
}
我们创建这样的一个结构体,就将树很好的表示了出来
2.二叉树
2.1二叉树的概念
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.2特殊的二叉树
-
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
-
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.3二叉树的性质
1.一棵非空二叉树的第i层上最多有 2^(i-1) 个结点
2.深度为h的二叉树的最大结点数是 2^h -1。
3.度为0的节点个数比度为2的节点个数大1。
4.n个结点的满二叉树的深度,h= log2(n+1)。
5.父子节点下标的关系:
leftchild = 2*parent + 1;
rightchild = 2* parent + 2;
parent = (child - 1) / 2;
3.二叉树的顺序结构及实现
3.1二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2堆的概念及结构
堆的性质:
1堆中某个节点的值总是不大于或不小于其父节点的值;
2堆总是一棵完全二叉树。
3.3堆的实现
3.3.1堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整 成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
3.3.2堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算 法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的 子树开始调整,一直调整到根节点的树,就可以调整成堆。
下面我们以如何调成一个大堆为例
3.3.3建堆的时间复杂度
O(N)
3.3.4堆的代码实现
heap.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HeapDataType;
#define NUM 4
typedef struct Heap
{
HeapDataType* arr;
size_t size;//有效元素个数
size_t capacity;//容量
}Heap;
void HeapInit(Heap* php);
void HeapDestroy(Heap* php);
void HeapPush(Heap* php, HeapDataType data);
void HeapPop(Heap* php);
HeapDataType HeapTop(Heap* php);
bool HeapEmpty(Heap* php);
int HeapSize(Heap* php);
void HeapPrint(Heap* php);
heap.c
#include "Heap.h"
void HeapInit(Heap* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(Heap* php)
{
assert(php);
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
void CheckCapacity(Heap* php)
{
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? NUM : php->capacity * 2;
Heap* tmp = (Heap*)realloc(php->arr, sizeof(HeapDataType) * newCapacity);
if (NULL == tmp)
{
printf("realloc fail\n");
exit(1);
}
php->arr = tmp;
php->capacity = newCapacity;
}
}
void AdjustUP(HeapDataType* arr, int child)
{
int father = (child - 1) / 2;
while (child > 0)
{
HeapDataType tmp = arr[father];
arr[father] = arr[child];
arr[child] = tmp;
child = father;
father = (child - 1) / 2;
if (arr[child] > arr[father])
{
//说明不需要继续向上调整了,提前结束循环
break;
}
}
}
void HeapPush(Heap* php, HeapDataType data)
{
assert(php);
CheckCapacity(php);
php->arr[php->size] = data;//先把新数据放在最后,接下来在判断是否需要调整位置
//今天咱们以建小堆为例,如果新插入的元素不比父亲小则不用调整,否则需要向上调整
// 如果是插入第一个元素,那么也不用调整
//首先需要明确,父子节点间的位置关系
//LeftChild = 2 * father + 1;
//RightChild = 2 * father + 2;
//father = (Chile - 1) / 2;
if (php->size == 0 || data >= php->arr[(php->size-1) / 2])
{
php->size++;
return;
}
//下面需要向上调整
AdjustUP(php->arr, php->size);
php->size++;
}
void AdjustDown(HeapDataType* arr, int end)
{
int father = 0;
int LeftChild = 2 * father + 1;
int RightChild = 2 * father + 2;
while (1)
{
if (RightChild > end)
{
if (LeftChild > end)
{
//左右孩子都越界了,不用在调了
break;
}
else
{
//左孩子存在,右孩子越界
//左孩子就是最后一个,调最后一次
if (arr[father] > arr[LeftChild])
{
HeapDataType tmp = arr[father];
arr[father] = arr[LeftChild];
arr[LeftChild] = tmp;
break;
}
else
{
//只剩下左孩子但是不需要调了
break;
}
}
}
//左右孩子都存在
if (arr[father] < arr[LeftChild] && arr[father] < arr[RightChild])
{
//左右孩子都存在但是都比父亲大,说明已经调完了
break;
}
HeapDataType tmp = arr[father];
if (arr[LeftChild] < arr[RightChild])
{
arr[father] = arr[LeftChild];
arr[LeftChild] = tmp;
father = LeftChild;
}
else
{
arr[father] = arr[RightChild];
arr[RightChild] = tmp;
father = RightChild;
}
if (father == end)
{
//此时已经调整到最后了
break;
}
LeftChild = 2 * father + 1;
RightChild = 2 * father + 2;
}
}
void HeapPop(Heap* php)
{
//堆的删除删的是堆顶的元素
//假如说,我们直接将顶部数据删除,然后把后面所有元素往前挪动一步,那么这样会破坏堆的结构
//为了删除堆顶元素之后保证堆的结构不被破幻,我们应该
//1.将堆顶元素和最后一个元素互换
//2.size--即是删除堆顶元素
//3.由于原来的最后一个元素被挪到了堆顶,破坏了堆的结构,自顶向下调整保证堆的结构
assert(php);
//先处理一下以下三种 元素个数为0,1,2的情况。这种不需要向下调整。
if (php->size == 0)
{
return;
}
if (php->size == 1)
{
php->size--;
return;
}
if (php->size == 2)
{
php->arr[0] = php->arr[1];
php->size--;
return;
}
//下面开始处理
HeapDataType tmp = php->arr[0];
php->arr[0] = php->arr[php->size-1];
php->arr[php->size-1] = tmp;
php->size--;
AdjustDown(php->arr, php->size-1);
}
void HeapPrint(Heap* php)
{
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->arr[i]);
}
printf("\n");
}
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0 ? true : false;
}
HeapDataType HeapTop(Heap* php)
{
assert(php);
if (HeapEmpty(php))
{
printf("堆中无元素\n");
exit(2);
}
return php->arr[0];
}
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}