1. 树概念及结构
1.1 树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
- 因此,树是递归定义的。
任何一个结点都包含0~N个孩子结点,结点可以分成自身结点和子树(跟自己同样结构定义的树)两部分
例如:A结点可以分成自身A结点,B子树,C子树,D子树。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
上图的数据结构类型都不是树,而是图的概念
1.2 树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;
其中节点的层次是可以自己定义的,若如上所述:将根定义为第一层,那么树的深度/高度就是4 ,空树的深度为0;(更加符合日常生活中的认知) ,同样也可以将根定义为第0层,那么树的深度/高度就是3,空树的深度为-1;
拓展:为什么数组的下标要从0开始?
因为数组访问的是内存地址空间,例如,访问a[0],其实等价与*(a+0)
a表示数组首元素的地址,在首元素的地址上再加0 正好契合地址的访问
1.3 树的表示
树的表示方法多种多样:
1.3.1 静态数组
规定孩子结点的上限为N,当面对超过N个孩子结点时无法存储,而叶子结点没有孩子结点时,空间浪费严重,所以通常不采用
1.3.2 顺序表
通过struct TreeNode* 指向struct TreeNode的孩子结点,再创建一个数组存储这些struct TreeNode* ,所以是二级指针的形式
实现动态存储孩子结点
1.3.3 孩子兄弟表示法
typedef int DataType;
struct TreeNode
{
struct TreeNode* child; // 左边第一个孩子结点
struct TreeNode* brother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};
child 指向左边的第一个孩子结点,而brother用于链接下一个兄弟结点
同一层的孩子结点都通过brother链接起来,而访问下一层就访问第一个孩子结点(child)开始
这种方式实现,树的结构可以还原出来,也不会出现环路的情况。
1.3.4 双亲表示法
通过数组的方式实现,单纯的存储父亲结点的下标,适合结点找祖先的情况
1.4 树在实际中的应用
通常用来表示操作系统中的文件系统的目录树结构
2. 二叉树概念及结构
2.1 概念
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.2 现实中的二叉树:
左图为标准的满二叉树,右图为完全二叉树
2.3 特殊的二叉树:
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
也就是说,如果一个二叉树的层数为K,且结点总数是2k -1,则它就是满二叉树。- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。
对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
要注意的是满二叉树是一种特殊的完全二叉树。
3. 堆
gitee代码提交:堆的代码实现
3.1 堆的概念及结构
这里的堆指的也是数据结构中的一种,要和操作系统中的堆(指的是进程地址内存区域的划分)区分开
3.2 堆的代码实现(重点)
实现堆过程中出现的问题:
01 堆结构体声明
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
使用类似顺序表:动态开辟数组的方式实现堆
02 初始化接口
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
03 插入接口(重点)
要实现插入接口首先要保证插入数据后,还得保持堆的数据结构特征:小根堆:双亲<孩子 / 大根堆:双亲>孩子
这里就需要用到向上调整算法
// 交换两结点的值要传 HPDataType* 类型
// 更标准的写法
//void Swap(HPDataType* p1, HPDataType* p2)
//{
// HPDataType tmp = *p1;
// *p1 = *p2;
// *p2 = tmp;
//}
void Swap(HPDataType* a, int parent, int child)
{
int tmp = a[child];
a[child] = a[parent];
a[parent] = tmp;
}
// 向上调整算法
void AdjustUp(HP* php, int child)
{
//向上和父节点进行比较
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
// 小跟堆(要实现大根堆将 > 换成 < 即可)
if (php->a[parent] > php->a[child])
{
// 交换
Swap(php->a, parent, child);
// 往上遍历
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapPush(HP* php, HPDataType x)
{
assert(php);
// 扩容
if (php->capacity == php->size)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(int));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->capacity = newCapacity;
php->a = tmp;
}
// 1.插入结点
php->a[php->size] = x;
php->size++;
// 2.调整
AdjustUp(php, php->size - 1);
}
04 弹出接口(重点)
因为堆主要实现的是Topk(前k大/前k小)问题,所以弹出实现的是将堆顶元素(最小/最大元素)弹出
如果只是单纯的将数组往前覆盖可以实现嘛?
很明显无法采用,这里就需要采用向下调整算法:
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
若要实现向下调整算法,要保证左右子树都为小根堆 / 大根堆
void AdjustDown(HP* php, int parent)
{
// 找最小孩子
// 假设左孩子为最小孩子
int minchild = parent * 2 + 1;
while (minchild < php->size)
{
//若 右孩子 < 左孩子
if (php->a[minchild + 1] < php->a[minchild] && minchild + 1 < php->size)
{
// 最小孩子换成右孩子
minchild = parent * 2 + 2;
}
//上述确定最小孩子
if (php->a[parent] > php->a[minchild])
{
Swap(php->a, parent, minchild);
parent = minchild;
minchild = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(php->a, 0, php->size - 1);
php->size--;
AdjustDown(php, 0);
}
05 销毁接口
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->size = php->capacity;
}
06 判空接口
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
07 返回堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
08 打印接口
void HeapPrint(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
09 堆大小接口
int HeapSize(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->size;
}
10 测试接口
void TestHeap()
{
// 创建数组
int a[] = { 65, 100, 70, 32, 50, 60 };
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
{
// 将整个数组插入进去
HeapPush(&hp, a[i]);
}
HeapPrint(&hp);
HeapPop(&hp);
HeapPrint(&hp);
}
看打印的结果是否符合堆的数据结构特征
验证发现,符合!成功实现堆
其实我们发现通过实现堆也可以快速的实现堆排序:
4. 堆排序
如果在堆创建完毕的情况下,实现堆排序仅仅是几行代码,但是当我们遇到未建堆的数组,不可能通过现场实现堆的方式(代价太大:首先要实现堆数据结构,然后不断调用接口Push插入堆)
那么如何快速建堆呢?
gitee代码提交:堆排序的代码实现
4.1 建堆
这里采取的向下调整建堆,建小堆后只是保证堆顶的元素为最小值,子树还不是有序的
4.2 建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
所以向上调整和向下调整建堆,相对而言向下调整建堆更优
4.3 升序选大堆还是小堆
升序 – 大堆
降序 – 小堆
5. TopK问题
建堆选数:时间复杂度为O(N)的算法
TopK问题建小堆选数(文件版)的代码实现
gitee代码提交:Heap_TopK 堆解决TopK问题
01 创建N个随机数的Data文件
void CreateDataFile(const char* filename, int N)
{
//写入
FILE* fin = fopen(filename, "w");
if (fin == NULL)
{
perror("fopen fail");
exit(-1);
}
//随机种子
srand((unsigned int)time(NULL));
for (int i = 0; i < N; i++)
{
//输入N个随机数
fprintf(fin, "%d\n", rand() % 10000);
// 所有数在0~10000 内 再手动插入10个大于10000的数 看选数过后是否选的出来
}
fclose(fin);
}
int main()
{
const char* filename = "Data.txt";
//从N个数当中选出前K大
int N = 10000;
int K = 10;
// 创建N个数的文件
CreateDataFile(filename, N);
return 0;
}
该函数只能调用一次,因为是“w”的形式打开文件,若文件有内容会进行删除重写
02 读取写入Data当中的数据
void PrintTopK(const char* filename, int K)
{
assert(filename);
//读取数据
FILE* fout = fopen(filename, "r");
if (fout == NULL)
{
perror("read fail");
exit(-1);
}
以只读的方式打开filename == “Data.txt” 的文件
03 开辟K个空间建堆
//将前K个数据取出
int* minHeap = (int*)malloc(sizeof(int) * K);
if (minHeap == NULL)
{
perror("malloc fail");
exit(-1);
}
// 将文件前K个数插入minHeap
for (int i = 0; i < K; i++)
{
fscanf(fout, "%d", &minHeap[i]);
}
//将K个数建堆 (K-2)/2 最后一个叶子结点
for (int j = (K - 2) / 2; j >= 0; j--)
{
//向下调整建堆
AdjustDown(minHeap, K, j);
}
04 选N-K次数
//继续读取后N-K
int val = 0;
while (fscanf(fout, "%d", &val) != EOF)
{
// 选出比当前堆大的元素 放入堆中
if (val > minHeap[0])
{
minHeap[0] = val;
AdjustDown(minHeap, K, 0);
}
}
经过前几步的操作,当前 minHeap 内数据为前K个最大数
05 将堆内数据打印
for (int i = 0; i < K; i++)
{
printf("%d ", minHeap[i]);
}
free(minHeap);
fclose(fout);
}