赞
踩
树是什么:
在数据结构中,树是一种数据的存储结构,他的结构像是一个颗倒着的树,一个数只能有一个根,一个根可以有很多树干,从树干往上可以有很多根树杈,树杈上面又可以长出很多树枝,树枝上面可以有很多树叶。每个树都有根,每个树杈都是从树干上长出来的,每个树枝又都是从树杈上长出来的,每个树叶都是从树枝上长出来的。
我们来用一张图解释一下树:
每颗树都只有一个树根,但是可以有很多个树干,树干和树干之间是互不相关的,如果去掉树根,每个树干都可以看做是一颗单独的树,把树干去掉,每颗树杈也可以看做是单独的树,如果一个颗树从树根往上被砍掉,也就是这颗树只有树根,那么这就是一颗空树。
把上图的树倒过来就是数据结构里的树:
树和非树的区别:
树的相关概念:
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林;
二叉树:
从根节点开始,每个节点的子节点不能超过2
二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
下面用一张图片来展示一下什么叫二叉树:
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
我们用二叉树来理解一下大堆和小堆:
大堆:所有的父节点都要大于子节点,所有的子节点都要小于父节点,堆顶的数据,也就是根部是最大的
小堆:所有的父节点都要小于子节点,所有的子节点都要大于父节点,堆顶的数据是最小的
堆总是完全二叉树
通过上面的介绍,我们大致了解了二叉树以及大堆和小堆的概念,那么堆在数据结构中是如何实现的呢?
首先我们用最简单的方式来实现堆 ------ 数组
假设现在有一个小堆:
他的逻辑结构如上图所示,总共有7个接地那,那他的物理结构要放在数组里,就要开辟一个7个int类型的数组。
那是如何放进数组中的:
我们把上面的堆分成3层,每一层的数据按照从左到右,从上到下存入数组中。
再通过一张图片俩分析:
我们来用动态数组实现一下堆的插入和删除:
首先定义一个结构体:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;//元素个数
int capacity;//动态内存大小
}Heap;
初始化:
//初始化
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = 0;
hp->size = 0;
}
插入:
// 堆的插入 void HeapPush(Heap* hp, HPDataType x) { //断言 assert(hp); //当元素个数等于数组大小时 if (hp->capacity == hp->size) { //将数组大小扩容至之前的两倍 //如果是初次扩容,开辟4个空间 int newnode = hp->capacity == 0 ? 4 : hp->capacity * 2; int* p = (HPDataType*)realloc(hp->a,sizeof(HPDataType) * newnode); if (p == NULL) { perror("malloc"); exit(-1); } hp->a = p; hp->capacity = newnode; } //找到堆的底部插入 hp->a[hp->size++] = x; 判断是否需要向上调整,大堆 AdjustUpMax(hp->a,hp->size - 1,hp->size); //判断是否需要向上调整,小堆 //AdjustUpMin(hp->a, hp->size - 1, hp->size); }
这里重点是向上调整部分:AdjustUpMin(向上调整为小堆)
AdjustUpMax(向上调整为大堆)
我们来仔细剖析向上调整:
假设现在有一个空堆,我们要创建一个小堆:
在首次插入元素的时候,用size作为下标,也就是0,那么在插入第二个元素时,下标是1,等于是先插入左子树,然后size再次加1等于2,再插入元素,等于是插入右子树,以此类推,堆是一个完全二叉树,所以不会存在左子树为空,右子树不为空的情况
这个时候数组下标为0的位置就是堆顶,在插入数据后将size++
,记录数组里的元素个数,并且作为下一次插入的下标。
再插入一个2:
2比3要小,也就是子树比父树要大,这时候交换位置:
然后再插入一个1:
1比2小,再次向上调整:
最后插入一个0:
0比3小,向上调整:
调整过后,再次和父节点比较,0比1小,再次调整:
直到调整至下标为0的位置,结束向上调整,这个时候最小的数已经被调整到堆顶,形成小堆
代码实现:
//交换 void Swap(HPDataType* p1, HPDataType* p2) { HPDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } //判断是否需要交换,像上调整,调整为小堆 void AdjustUpMin(HPDataType* a, int child, int n) { //child作为插入元素的下标 //找到child的父节点 int parent = (child - 1) / 2; //当子节点来到堆顶的位置,就不需要再向上调整了 while (child > 0) { //如果子树比父数小 if (a[child] < a[parent]) { //交换 Swap(&a[child],&a[parent]); //向上调整,让子节点等于父节点,成为新的子节点 child = parent; //在利用上面的公式求出父节点的下标 parent = (child - 1) / 2; } //如果插入的数不小于父节点,则不需要调整,直接挑出循环 else { break; } } }
向上调整终止的条件:
向上调整大堆:
和调整小堆的原理是一样的,但是是调整的条件判断要改成:
当子节点大于父节点的时候才发生交换:
//判断是否需要交换,像上调整,调整为大堆 void AdjustUpMax(HPDataType* a, int child, int n) { assert(a); //找到父节点的下标 int parent = (child - 1) / 2; while (child > 0) { //如果孩子小于父亲 if (a[child] > a[parent]) { //交换 Swap(&a[child],&a[parent]); //向上调整 child = parent; parent = (child - 1) / 2; } else { break; } } }
堆的删除:
堆的删除是删除堆顶的数据,假设现在有一个小堆,堆顶的元素就是最小的,如果删除了堆顶的元素,那么让谁来做堆顶?
来分析一下:
小堆的堆顶一定是最小的那个,所要在删除1以后,就要从1的左树和右树当中选出一个较小值来作为新的堆顶,1的左树是2,右树是3,所以要用2来做新的堆顶。
那怎样删除堆顶的元素呢?我们换一种思路,向下调整:
1,先将堆顶的数据和堆顶的数据交换
2,将数组的长度减1,达到删除的效果
3,将堆顶的新数据向下调整
向下调整:
1,从0开始,找到子树和右树较小的那个
2,交换
3,向下调整,找到新的父节点,继续调整
代码实现:
//判断是否需要交换,像下调整小堆 void AdjustDownMin(HPDataType* a, int parent, int n) { assert(a); //找到左子树 int child = parent * 2 + 1; //当子树大于数组长度循环停止 while (child < n) { //child + 1 得到右树,对比找较小值 if (child + 1 < n && a[child] > a[child + 1]) { child++; } //子树小于父数,交换 if (a[child] < a[parent]) { Swap(&a[child],&a[parent]); parent = child; child = child * 2 + 1; } //父数小于子树,跳出循环 else { break; } } } // 堆的删除 void HeapPop(Heap* hp) { assert(hp); if (hp->size == 0) { printf("堆为空\n"); return; } //将堆顶数据和堆底数据交换,size-- Swap(&hp->a[0],&hp->a[hp->size - 1]); hp->size--; 再将堆顶的数据向下调整,大堆 //AdjustDownMax(hp->a,0,hp->size); //再将堆顶的数据向下调整,小堆 AdjustDownMin(hp->a, 0, hp->size); }
大堆向下调整:
和小堆的原理一样,只需要在判断是否需要交换时修改条件,子节点大于父节点才交换
//判断是否需要交换,像下调整大堆 void AdjustDownMax(HPDataType* a, int parent, int n) { assert(a); //找到左子树 int child = parent * 2 + 1; //找到右子树 /*int child = (parent + 1) * 2;*/ while (child < n) { //找出左子树和右子树大的那个节点 if (child + 1 < n && a[child] < a[child + 1]) { child++; } /*if (child < n && a[child] < a[child - 1]) { child--; }*/ if (a[parent] < a[child]) { Swap(&a[parent],&a[child]); parent = child; child = parent * 2 + 1; } else { break; } } }
//找出左子树和右子树大的那个节点
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
为什么在找较小值要加上 child + 1 < n 这个条件呢?
假设,堆的最后一个数据是左子树,如果加一就超过了数组长度,所以child + 1必须小于数组的长度
假设现在有一个数组,数组中有10个元素,
【1,2,3,4,5,6,7,8,9,10】
如何将这组数据变成大堆
首先来分析一下思路,将这个数组转换成堆,逻辑层面是这样的:
怎样才能让这颗数有序:
1,暴力解法
可以通过上面的HeapPush函数来排序,新建一个堆,将数组的元素依次插入堆,让HeapPush函数取向上调整,然后将堆拷贝到数组,达到排序的效果:
// 对数组进行堆排序,大堆 void HeapSortMax(int* a, int n) { //暴力解法 //新建一个堆,将数组的元素依次插入 //使用HeapPush自动排序 Heap hp; HeapInit(&hp); int i = 0; for (i = 0; i < n; i++) { HeapPush(&hp,a[i]); } //将堆拷贝至原来的数组 memcpy(a,hp.a,sizeof(a[0]) * n); }
2,排序算法:
1,向上调整法:
我们用一个树来做演示:
(排成小堆)
核心思想是,从下标为1的位置开始向上调整,然后把每个下标的位置都向上调整一遍,达到排序的效果
代码实现:
// 对数组进行堆排序,小堆
void HeapSortMin(int* a, int n)
{
int i = 0;
//从下标为1的位置开始,依次向上调整
for (int i = 1; i < n; i++)
{
//用i作为下标,向上调整
AdjustUpMin(a, i,n);
}
}
排成大堆:
只需将AdjustUpMin换成AdjustUpMax即可
// 对数组进行堆排序,大堆
void HeapSortMin(int* a, int n)
{
int i = 0;
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
//建大堆,向下调整
AdjustDownMin(a, i, n);
}
}
向下调整法:
核心思想:
从堆的最后一个数据的父节点开始向下调整,也就是堆的倒数第二层,然后依次排序,直到排序到堆顶,排序结束
从上面的向上调整和向下调整两种方法的图片来看,向下调整的时间复杂度明显要优于向上调整
代码实现:
// 对数组进行堆排序,小堆
void HeapSortMin(int* a, int n)
{
int i = 0;
//找到数组最后一个数据的父节点
//当父节点小于0时,排序完成
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
//建大堆,向下调整
//AdjustDownMax(a, i, n);
//建小堆,向下调整
AdjustDownMin(a,i,n);
}
}
将堆排成升序或者降序:
从上图看到,两个堆一个是小堆,一个是大堆,但是将他们放入数组中排列是时不规则的,将堆排成升序和降序的意思就是,将一个堆放入数组展示,能展现出升序和降序
思路:
如果要排序成升序,首先要让堆成为大堆,这样堆顶的元素就是最大的,然后将堆顶的元素和堆底最后一个元素交换,这样最大的元素就到了最末尾,然后依次向前推,交换堆顶的元素,向下排序,最后就会成为升序
如果要排序成降序,首先要让堆成为小堆,这样堆顶的元素就是最小的,然后将堆顶的元素和堆底的最后一个元素交换,这样最小的元素就到了最末尾,然后依次向前推,交换堆顶的元素,向下排序,最后就会成为降序
假设我们要将左边的小堆排成升序:
1,让其变成大堆,让最大的数到堆顶
2,用一个变量控制下标,从堆底最后一个数据开始向前
,依次和堆顶的数据交换,向下排序
3,当将整个堆的元素都和堆顶的元素交换,向下排序一次,就会成为升序
上图:
代码实现:
//堆排序 void HeapSort(Heap* hp) { //先排成大堆 int i = 0; for (i = 1; i < hp->size; i++) { AdjustUpMax(hp->a,i,hp->size); } printf("排成大堆:"); HeapPrint(hp); //排升序 int end = hp->size - 1; int y = 0; while (end > 0) { //将顶部和底部交换 Swap(&hp->a[0], &hp->a[end]); //堆顶最大的数被换到末尾,end--,末尾最大的数不参与排序 end--; //向下排序,选出最大的放在堆顶 AdjustDownMax(hp->a, 0, end); printf("第%d次排序:",++y); HeapPrint(hp); } }
同理,如果是要排成降序,首先要排成小堆,然后去堆顶最小的数替换到堆末尾,在使用end作为下标,依次向前推进,不断的将次小的数替换下来,最终成为降序
降序代码实现:
//堆排序 void HeapSort(Heap* hp) { 先排成小堆 int i = 0; for (i = (hp->size - 1 - 1) / 2; i >= 0; i--) { AdjustDownMin(hp->a,i,hp->size); } //排降序 int end = hp->size - 1; while (end > 0) { //将顶部和底部交换,最小的数被换到末尾 Swap(&hp->a[0],&hp->a[end]); //向下排序,再找最小的数 AdjustDownMin(hp->a,0,end); end--; } }
Topk问题:
在一组数据中找出最大的K个数,排成小堆或这找出最小的K个数,排成大堆。
也就是说将最大或者最小的K个数找出来,排成大堆或者小堆
思路:
假设要找出,最大的K个数,排成小堆:
1,先将数组中任意K个数放入堆中
2,将堆排成小堆
3,遍历数组,用数组中的每一个元素和堆顶的数据比较,当数组中的数据比堆顶数据要大时,替换,然后向下排序
4,遍历完数组,最大的K个数已经到了堆中
原理:将任意K个数排成小堆,这样堆顶的数据就是最小的,如果这个时候有比堆顶大的数据,就替换下来,然后再向下调整,这样堆底的数据永远都是堆中最小的。
代码实现:
// 找最大的前K个,建立K个数的小堆 void MaxTopKMin(Heap* hp,int* a, int n, int k) { //先将数组的任意前k个数放入堆 HeapInit(hp); hp->a = (int*)realloc(hp->a,sizeof(int) * k); hp->capacity = k; hp->size = k; memcpy(hp->a,a,sizeof(int) * k); //让堆变成小堆 int i = 0; for (i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDownMin(hp->a,i,k); } //遍历数组,遇到比堆底大的数据就替换下来 //然后向下排序 i = 0; while (i < n) { //替换 if (a[i] > hp->a[0]) { hp->a[0] = a[i]; //向下排序 AdjustDownMin(hp->a, 0, k); } i++; } }
同理,找出最小的K个数,排成大堆
1,将任意K个数放入堆中
2,排成大堆
3,只要比堆顶的数小,就替换
4,向下排序,保证堆顶的数据是堆中最大的
代码实现:
// 找最小的前K个,建立K个数的大堆 void MinTopkMax(Heap* hp, int* a, int n, int k) { HeapInit(hp); hp->capacity = k; hp->size = k; hp->a = (int*)realloc(hp->a,sizeof(int) * k); memcpy(hp->a,a,sizeof(int) * k); //让堆成为大堆 int i = 0; for (i = (k - 1 - 1) / 2; i >= 0; i--) { //调整 AdjustDownMax(hp->a,i,k); } //此时堆顶的数据是最大的 HeapPrint(hp); i = 0; while (i < n) { //只要比堆顶的数据小,替换,然后向下排序 if (a[i] < hp->a[0]) { hp->a[0] = a[i]; //向下调整 AdjustDownMax(hp->a,0,k); } i++; } }
堆问题的全部代码:
Heap.h
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> #include<string.h> typedef int HPDataType; typedef struct Heap { HPDataType* a; int size;//元素个数 int capacity;//动态内存大小 }Heap; //交换 void Swap(HPDataType* p1,HPDataType* p2); //打印堆 void HeapPrint(Heap* hp); //初始化 void HeapInit(Heap* hp); // 堆的构建 void HeapCreate(Heap* hp, HPDataType* a, int n); // 堆的销毁 void HeapDestory(Heap* hp); // 对数组进行堆排序,小堆 void HeapSortMin(int* a, int n); // 对数组进行堆排序,大堆 void HeapSortMax(int* a, int n); // 对数组进行堆排序,小堆 void HeapSortMin(int* a, int n); //判断是否需要交换,像上调整,调整为大堆 void AdjustUpMax(HPDataType* a, int child, int n); //判断是否需要交换,像上调整,调整为小堆 void AdjustUpMin(HPDataType* a, int child, int n); // 堆的插入 void HeapPush(Heap* hp, HPDataType x); //判断是否需要交换,像下调整大堆 void AdjustDownMax(HPDataType* a, int parent, int n); //判断是否需要交换,像下调整小堆 void AdjustDownMin(HPDataType* a, int parent, int n); // 堆的删除 void HeapPop(Heap* hp); // 取堆顶的数据 HPDataType HeapTop(Heap* hp); // 堆的数据个数 int HeapSize(Heap* hp); // 堆的判空 bool HeapEmpty(Heap* hp); // 找最大的前K个,建立K个数的小堆 void MaxTopKMin(Heap* hp,int* a, int n, int k); // 找最小的前K个,建立K个数的大堆 void MinTopkMax(Heap* hp,int* a, int n, int k); //堆排序 void HeapSort(Heap* hp);
Heap.c
#define _CRT_SECURE_NO_WARNINGS #include"Heap.h" //初始化 void HeapInit(Heap* hp) { assert(hp); hp->a = NULL; hp->capacity = 0; hp->size = 0; } // 堆的构建 void HeapCreate(Heap* hp, HPDataType* a, int n) { //方法1,将数组的元素依次使用HeapPush插入到堆 /*int i = 0; for (i = 0; i < n; i++) { HeapPush(hp,a[i]); }*/ //方法2,建堆算法 //1,向下调整 //先找到堆的最后一个数据,然后找到他的父节点 //依次向下调整 int i = 0; //for (i = (n - 1 - 1) / 2; i >= 0; i--) //{ // //分成成数个小数,向下调整 // //AdjustDownMax(a,i,n);//大堆 // AdjustDownMin(a,i,n);//小堆 //} //2,向上调整 //假设下标为0的堆顶数据是有序,从下标为1的位置开始 //依次向上调整 for (i = 1; i < n; i++) { //向上调整,大堆 //AdjustUpMax(a,i,n); //向上调整,小堆 AdjustUpMin(a,i,n); } hp->a = (int*)realloc(hp->a,sizeof(int) * n); hp->capacity = n; hp->size = n; memcpy(hp->a,a,sizeof(int) * n); } //打印堆 void HeapPrint(Heap* hp) { if (hp->size == 0) { printf("堆为空\n"); return; } int i = 0; for (i = 0; i < hp->size; i++) { printf("%d ",hp->a[i]); } printf("\n"); } //交换 void Swap(HPDataType* p1, HPDataType* p2) { HPDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } //判断是否需要交换,像上调整,调整为大堆 void AdjustUpMax(HPDataType* a, int child, int n) { assert(a); //找到父节点的下标 int parent = (child - 1) / 2; while (child > 0) { //如果孩子小于父亲 if (a[child] > a[parent]) { //交换 Swap(&a[child],&a[parent]); //向上调整 child = parent; parent = (child - 1) / 2; } else { break; } } } //判断是否需要交换,像上调整,调整为小堆 void AdjustUpMin(HPDataType* a, int child, int n) { int parent = (child - 1) / 2; while (child > 0) { if (a[child] < a[parent]) { Swap(&a[child],&a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } // 堆的插入 void HeapPush(Heap* hp, HPDataType x) { //断言 assert(hp); //当元素个数等于数组大小时 if (hp->capacity == hp->size) { //将数组大小扩容至之前的两倍 //如果是初次扩容,开辟4个空间 int newnode = hp->capacity == 0 ? 4 : hp->capacity * 2; int* p = (HPDataType*)realloc(hp->a,sizeof(HPDataType) * newnode); if (p == NULL) { perror("malloc"); exit(-1); } hp->a = p; hp->capacity = newnode; } //找到堆的底部插入 hp->a[hp->size++] = x; 判断是否需要向上调整,大堆 //AdjustUpMax(hp->a,hp->size - 1,hp->size); //判断是否需要向上调整,小堆 AdjustUpMin(hp->a, hp->size - 1, hp->size); } //判断是否需要交换,像下调整大堆 void AdjustDownMax(HPDataType* a, int parent, int n) { assert(a); //找到左子树 int child = parent * 2 + 1; //找到右子树 /*int child = (parent + 1) * 2;*/ while (child < n) { //找出左子树和右子树大的那个节点 if (child + 1 < n && a[child] < a[child + 1]) { child++; } /*if (child < n && a[child] < a[child - 1]) { child--; }*/ if (a[parent] < a[child]) { Swap(&a[parent],&a[child]); parent = child; child = parent * 2 + 1; } else { break; } } } //判断是否需要交换,像下调整小堆 void AdjustDownMin(HPDataType* a, int parent, int n) { assert(a); //找到左子树 int child = parent * 2 + 1; //当子树大于数组长度循环停止 while (child < n) { //child + 1 得到右树,对比找较小值 if (child + 1 < n && a[child] > a[child + 1]) { child++; } //子树小于父数,交换 if (a[child] < a[parent]) { Swap(&a[child],&a[parent]); parent = child; child = child * 2 + 1; } //父数小于子树,跳出循环 else { break; } } } // 堆的删除 void HeapPop(Heap* hp) { assert(hp); if (hp->size == 0) { printf("堆为空\n"); return; } //将堆顶数据和堆底数据交换,size-- Swap(&hp->a[0],&hp->a[hp->size - 1]); hp->size--; 再将堆顶的数据向下调整,大堆 //AdjustDownMax(hp->a,0,hp->size); //再将堆顶的数据向下调整,小堆 AdjustDownMin(hp->a, 0, hp->size); } // 取堆顶的数据 HPDataType HeapTop(Heap* hp) { assert(hp); if (hp->size == 0) { printf("堆为空\n"); } return hp->a[0]; } // 堆的数据个数 int HeapSize(Heap* hp) { assert(hp); if (hp->size == 0) { printf("堆为空\n"); exit(-1); } return hp->size; } // 堆的判空 bool HeapEmpty(Heap* hp) { assert(hp); return hp->size == 0; } // 对数组进行堆排序,大堆 void HeapSortMax(int* a, int n) { 暴力解法 新建一个堆,将数组的元素依次插入 使用HeapPush自动排序 //Heap hp; //HeapInit(&hp); //int i = 0; //for (i = 0; i < n; i++) //{ // HeapPush(&hp,a[i]); //} 将堆拷贝至原来的数组 //memcpy(a,hp.a,sizeof(a[0]) * n); //排序算法 //找到堆底的数据,再找到堆底数据的父节点 int i = 0; //从下标为1的位置开始,依次向上调整 /*for (int i = 1; i < n; i++) { AdjustUpMax(a, i,n); }*/ for (i = (n - 1 - 1) / 2; i >= 0; i--) { //建大堆,向下调整 AdjustDownMax(a, i, n); } } // 对数组进行堆排序,小堆 void HeapSortMin(int* a, int n) { int i = 0; //从下标为1的位置开始,依次向上调整 /*for (int i = 1; i < n; i++) { AdjustUpMin(a, i,n); }*/ //找到数组最后一个数据的父节点 //当父节点小于0时,排序完成 for (i = (n - 1 - 1) / 2; i >= 0; i--) { //建小堆,向下调整 AdjustDownMin(a, i, n); } } // 找最大的前K个,建立K个数的小堆 void MaxTopKMin(Heap* hp,int* a, int n, int k) { //先将数组的任意前k个数放入堆 HeapInit(hp); hp->a = (int*)realloc(hp->a,sizeof(int) * k); hp->capacity = k; hp->size = k; memcpy(hp->a,a,sizeof(int) * k); //让堆变成小堆 int i = 0; for (i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDownMin(hp->a,i,k); } //遍历数组,遇到比堆底大的数据就替换下来 //然后向下排序 i = 0; while (i < n) { //替换 if (a[i] > hp->a[0]) { hp->a[0] = a[i]; //向下排序 AdjustDownMin(hp->a, 0, k); } i++; } } // 找最小的前K个,建立K个数的大堆 void MinTopkMax(Heap* hp, int* a, int n, int k) { HeapInit(hp); hp->capacity = k; hp->size = k; hp->a = (int*)realloc(hp->a,sizeof(int) * k); memcpy(hp->a,a,sizeof(int) * k); //让堆成为大堆 int i = 0; for (i = (k - 1 - 1) / 2; i >= 0; i--) { //调整 AdjustDownMax(hp->a,i,k); } //此时堆顶的数据是最大的 HeapPrint(hp); i = 0; while (i < n) { //只要比堆顶的数据小,替换,然后向下排序 if (a[i] < hp->a[0]) { hp->a[0] = a[i]; //向下调整 AdjustDownMax(hp->a,0,k); } i++; } } //堆排序 void HeapSort(Heap* hp) { //先排成大堆 int i = 0; /*for (i = (hp->size - 1 - 1) / 2; i >= 0; i--) { AdjustDownMax(hp->a,i,hp->size); }*/ for (i = 1; i < hp->size; i++) { AdjustUpMax(hp->a,i,hp->size); } printf("排成大堆:"); HeapPrint(hp); //排升序 int end = hp->size - 1; int y = 0; while (end > 0) { //将顶部和底部交换 Swap(&hp->a[0], &hp->a[end]); //堆顶最大的数被换到末尾,end--,末尾最大的数不参与排序 //向下排序,选出最大的放在堆顶 AdjustDownMax(hp->a, 0, end); end--; printf("第%d次排序:",++y); HeapPrint(hp); } 先排成小堆 //int i = 0; //for (i = (hp->size - 1 - 1) / 2; i >= 0; i--) //{ // AdjustDownMin(hp->a,i,hp->size); //} ///*for (i = 1; i < hp->size; i++) //{ // AdjustUpMin(hp->a,i,hp->size); //}*/ //HeapPrint(hp); 排降序 //int end = hp->size - 1; //while (end > 0) //{ // //将顶部和底部交换,最小的数被换到末尾 // Swap(&hp->a[0],&hp->a[end]); // //向下排序,再找最小的数 // AdjustDownMin(hp->a,0,end); // end--; //} }
首先来了解一下二叉树的物理结构:
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;//数据
struct BinaryTreeNode* left;//左子树
struct BinaryTreeNode* right;//右子树
}BTNode;
用一个结构体作为二叉树的根,然后再结构体里定义两个结构体指针来做左子树,和右子树
用结构体存储树和用数组存储树是两种完全不同的结构,当left,right为NULL时,表示下面没有节点。
二叉树的遍历分为4种:
1,前序遍历:根 => 左子树 =>右子树
2,中序遍历:左子树 =>根 =>右子树
3,后序遍历:左子树 =>右子树 =>根
4,层序遍历:将二叉树的物理层面展开,像堆那样存入数组,然后遍历
我们来用4张图来解释上面的遍历方式:
1,前序遍历
前序遍历的原则是先访问根,再访问左子树,最后是右子树
从图看,1是根,左子树是2,右子树是3,当访问完1后,访问1的左子树2,这个时候,要把2也看做是一颗树,那么2就是根,所以根据前序遍历的原则,访问2。
再往下走,访问2的左子树4,而4又可以看做是一颗树,再次用前序遍历原则,先访问根,也就是4,再访问4的左树,4的左树是NULL,也就是叶子节点,然后访问4的右树,4的右树也是NULL
访问完2的左子树,接下来访问2的右子树,2的右子树是NULL
2已经被访问完了,再访问1的右子树,1的右子树是3,3又可以当做是一颗树,访问3的根,再访问3的左子树,3的左子树是NULL,再访问3的右子树5,5又被当成是一个树,访问5的根,再访问5的左子树NULL,再访问5的右子树NULL.
访问完5的右树,整颗树就已经遍历完毕了
(二叉树遍历不论是前,中后都要保持一个原则,除了叶子节点,其他的节点都要单独当成一颗树,然后再遵循遍历原则去遍历)
2,中序遍历
3,后序遍历
具体代码实现:
// 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root) { //当root等于空时,返回 if (root == NULL) { printf("NULL->"); return; } //打印自身节点 printf("%d->",root->data); //左子树 BinaryTreePrevOrder(root->left); //右子树 BinaryTreePrevOrder(root->right); } // 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root) { if (root == NULL) { printf("NULL->"); return; } BinaryTreeInOrder(root->left); printf("%d->",root->data); BinaryTreeInOrder(root->right); } // 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root) { if (root == NULL) { printf("NULL->"); return; } BinaryTreePostOrder(root->left); BinaryTreePostOrder(root->right); printf("%d->",root->data); }
验证一下:
//创建节点 BTNode* Add(int x) { BTNode* p = (BTNode*)malloc(sizeof(BTNode)); if (p == NULL) { exit(-1); } p->data= x; p->left = NULL; p->right = NULL; return p; } void test1() { //构建二叉树 BTNode* p1 = Add(1); BTNode* p2 = Add(2); BTNode* p3 = Add(3); BTNode* p4 = Add(4); BTNode* p5 = Add(5); p1->left = p2; p1->right = p3; p2->left = p4; p3->right = p5; //前序遍历 printf("前序遍历:"); BinaryTreePrevOrder(p1); printf("\n"); //中序遍历 printf("中序遍历:"); BinaryTreeInOrder(p1); printf("\n"); //后序遍历 printf("后序遍历:"); BinaryTreePostOrder(p1); printf("\n"); } int main() { test1(); return 0; }
验证结果:
二叉树层序遍历:
这玩意可有点上头了!!!
常规的递归思路不太好玩这个
我们可以借助队列来实现
思路:
1,创建一个队列,将根先插入队列
2,保存队列头部元素,再将队列头部出队列
3,将实现保存好的队列头部的左右数再入队列
上图:
代码实现:
// 层序遍历 void BinaryTreeLevelOrder(BTNode* root) { //借助队列来实现 //1,先将根入队列 //2,保存队列头部 //3,头部出队列,将左子树和右子树带入 //创建队列并初始化 Queue p; QueueInit(&p); //根入队列 QueuePush(&p,root); //队列不为空继续 while (!QueueEmpty(&p)) { //保存队头数据 BTNode* ptr = QueueFront(&p); //打印数据 printf("%d->", ptr->data); //队头出列 QueuePop(&p); //子树不为空 //将ptr的左右子树带入队列 if(ptr->left) QueuePush(&p,ptr->left); if(ptr->right) QueuePush(&p,ptr->right); } }
队列函数实现:
typedef BTNode* QDataType; // 链式结构:表示队列 typedef struct QListNode { QDataType data;//树 struct QListNode* next;//下一个节点 }QNode; // 队列的结构 typedef struct Queue { QNode* front;//队列头部 QNode* rear;//队列尾部 int size;//记录元素个数 }Queue; // 初始化队列 void QueueInit(Queue* q) { assert(q); //全部初始化为空 q->front = NULL; q->rear = NULL; q->size = 0; } // 队尾入队列 void QueuePush(Queue* q, QDataType data) { assert(q); //申请空间 QNode* p = (QNode*)malloc(sizeof(QNode)); if (p == NULL) { perror("malloc"); exit(-1); } //将值赋给p p->data = data; p->next = NULL; //第一次入队列 if (q->front == NULL) { //第一次入队,两个指针同时指向栈底 q->front = q->rear = p; } else { //将尾部的next和新节点连接 q->rear->next = p; q->rear = p; } q->size++; } // 队头出队列 void QueuePop(Queue* q) { assert(q); //判断是否只有一个节点 if (q->front == q->rear) { q->front = NULL; q->rear = NULL; } else { //保留头部 QNode* p = q->front; q->front = q->front->next; //释放头部 free(p); } q->size--; } // 获取队列头部元素 QDataType QueueFront(Queue* q) { assert(q); //判断队列是否为空 if (QueueEmpty(q)) { printf("队列为空\n"); exit(-1); } //返回队头指针的数据 return q->front->data; } // 检测队列是否为空,如果为空返回true,如果非空返回false bool QueueEmpty(Queue* q) { assert(q); //这里可以加上数据个数和尾部指针,也可以不加 //保险一定可以都加上 return q->front == NULL && q->rear == NULL; } // 销毁队列 void QueueDestroy(Queue* q) { assert(q); while (q->front) { //保留上一个节点,再销毁下一个节点 QNode* p = q->front; q->front = q->front->next; free(p); } q->size = 0; q->rear = NULL; }
1,求二叉树节点的个数
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
//当为空的时候返回0
if (root == NULL)
{
return 0;
}
return BinaryTreeSize(root->left) +
BinaryTreeSize(root->right) + 1;
}
2,二叉树的高度
//二叉树的高度 int BinaryTreeHight(BTNode* root) { if (root == NULL) { return 0; } //左树高度 int i = BinaryTreeHight(root->left) + 1; //右树高度 int j = BinaryTreeHight(root->right) + 1; //返回最高的那个 return i > j ? i : j; }
3,二叉树的叶子节点个数
// 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root) { //如果为NULL,不能再继续向下找左树和右树 if (root == NULL) { return 0; } //一个节点的左右子树都为NULL就是叶子节点 if (root->left == NULL && root->right == NULL) { return 1; } //以根作为分割,将左树和右树的叶子节点加起来 return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right); }
4,二叉树查找值为X的节点
// 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x) { if (root == NULL) { return NULL; } if (root->data == x) { //root无法返回到最初的递归函数处 return root; } //创建两个指针变量来接收返回结果 BTNode* p1 = BinaryTreeFind(root->left,x); BTNode* p2 = BinaryTreeFind(root->right,x); //这里要加上p1 != NULL 这个条件 //如果不加上,假如p1是NULL,是不能去访问他的内部数据 if (p1 != NULL && (p1->data = x)) { return p1; } if (p2 != NULL && (p2->data = x)) { return p2; } //如果左树和右树都没有找到,返回NULL return NULL; }
5,二叉树第K层节点个数
// 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k) { //为空说明不是节点 if (root == NULL) { return 0; } //如果节点不为NULL,并且k = 1,说明走到k层 //这个时候返回1 if (k == 1) { return 1; } /*注意,这里函数的第二个参数不能用k--,--k,或者在前面提前将K-1 因为从左右子树的K层去找,如果先将k-1,那么第二个函数将会往后 一层*/ //将左右数的K层节点加起来返回 return BinaryTreeLevelKSize(root->left,k - 1) + BinaryTreeLevelKSize(root->right,k - 1); }
6,判断是否为完全二叉树
完全二叉树的概念:1.叶子结点只会在最后两层出现, 2.当某个结点左右孩子为空或者右孩子为空时,后面所有结点孩子均为空。
上图:
思路:
借助队列,先将根部节点入队,然后保留队列头部节点,将队列头部出队列,将之前保留的节点的左右子树带入队列,依次循环,当遇到第一个为NULL的节点时,不再入队,再遍历队列里剩下的数据,如果有不为NULL的节点,则不是完全二叉树
上图:
代码实现:
// 判断二叉树是否是完全二叉树 bool BinaryTreeComplete(BTNode* root) { //借用队列来实现 //1,首先根节点入队列 //2,然后pop队列首元素 //3,再将新队头的子树入队列 //4,直到遇到空,停止入队 //5,然后一直pop //6,如果是完全二叉树,队列最后一个NULL后面不会有其他数据 Queue p; QueueInit(&p); QueuePush(&p, root); //队列为空停止 while (!QueueEmpty(&p)) { //保存队头 BTNode* ptr = QueueFront(&p); //队头出列 QueuePop(&p); //当遇到第一个NULL时,停止循环 if (ptr == NULL) { break; } //将之前出队的队头的左树和右树入队列 QueuePush(&p,ptr->left); QueuePush(&p,ptr->right); } while (!QueueEmpty(&p)) { //队列有一个不为空不是完全二叉树 //保存队头 BTNode* ptr = QueueFront(&p); if (ptr != NULL) { //销毁队列 QueueDestroy(&p); return false; } //队头出列 QueuePop(&p); } //销毁队列 QueueDestroy(&p); return true; }
1,二叉树的创建
假设现有一个数组 int arr[] = {1,2,4,0,0,5,0,7,0,0,3,8,0,0,9,0,0};
(0代表NULL)
用数组里的元素创建一个二叉树:
思路:
通过前序遍历数组,依次创建节点,组成一个二叉树
创建好的二叉树逻辑图:
通过前序遍历,递归实现
//二叉树创建 //数组 //数组长度 BTNode* BinaryTreeCreate(int* arr, int* i) { //等于0,表示NULL if (arr[*i] == 0) { //这里的i++必须放在括号内,不能放在判断语句中(arr[*i++] == 0) //不然每次判断都会让i往后走 //只有等于0时,才往后跳过 (*i)++; return NULL; } //创建节点 BTNode* p = (BTNode*)malloc(sizeof(BTNode)); //赋值 p->data = arr[(*i)++];//i++,让数组往后走,达到前序遍历的效果 //递归左子树 p->left = BinaryTreeCreate(arr, i); //递归右子树 p->right = BinaryTreeCreate(arr, i); //返回其实节点 return p; }
2,二叉树的销毁
使用后序遍历是最优的方式:
// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
//后序遍历
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
//走完左树,走完右树,再销毁节点
free(root);
root = NULL;
}
单值二叉树
代码实现:
bool isUnivalTree(struct TreeNode* root){
if(root == NULL)
{
return true;
}
//如果左子树不为NULL,并且和自身节点的值相等
if(root->left != NULL && root->left->val != root->val)
return false;
//如果右子树不为NULL,并且和自身节点的值相等
if(root->right != NULL && root->right->val != root->val)
return false;
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
二叉树的最大深度
代码实现:
int maxDepth(struct TreeNode* root){
if(root == NULL)
{
return NULL;
}
//记录左树高度
int i = maxDepth(root->left) + 1;
//记录右树高度
int j = maxDepth(root->right) + 1;
//返回最高的那颗数的深度
return i > j ? i : j;
}
相同的树
代码实现:
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
//两个都是NULL,相同
if(p == NULL && q == NULL)
return true;
//当上面的if判断没有进去,如果有一个为NULL,肯定不相等
if(p == NULL || q == NULL)
return false;
//都不为NULL,再进行对比
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
二叉树的前序遍历
代码实现:
//计算二叉树节点个数 int TreeSize(struct TreeNode* root) { if(root == NULL) { return 0; } return TreeSize(root->left) + TreeSize(root->right) + 1; } //将二叉树的节点数据放入数组 void AddTree(struct TreeNode* root,int* arr,int* i) { if(root == NULL) return; arr[(*i)++] = root->val; AddTree(root->left,arr,i); AddTree(root->right,arr,i); } int* preorderTraversal(struct TreeNode* root, int* returnSize){ //求出树的节点个数 int i = TreeSize(root); //开辟动态内存 int* arr = (int*)malloc(sizeof(int) * i); *returnSize = i; int j = 0; //将树的节点放入数组 AddTree(root,arr,&j); //返回数组 return arr; }
翻转二叉树
代码实现:
struct TreeNode* invertTree(struct TreeNode* root){
if(root == NULL)
return NULL;
//交换左右子树
struct TreeNode* p = root->left;
root->left = root->right;
root->right = p;
invertTree(root->left);
invertTree(root->right);
//返回根节点
return root;
}
对称二叉树
代码实现:
方法1:
struct TreeNode* invertTree(struct TreeNode* root){ if(root == NULL) return NULL; //交换左右子树 struct TreeNode* p = root->left; root->left = root->right; root->right = p; invertTree(root->left); invertTree(root->right); //返回根节点 return root; } bool isSameTree(struct TreeNode* p, struct TreeNode* q){ //两个都是NULL,相同 if(p == NULL && q == NULL) return true; //当上面的if判断没有进去,如果有一个为NULL,肯定不相等 if(p == NULL || q == NULL) return false; //都不为NULL,再进行对比 if(p->val != q->val) return false; return isSameTree(p->left,q->left) && isSameTree(p->right,q->right); } bool isSymmetric(struct TreeNode* root){ //先将root的一颗子树翻转 struct TreeNode*p = invertTree(root->left); //再用左树和右树对比 bool i = isSameTree(p,root->right); return i; }
方法2:
bool isSameTree(struct TreeNode* p, struct TreeNode* q){ //两个都是NULL,相同 if(p == NULL && q == NULL) return true; //当上面的if判断没有进去,如果有一个为NULL,肯定不相等 if(p == NULL || q == NULL) return false; //都不为NULL,再进行对比 if(p->val != q->val) return false; //用p的左树对比q的右树 //用p的右树,对比q的左树 //达到对称对比效果 return isSameTree(p->left,q->right) && isSameTree(p->right,q->left); } bool isSymmetric(struct TreeNode* root){ //直接对比左右子树 return isSameTree(root->left,root->right); }
另一颗树的子树
代码实现:
//比较 bool _test(struct TreeNode* p1,struct TreeNode* p2) { if(p1 == NULL && p2 == NULL) return true; if(p1 == NULL || p2 == NULL) return false; if(p1->val != p2->val) return false; return _test(p1->left,p2->left) && _test(p1->right,p2->right); } bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){ //如果两个都为NULL,就是一样的 if(root == NULL && subRoot == NULL) return true; //如果有一个为NULL,一个不为NULL,肯定不相等 if(root == NULL || subRoot == NULL) return false; //两个都不为NULL,进行比较 if(_test(root,subRoot)) return true; //左树和右树,有一个相等返回true return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。