数据结构第八讲:二叉树_堆
1.树
1.1树的概念
树是由n个有限结点组成的一个具有一定层次关系的集合,与现实中的树相似,但是它却是“倒过来”的一棵树,它的根结点在头部,叶子在下面:
1.每个子树的根节点只能有一个或多个前驱,有0个或多个后继,也就是说,一个子结点只能有一个父节点,一个父节点可以有0个或多个子节点
2.子树是互不相交的
3.一颗N个结点的树有N-1条边
1.2树的相关语
1.3树的表示
对于一个树的表示还是比较困难的,因为我们既要保存数据,又要保存结点之间的关系,这里我们介绍一种孩子兄弟表示法
//树-孩子兄弟表示法
typedef int TreeDataType;
struct TreeNode
{
TreeDataType data;//存储数据
struct TreeNode* child;//表示自己的子结点
struct TreeNode* brother;//表示兄弟结点
};
也就是这样的:
1.4树形结构实际运用场景
树形的结构在实际生活中的运用还是比较多的,例如我们的文件夹:
2.二叉树
2.1概念与结构
二叉树,顾名思义,就是指一个父节点最多只能有两个子节点,分别称为左节点和右节点:
二叉树具有的特点:
1.二叉树中的节点不存在度大于2的情况
2.子树具有左子树和右子树之分,次序不能颠倒,因此二叉树是有序树
2.2特殊的二叉树
2.2.1满二叉树
当所有层中的节点树达到最大时,就是一个满二叉树:
2.2.2完全二叉树
满二叉树是一种特殊的完全二叉树,要注意的是,完全二叉树中的最后一层必须从左向右进行插入二叉树,也就是下面这样:
此时就不是一个完全二叉树,只有按顺序插入的才叫完全二叉树,如果想要在那个位置插入节点,需要在前面都插入节点:
二叉树的性质:
1.若规定根节点的层数为1,那么一颗非空二叉树的第i层上最多有2的i次方减1个节点
2.若规定根节点的层数为1,当存在k层时,那么所有的节点数最多为2的k次方-1
3.根据性质2可以推导出,具有n个节点的二叉树的层数为:log2(n+1)
2.3二叉树的存储结构
2.3.1顺序结构
顺序结构存储时,我们使用数组来进行存储,一般我们对于完全二叉树实行顺序结构的存储方式,用来避免空间浪费:
我们通常将堆使用二叉树进行存储
2.3.2链式结构
链式结构存储使用的是节点进行存储,与链表相似,节点中存储了三个域:数据域与左右指针域,左右指针域分别指向左孩子和右孩子所在的链结点的存储地址。链式结构又分为二叉链和三叉链,我们现在 需要实现的是二叉链
3.堆的实现
堆是一种特殊的二叉树,通常使用顺序结构来存储,在具有二叉树性质的同时,还具有一些其他的性质。
3.1概念与结构
堆分为大堆和小堆,根节点最大的堆称为大堆,根节点最小的堆称为小堆
堆的性质:
1.堆中某节点中存储的数据总是大于或小于父节点中存储的值
2.堆总是一颗完全二叉树
下面就是堆的实现:
3.2堆的基础结构
堆的实现和顺序表的实现非常相似,它的基础结构和顺序表的基础结构甚至是一样的
//堆的基础结构
typedef int HeapDataType;
typedef struct Heap
{
HeapDataType* arr;
int size;
int capacity;
}HP;
3.3堆的初始化
参考顺序表的初始化
//堆的初始化
void HeapInit(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
3.4堆的销毁
参考顺序表的销毁
//堆的销毁
void HeapDestory(HP* php)
{
assert(php);
if (php->arr)
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
3.5堆的插入
堆的插入与顺序表不同,具体看图:
//交换函数
void Swap(HeapDataType* x, HeapDataType* y)
{
HeapDataType tmp = *x;
*x = *y;
*y = tmp;
}
//堆的向上排序
void ADJustUp(HP* php)
{
int child = php->size - 1;
int father = (child - 1) / 2;
while (child > 0)
{
if (php->arr[child] < php->arr[father])
{
Swap(&php->arr[child], &php->arr[father]);
child = father;
father = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入
void HeapPush(HP* php, HeapDataType x)
{
assert(php);
//首先要检查空间是否充足
if (php->size == php->capacity)
{
//当空间不足时,进行空间的开辟
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HeapDataType* newarr = (HeapDataType*)realloc(php->arr, newcapacity * sizeof(HeapDataType));
if (newarr == NULL)
{
perror("realloc faile!");
exit(1);
}
php->arr = newarr;
php->capacity = newcapacity;
}
//当空间充足时,需要将数据进行插入,插入时直接将数据插入堆尾即可
php->arr[php->size++] = x;
//然后要对这个数据进行向上排列,使得堆仍为小堆排序
ADJustUp(php);
}
3.6堆的删除
堆的删除也需要看图:
//堆的向下排序
void ADJustDown(HP* php, int father)
{
int child = father * 2 + 1;
while (child < php->size - 1)
{
//此时要寻找左右孩子中最小的那个孩子
if (child+1<php->size && php->arr[child] > php->arr[child + 1])
{
child += 1;
}
//如果目标数据够大的话,将最小的那个孩子与目标数据进行交换
if (php->arr[father] > php->arr[child])
{
Swap(&php->arr[father], &php->arr[child]);
father = child;
child = father * 2 + 1;
}
else
{
break;
}
}
}
//堆的删除
void HeapPop(HP* php)
{
assert(php && php->arr);
//先将堆中最后一个节点中的数据和堆中第一个节点中的数据进行交换
int child = php->size - 1;
Swap(&php->arr[0], &php->arr[child]);
--php->size;
//交换成功后,对整个堆进行向下排列,使得堆为小堆排序
ADJustDown(php, 0);
}
3.7堆的判空
//堆的判空
bool HeapEmpty(HP* php)
{
assert(php);
//当php中没有数据时,表示为空
return php->size == 0;
}
3.8堆的大小
//求size
int HeapSize(HP* php)
{
assert(php);
//直接将size返回即可
return php->size;
}
3.9打印堆顶数据
//打印堆顶数据
HeapDataType HeapTop(HP* php)
{
assert(php && php->size);
return php->arr[0];
}
4.堆的应用
4.1堆排序
4.1.1冒泡排序
冒泡排序的实现相对来说非常简单,而且它的时间复杂度较大,在实际应用中基本不会应用:
//冒泡排序
void BubbleSort(int *arr, int n)
{
for (int i = 0; i < n; i++)
{
int def = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
def = 1;
}
}
if (def == 0)
{
break;
}
}
}
可见,冒泡排序的时间复杂度为o(n^2),这个时间复杂度还是很大的,既然这样,我们就来学习一个新的排序方法:堆排序
4.1.2堆排序实现思路
假设我们现在要将一个数组进行逆序排列,也就是从大到小排列
堆排序并不是意味着给堆排序,而是使用堆的思想来对数组进行排序,意思是说,我们不用将数据一个一个地去入堆,这样会由于创建堆开辟不必要的空间,增大空间复杂度,那么我们究竟该如何实现呢?
4.1.2.1使用向上排序算法创建堆
先在数组中拿出一个数据,将数据使用向上排序算法进行排序,再拿出一个数据,再使用向上排序算法进行排序,以此循环,直到将数据全部拿出,代码实现十分简单,主要是理解:
//首先需要一个数据一个数据地拿出来,创建一个堆
for (int i = 0; i < 6; i++)
{
ADJustUp(arr, i);
}
创建堆并不是重新开辟了空间,而是在原数组的基础上将原数组改变成小堆的形式:
4.1.2.2对堆进行排序
要求为逆序排列,也就是要将小数放在最后,所以我们使用小堆,因为小堆的根节点的值是最小的
首先需要我们将根节点的值和最后一个节点的值进行交换,使得最后一个节点的值最小,然后进行向下排序,让根节点的值再次为最小,然后再进行交换,以此循环:
代码的实现也不是特别困难:
//先交换数据
int end = sz;
while(end > 1)
{
Swap(&arr[0], &arr[end - 1]);
end--;
//然后对堆中第一个数据进行向下排序
ADJustDown(arr, 0, end);
}
4.1.3堆排序–逆序(降序)排序代码实现
//堆排序
void HeapSort(int* arr, int sz)
{
//首先需要一个数据一个数据地拿出来,创建一个堆
for (int i = 0; i < sz; i++)
{
ADJustUp(arr, i);
}
//先交换数据
int end = sz;
while(end > 1)
{
Swap(&arr[0], &arr[end - 1]);
end--;
//然后对堆中第一个数据进行向下排序
ADJustDown(arr, 0, end);
}
}
4.1.4堆排序–顺序(升序)排序代码实现
升序实现方法和降序相同,差别在于向上排序和向下排序,因为此时我们要创建的是大堆:
//堆的向上排序
void ADJustUp(HeapDataType* arr, int child)
{
int father = (child - 1) / 2;
while (child > 0)
{
//建大堆:>
//建小堆:<
if (arr[child] > arr[father])
{
Swap(&arr[child], &arr[father]);
child = father;
father = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的向下排序
void ADJustDown(HeapDataType* arr, int father, int n)
{
int child = father * 2 + 1;
while (child < n)
{
//大堆:寻找左右孩子中最大的那个孩子
//小堆:寻找左右孩子中最小的那个孩子
if (child + 1 < n && arr[child] < arr[child + 1])
{
child += 1;
}
//如果目标数据小/大的话,将最大的那个孩子与目标数据进行交换
if (arr[father] < arr[child])
{
Swap(&arr[father], &arr[child]);
father = child;
child = father * 2 + 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int* arr, int sz)
{
//首先需要一个数据一个数据地拿出来,创建一个堆
for (int i = 0; i < sz; i++)
{
ADJustUp(arr, i);
}
//先交换数据
int end = sz;
while(end > 1)
{
Swap(&arr[0], &arr[end - 1]);
end--;
//然后对堆中第一个数据进行向下排序
ADJustDown(arr, 0, end);
}
}
4.1.5向下调整方法建堆
我们刚刚使用的是向上调整算法建堆,那么我们能不能使用向下调整算法建堆呢?
代码实现比较简单,主要是思路:
//向下调整算法建堆
int n = (sz - 1 - 1) / 2;
for (int i = n; i >= 0; i--)
{
ADJustDown(arr, i, sz);
}
4.1.6向上调整创建堆–时间复杂度求解
//首先需要一个数据一个数据地拿出来,创建一个堆
for (int i = 0; i < sz; i++)
{
ADJustUp(arr, i);
}
当我们第一眼看时,我们应该会想:这时间复杂度不就是o(n)嘛,其实不是,理由:
我们现在分别分析在不同的层数需要向上调整的次数(最坏情况)
第一层:0次
第二层:1次
第三层:2次
第四层:3次
…
第k层:k-1次
然而每一层都有2的k-1次方个节点,所以所有层数向上移动的次数为:
利用数学方法:错位相减,可得:
所以说该算法的时间复杂度为O(nlogn)
4.1.7向下调整创建堆–时间复杂度求解
和上边的分析思路相同
计算每一层节点向下移动的次数:
第一层:h-1层
第二层:h-2层
第三层:h-3层
…
第h-1层:1层
最后一层:0层
所以说向下排序建堆的时间复杂度为o(n),说明向下调整算法建堆要优于向上调整算法建堆
5.OJ题—TOP-K问题
代码实现起来并不困难,难的是对于文件的操作不能忘:
//TOP-K问题
void CreateNDate()
{
// 造数据
int n = 100000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
//首先是创造了100000个数据
for (int i = 0; i < n; ++i)
{
int x = (rand() + i) % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
//解题
void topk()
{
//先输入k值,也就是要寻找的最大/小的前k个元素,这里假设要找最大的元素
int k = 0;
printf("请输入K值:");
scanf("%d", &k);
//在文件中先读取k个值,将数据保存在堆中
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen file!");
return;
}
//先创建一个具有K个数据的堆
int val = 0;
int* minheap = (int*)malloc(k * sizeof(int));
if (minheap == NULL)
{
perror("malloc fail!");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &minheap[i]);
//读取的是大数据,我们要用小堆,向下调整创建小堆
ADJustDown(minheap, 0, i + 1);
}
//然后逐一读取数据,直到读取结束
while (fscanf(fout, "%d", &val) != EOF)
{
//当数据要大于堆顶数据时,将数据与堆顶数据进行交换
if (val > minheap[0])
{
Swap(&val, &minheap[0]);
//然后将数据进行向下排序
ADJustDown(minheap, 0, k);
}
}
//最后将数据进行打印检验
for (int i = 0; i < k; i++)
{
printf("%d ", minheap[i]);
}
free(minheap);
minheap = NULL;
fclose(fout);
}