目录
二叉树介绍
概念
特殊的二叉树
二叉树的性质
二叉树的存储方式
1.顺序存储
根据上图可知,顺序存储只适合存储完全二叉树,并不适用于非完全二叉树的存储
2.链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,先接触二叉链,后面接触的数据结构如红黑树等会用到三叉链。
堆实现二叉树
引入逻辑概念与物理概念
逻辑概念:即上面图所示的二叉树概念图
物理概念:以数组实现二叉树
也就是说树状图是我们为方便理解而设立的一个假想的概念图,实际上我们操作的还是一个数组,利用下标来实现数据的跳跃访问来帮助实现一些问题
代码
Heap.h
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
//打印
void HeapPrint(HP* php);
//初始化
void HeapInit(HP* php);
//销毁
void HeapDestroy(HP* php);
//插入 -- 根据堆的特性,只会尾插,但是插入x继续保持堆形态
void HeapPush(HP* php , HPDataType x);
//出堆 -- 删除堆顶的元素
void HeapPop(HP* php);
//调整顺序
void Swap(HPDataType* p1, HPDataType* p2);
//向上调整元素 -- 保持堆的形态
void AdjustUp(HPDataType* a, int child);
//向下调整 -- 从头开始调
void AdjustDown(HPDataType* a, int n, int parent);
//看头 -- 返回堆顶的元素
HPDataType HeapTop(HP* php);
//判断是否为空
bool HeapEmpty(HP* php);
//返回元素个数
int HeapSize(HP* php);
Heap.c
#include "Heap.h"
//打印
void HeapPrint(HP* php)
{
for (int i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//初始化
void HeapInit(HP* php)
{
assert(php); //不能为空
php->a = NULL;
php->size = php->capacity = 0;//这里也可以先扩容,也可以在后面扩容
}
//销毁
void HeapDestroy(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 (child > 0) //当孩子等于0的时候就停下
{
if (a[child] < a[parent]) //这里大于还是小于可以控制是大堆还是小堆,这里是在建小堆
{
//传地址过去
Swap(&a[child], &a[parent]);
//更新父节点
child = parent;
parent = (child - 1) / 2;
}
else
{
break; //当满足堆的性质时就跳出循环
}
}
}
//插入 -- 根据堆的特性,只会尾插,但是插入x继续保持堆形态
void HeapPush(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, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
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 minChild = parent * 2 + 1;
while (minChild < n) //防止越界
{
if (minChild + 1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild; //
minChild = parent * 2 + 1; //计算左孩子
}
else
{
break;
}
}
}
//出堆 -- 删除堆顶的元素
//时间复杂度: O(logN)
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
//看头 -- 返回堆顶的元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
//判断是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//返回元素个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
以下是测试用例
Test.c
#include "Heap.h"
//测试一
//int main()
//{
// int a[] = { 15,18,19,25,28,34,65,49,27,37 };
// int a[] = { 65,100,70,32,50,60 };
// HP hp;
// //先创立一个再初始化
// HeapInit(&hp);
//
// for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
// {
// HeapPush(&hp, a[i]);
// }
//
// //HeapPush(&hp, 10);
// //HeapPrint(&hp);
//
// //HeapPop(&hp);
// //HeapPrint(&hp);
//
// //HeapPop(&hp);
// //HeapPrint(&hp);
//
// while (!HeapEmpty(&hp))
// {
// printf("%d ", HeapTop(&hp));
// HeapPop(&hp);
// }
//
// return 0;
//}
//测试二
void HeapSort(int* a, int n)
{
//建堆 -- 向上调整建堆 -- 时间复杂度: -- O(N*logN)
//for (int i = 1; i < n; i++)
//{
// AdjustUp(a, i);
//}
//大思路:选择排序,依次选数,从后往前排
//升序 -- 建大堆
//降序 -- 建小堆
//建堆 -- 向下调整建堆 -- 时间复杂度更简单 -- 解释示例1
//建堆 -- 向下调整建堆 -- 时间复杂度: -- O(N)
for (int i =(n-1-1)/2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//选数
int i = 1; //只需要选n-1个数,最后留下的自然是最大或最小的数
while (i < n)
{
Swap(&a[0], &a[n - i]); //把第一个数与最后一个数交换
//向下调整建堆
AdjustDown(a, n - i, 0);
++i;
}
}
//int main()
//{
// //int a[] = { 65,100,70,32,50,60 };
//
// int a[] = { 15,1,19,25,8,34,65,4,27,7 };
// HeapSort(a, sizeof(a) / sizeof(int));
//
// for (size_t i = 0; i < sizeof(a)/sizeof(int); ++i)
// {
// printf("%d ", a[i]);
// }
// printf("\n");
//
// return 0;
//}
//TOP - K问题 -- 解释示例2
//TOP - K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
//比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
//对于Top - K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
// 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
// 1. 用数据集合中前K个元素来建堆
// 前k个最大的元素,则建小堆
// 前k个最小的元素,则建大堆
// 2. 用剩余的N - K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
// 比特就业课
// 将剩余N - K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素
//以文件的方式进行堆排序选数
//测试用例三
void CreateDataFile(const char* filename, int N)
{
FILE* fin = fopen(filename, "w"); //以写文件的方式打开
if (fin == NULL)
{
perror("fopen fail");
return;
}
srand(time(NULL)); //给定时间种子让其随机生成数据
for (int i = 0; i < N ; ++i)
{
fprintf(fin, "%d\n", rand()%1000000); //%的原因是为了测试该项目的正确性,下面可以手动在文件里添加超过一百万的数据,让其测试,看是否可以选出来
}
fclose(fin);
}
void PrintTopK(const char* filename, int k)
{
assert(filename);
FILE* fout = fopen(filename, "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("minHeap fail");
return;
}
//如何读取前k个数据
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &minHeap[i]); //默认空格或者换行键为改数据的终止
}
//建k个数的小堆
for (int j = (k-2)/2; j >= 0; --j)
{
AdjustDown(minHeap, k, j);
}
//继续读取后N-K个数据
int val = 0;
while (fscanf(fout , "%d", &val) != EOF)
{
if (val > minHeap[0])
{
minHeap[0] = val; //直接赋值
AdjustDown(minHeap, k, 0);
}
}
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
free(minHeap);
fclose(fout);
}
int main()
{
const char* filename = "Data.txt";
int N = 10000;
int k = 10;
//CreateDataFile(filename, N);//随机创立一些数据
PrintTopK(filename, k);
return 0;
}
解释示例1
调整次数 = 每一层节点个数 * 这一层最坏向下调整次数
使用向下调整建堆
第1层,2^0个节点,需要向下移动h-1层
第2层,2^1个节点,需要向下移动h-2层
第3层,2^2个节点,需要向下移动h-3层
第4层,2^3个节点,需要向下移动h-4层
……
第h-1层,2^(h-2)个节点,需要向下移动1层
第h层,2^(h-1)个节点,不需要向下移动
向上调整建堆
第1层,2^0个节点,不需要向下移动
第2层,2^1个节点,需要向下移动1次
第3层,2^2个节点,需要向下移动2次
第4层,2^3个节点,需要向下移动3次
……
第h-1层,2^(h-2)个节点,需要向下移动h-2次
第h层,2^(h-1)个节点,需要向下移动h-1次
而且根据二叉树的性质,最后一层一定是占最大节点数的大概有一半,综上所述使用向下调整是最优解
解释示例2
N个数,找k个最大的
1、排序 -- O(N*logN)
2、堆选数
a、建大堆 -- 选K次即可(Pop k次)-- 时间复杂度:O(N+logN*K)
一般而言:K较小,而当N很大的时候就不行了,比如:N = 100亿 K = 100,那么a方法就不行了 -- 空间浪费并且需要注意的是在堆实现中,内存是存不下这么多的数据 例:100亿个整数大约就是40G了,这对于内存来说是一个很大的空间
b、 建小堆
1、用前K个数,建K个数的小堆
2、依次遍历后续N-K个数,比堆顶的数据大,就替换堆顶的数据,向下调整进堆,最后堆里面的数据就是最大的前K个了
时间复杂度:K + logK*(N-K) ≈ O(N) 空间复杂度:O(K)
结束语
上穷碧落下黄泉,两处茫茫皆不见。
唐·白居易 《长恨歌》