DS:二叉树的顺序结构及堆的实现
https://i-blog.csdnimg.cn/blog_migrate/d3e1b85bd539b480e899c353d15dc042.gif创作不易,兄弟们给个三连!!
一、二叉树的顺序存储
https://i-blog.csdnimg.cn/blog_migrate/2fd3dca9eb547007e3811c818b8cded5.png
顺序结构指的是利用数组来存储,一般只适用于表示完全二叉树,缘故原由如上图,存储不完全二叉树会造成空间上的浪费,有的人又会问,为什么图中空的位置不能存储呢??缘故原由是我们必要根据数组的下标关系才气访问到对应的节点!!有以下两个下标关系公式:
1、父亲找孩子:leftchild=parent*2+1,rightchild=parent*2+2
2、孩子找父亲:parent=(child-1)/2 要注意,这边无论用左孩子算还是右孩子算都是可以的,因为一般俩说,(child-1)/2 由于int类型向下取整的特点,所以得到的结果都是一样的!!
所以我们想要上面这种方式去访问节点,并且还不渴望有大量的空间浪费,现实中只有堆才会利用数组存储,二叉树的顺序存储中在物理上是一个数组,再逻辑上是一颗二叉树!!
二、堆的概念及结构
现实中我们把堆(类似完全二叉树)利用顺序结构来存储,要注意这里的堆和操作体系虚拟历程地址空间中的堆是两回事,一个是数据结构,一个是操作体系中管理内存的一块地区分区。
假如有一个关键码的集合k,我们将他的全部元素按照完全二叉树的存储逻辑放在一个一维数组中,则成为堆,根节点最大的堆叫做大堆,根节点最小的堆叫做小堆。
堆的性子:
1、堆中某个节点的值总是不大于或不小于其父节点的值
2、堆总是一颗完全二叉树
https://i-blog.csdnimg.cn/blog_migrate/be0b472b276cac1d934b6540431e890b.png
注意:并不肯定有序
三、堆的实现
假设我们实现小堆
3.1 相关结构体的创建
跟顺序表的形式是一样的,但是换了个名字
typedef int HPDataType;
typedef struct Heap
{
HPDataType * a;
int size;
int capacity;
}Heap; 3.2 堆的初始化
void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
} 3.3 堆的插入
堆的插入很简单,但是我们要包管堆插入后还能维持堆的形状
https://i-blog.csdnimg.cn/blog_migrate/8d67348fe35d4c65832b2fc1381e017f.png
所以我们在插入后,还要举行向上调解,也就是孩子要根据下标关系找到自己的父亲去比力,小就互换
void HeapPush(Heap* php, HPDataType x)
{
assert(php);
//首先要判断是否需要扩容
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* temp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
if (temp == NULL)
{
perror("malloc fail");
exit(1);
}
//扩容成功
php->a = temp;
php->capacity = newcapacity;
}
//扩容后,我们插入这个元素并size++
php->a = x;
//但是插入之后可能会破坏堆的结构,所以我们需要这个元素和他的父辈进行逐个比较,
AdjustUp(php->a,php->size-1);//封装一个向上调整函数,传入数组和新加元素的下标
} 3.4 向上调解算法
void AdjustUp(HPDataType* a, int child)
{
assert(a);
//通过孩子找父亲parent=(child-1)/2
int parent = (child - 1) / 2;
//孩子和父亲开始比较,如果孩子小,就交换,如果孩子大,退出循环
while (child>0)//如果孩子变成了根节点,就没有必要再找了,因为已经没有父母了
//如果用parent>=0来判断,那么由于(0-1)/2是-1/2,取整后还是0,就会一直死循环,所以必须用孩子来当循环条件
{
if (a < a)//孩子小,交换
{
Swap(&a, &a);
//但是交换过后,可能还需要继续往上比,所以我们要让原来的父亲变成孩子,然后再找新的父亲进行比较
child = parent;
parent = (child - 1) / 2;
}
else//孩子大,退出
break;
}
} 注:这里的向上调解算法和后面向下调解算法我们都不用跟堆有关的接口,缘故原由就是这个算法的运用范围很广,可以用在堆排序以及top-k问题中!!
3.5 互换函数
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType temp = *p1;
*p1 = *p2;
*p2 = temp;
} 3.6 堆的删除
一般来说,假如直接删除堆的末了一个元素,实在是没什么意义的,一行代码就可以搞定,没必要封装什么函数,所以这里的堆的删除指的是删除根部的元素!!
https://i-blog.csdnimg.cn/blog_migrate/a1bd63d47cd3b0809e40650bc458a8b1.png
void HeapPop(Heap* php)//一般来说,堆中的删除指的是删除根位置的数据
//如果直接删除根然后往前挪动一位,那么亲缘关系就会十分混乱,为了能够尽量在调整中减少对关系的改变
//我们将根部元素与最后一个元素进行交换之后再删除,此时的根是原先的最后一个元素
//然后将该元素进行向下调整(封装一个函数,传入数组、元素个数、)
{
assert(php);
assert(!HeapEmpty(php));//为空的话没有删除的必要
Swap(&php->a, &php->a);
php->size--;
//开始向下调整
AdjustDown(php->a, php->size,0);
} 3.7 向下调解算法
void AdjustDown(HPDataType* a, int n,int parent)
{
assert(a);
//此时根部为原来的最后一个元素,往下比较
//即通过父亲去找到自己的孩子,如果孩子比自己小,就得交换位置,如果孩子比自己大,就退出
//但是因为父亲有一个左孩子parent*2+1,右孩子parent*2+2,我们选择孩子中较小的和自己交换
int child = parent * 2 + 1;//假设左孩子比右孩子小
while (child<n)//当child超出个数的时候结束
{
if (child+1<n && a<a)//如果右孩子比左孩子小,假设错误,修正错误
//注意,一定不能写反,要注意只有左孩子没有右孩子的情况
child++;
if (a < a)//如果孩子小于父亲,交换
{
Swap(&a, &a);
//交换完后,让原来的孩子变成父亲,然后再找新的孩子
parent = child;
child = parent * 2 + 1;
}
else
break;//如果孩子大于等于父亲,直接退出
}
} 在上述算法中,我们应用了先假设再推翻的方法,一开始我们先假设左孩子比力小,然后我们再给个条件判定,假如左孩子大于右孩子,假设不成立,再推翻,如许可以包管我们的child变量肯定是较小的孩子!!
虽然这里的parent很明显是从a开始,似乎不必要专门去传一个parent的参数,但是这也是为了之后的堆排序做预备!
3.8 取堆顶的数据
HPDataType HeapTop(Heap* php)
{
assert(php);
assert(!HeapEmpty(php));//为空的话没有取的必要
return php->a;
} 3.9 堆的数据个数
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
3.10 堆的判空
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
} 3.11 堆的销毁
void HeapDestory(Heap* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
} 3.12 堆的打印(测试)
我们要实现堆的打印,利用我们之前封装的函数,每获取一次堆顶元素就删除一次,直到堆删完就可以获取全部的元素了!!
#include"Heap.h"
int main()//该方法实现堆的顺序打印
{
Heap hp;
HeapInit(&hp);
int a[] = { 55,100,70,32,50,60 };
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
HeapPush(&hp, a);//不断进堆
while (!HeapEmpty(&hp))
{
int top = HeapTop(&hp);
printf("%d\n", top);
HeapPop(&hp);
}
HeapDestory(&hp);
return 0;
} 前面只是先创建一个堆,从while循环开始才是实现对堆的打印!!
运行结果 :32 50 55 60 70 100
我们发现了一个情况:按原理来说堆只有父子节点之间有大小关系,兄弟之间没有的,但是我们末了打印出来的结果却完成了排序!!!下面我们来举行分析
https://i-blog.csdnimg.cn/blog_migrate/7d6843048a2f49b4f2245fb0d6d2236e.png
总之任何一个堆,我们都可以通过不断地pop去实现它的顺序打印!!堆排序后面会介绍!
四、堆实现的全部代码
4.1 Heap.h
#pragma once#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<stdbool.h>typedef int HPDataType;
typedef struct Heap
{
HPDataType * a;
int size;
int capacity;
}Heap;void Swap(HPDataType* p1, HPDataType* p2);//实现父亲和孩子的互换void AdjustUp(HPDataType* a, int child);//向上调解算法// 堆的初始化void HeapInit(Heap* php);// 堆的插入void HeapPush(Heap* php, HPDataType x);// 堆的删除void HeapPop(Heap* php);// 取堆顶的数据HPDataType HeapTop(Heap* php);// 堆的数据个数int HeapSize(Heap* php);// 堆的判空bool HeapEmpty(Heap* php);// 堆的销毁void HeapDestory(Heap* php); 4.2 Heap.c
#include"Heap.h"//当前实现小堆void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType temp = *p1;
*p1 = *p2;
*p2 = temp;
}void AdjustUp(HPDataType* a, int child)
{
assert(a);
//通过孩子找父亲parent=(child-1)/2
int parent = (child - 1) / 2;
//孩子和父亲开始比较,如果孩子小,就交换,如果孩子大,退出循环
while (child>0)//如果孩子变成了根节点,就没有必要再找了,因为已经没有父母了
//如果用parent>=0来判断,那么由于(0-1)/2是-1/2,取整后还是0,就会一直死循环,所以必须用孩子来当循环条件
{
if (a < a)//孩子小,交换
{
Swap(&a, &a);
//但是交换过后,可能还需要继续往上比,所以我们要让原来的父亲变成孩子,然后再找新的父亲进行比较
child = parent;
parent = (child - 1) / 2;
}
else//孩子大,退出
break;
}
}void AdjustDown(HPDataType* a, int n,int parent)
{
assert(a);
//此时根部为原来的最后一个元素,往下比较
//即通过父亲去找到自己的孩子,如果孩子比自己小,就得交换位置,如果孩子比自己大,就退出
//但是因为父亲有一个左孩子parent*2+1,右孩子parent*2+2,我们选择孩子中较小的和自己交换
int child = parent * 2 + 1;//假设左孩子比右孩子小
while (child<n)//当child超出个数的时候结束
{
if (child+1<n && a<a)//如果右孩子比左孩子小,假设错误,修正错误
//注意,一定不能写反,要注意只有左孩子没有右孩子的情况
child++;
if (a < a)//如果孩子小于父亲,交换
{
Swap(&a, &a);
//交换完后,让原来的孩子变成父亲,然后再找新的孩子
parent = child;
child = parent * 2 + 1;
}
else
break;//如果孩子大于等于父亲,直接退出
}
}void HeapPush(Heap* php, HPDataType x)
{
assert(php);
//首先要判断是否需要扩容
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* temp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
if (temp == NULL)
{
perror("malloc fail");
exit(1);
}
//扩容成功
php->a = temp;
php->capacity = newcapacity;
}
//扩容后,我们插入这个元素并size++
php->a = x;
//但是插入之后可能会破坏堆的结构,所以我们需要这个元素和他的父辈进行逐个比较,
AdjustUp(php->a,php->size-1);//封装一个向上调整函数,传入数组和新加元素的下标
}void HeapPop(Heap* php)//一般来说,堆中的删除指的是删除根位置的数据
//如果直接删除根然后往前挪动一位,那么亲缘关系就会十分混乱,为了能够尽量在调整中减少对关系的改变
//我们将根部元素与最后一个元素进行交换之后再删除,此时的根是原先的最后一个元素
//然后将该元素进行向下调整(封装一个函数,传入数组、元素个数、)
{
assert(php);
assert(!HeapEmpty(php));//为空的话没有删除的必要
Swap(&php->a, &php->a);
php->size--;
//开始向下调整
AdjustDown(php->a, php->size,0);
}HPDataType HeapTop(Heap* php)
{
assert(php);
assert(!HeapEmpty(php));//为空的话没有取的必要
return php->a;
}int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
}void HeapDestory(Heap* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
} 4.3 test.c(测试)
#include"Heap.h"
int main()//该方法实现堆的顺序打印
{
Heap hp;
HeapInit(&hp);
int a[] = { 55,100,70,32,50,60 };
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
HeapPush(&hp, a);//不断进堆
while (!HeapEmpty(&hp))
{
int top = HeapTop(&hp);
printf("%d\n", top);
HeapPop(&hp);
}
HeapDestory(&hp);
return 0;
} 五、堆的应用
5.1 堆排序
要对数组排序前,我们要用堆排序,起首要建堆!
大家看看之前堆的打印时的测试代码逻辑的方法
就是我们得到一个数组,就先建堆,然后先把数组push进去,再pop出来,是可以实现有序的
https://i-blog.csdnimg.cn/blog_migrate/594c510b6a3c149a053789201136f34a.png
但是现在我们的需求不是打印出来,而是将他排好序后放进数组里,所以们可以这么写:
void HeapSort(int* a, int n)
{
HP hp;
HeapInit(&hp);
// N*logN
for (int i = 0; i < n; ++i)
{
HeapPush(&hp, a);
}
// N*logN
int i = 0;
while (!HeapEmpty(&hp))
{
int top = HeapTop(&hp);
a = top;
HeapPop(&hp);
}
HeapDestroy(&hp);
} 这个方法固然是可以的,但是很贫苦,缘故原由如下:
1、每次都要创建一个新的堆,然后再销毁,比力贫苦,而且空间复杂度比力高
2、我通过把数组放进酿成堆,还要再把堆拷贝到数组中,数据的拷贝是很繁琐的!!
所以我们要思索一种方式避免数据的拷贝,所以就有了向上调解建堆和向下调解建堆的方法了!!
也就是我们在原数组的基础上直接建堆,然后向下调解排序即可,下面会详细介绍
5.1.1 向上调解建堆
https://i-blog.csdnimg.cn/blog_migrate/54a4e64bdf96a37438ee88e3ea6203ce.png 假设数组有n个元素
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
} 5.1.2 向下调解建堆
https://i-blog.csdnimg.cn/blog_migrate/00ddd1f1d754fd1a64bbbe5d4f8735ba.png
for (int i = (n-1-1)/2; i >= 0; i--)
{
AdjustDown(a, n, i);
} 5.1.3 堆排序的实现
那我们究竟选择向下建堆好还是向下建堆好呢??我们来分析一下
https://i-blog.csdnimg.cn/blog_migrate/11019ad238d85c34de98238953cb13f0.png
https://i-blog.csdnimg.cn/blog_migrate/ac16c51a815a9dd63501f72dcb11c880.png
所以我们发现向上调解建堆的时间复杂度大概是N*logN,而向下调解建堆的时间复杂度是N
实在们在推导的时候也能发现,向上调解建堆是节点多的情况调解得多,节点少的情况调解的少,次数是多*多+少*少 ,而向下调解建堆是节点多的情况调解得少,节点少的情况调解的多,次数是多*少+少*多,显然是向下调解建堆是更有优势的!!
接下去我们建好堆,就要想着怎么去排序了,我们思索一下,之前我们对堆的打印时,不断pop打印出来有序结果的缘故原由是什么??缘故原由就是pop函数里的向下调解算法!!每一次互换根节点和尾节点,将每个节点举行向下调解,末了就可以得到有序的
https://i-blog.csdnimg.cn/blog_migrate/0cd02502a90d8fe4e43c440872443600.png
因为我们之前实现的向下调解算法是小堆的,所以我们这边来实现一个降序的堆排序算法
void HeapSort(int* a, int n)
{
//降序建小堆
//升序建大堆
for (int i = (n-1-1)/2; i >=0;i--)
AdjustDown(a, n, i);
//开始排序 先交换向下调整
int end = n - 1;
while (end >= 0)
{
Swap(&a, &a);
AdjustDown(a, end, 0);
--end;
}
} https://i-blog.csdnimg.cn/blog_migrate/6a9a9dadd45eb5ba875e73ba77b6d29f.png
假如我们想实现升序,将向下调解算法按照大堆的规则改一下就行
向下调解算法和向上调解算法的空间复杂度都是(logN)
堆排序中,建堆的时间复杂度是o(N),排序的时间复杂度是(N*logN)所以堆排序的总时间复杂度是N*logN
5.2 TOP-K问题
Top-k问题:即求数据中前k个最大的元素或者是最小的元素,一般情况下的数据量都比力大!
比如:专业前10名、世界五百强、富豪榜前十
堆排序可以或许资助我们在大量数据中筛选出最好的几个。
5.2.1 思绪
比如说我们要从1000个学生的成绩中找到前10个分数最高的,方法就是将所有的数据放在一个数组里,直接建大堆,然后pop9次就可以找到了(pop中的向下调解算法可以使得每次pop出去的都是最大值,然后pop9次的缘故原由是因为第10次就可以直接去获取堆顶元素即可)
但是有些情况,上述思绪办理不了,分析:
https://i-blog.csdnimg.cn/blog_migrate/7381835f9a5da4585b015a8b9abf201c.png
5.2.2 通过数组验证TOP-K
void PrintTopK(int* a, int n, int k)
{
//建前k个建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
AdjustDown(a, k, i);
//将剩余n个数据不断与堆顶元素比较,大就交换,然后向下调整
for (int i = k; i < n; i++)
{
if (a > a)
{
a = a;//直接覆盖就行,不用交换
AdjustDown(a, k, 0);
}
}
//打印
for(int i=0;i<k;i++)
printf("%d ", a);
}
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
srand((unsigned int)time(NULL));
for (size_t i = 0; i < n; ++i)
{
a = rand() % 1000000;//随机数范围0-999999
}
// 为了能够方便找到这些数
a = 1000000 + 1;
a = 1000000 + 2;
a = 1000000 + 3;
a = 1000000 + 4;
a = 1000000 + 5;
a = 1000000 + 6;
a = 1000000 + 7;
a = 1000000 + 8;
a = 1000000 + 9;
a = 1000000 + 10;
PrintTopK(a, n, 10);
}
int main()
{
TestTopk();
return 0;
}
https://i-blog.csdnimg.cn/blog_migrate/3ab50d86e9e6a5849015425a2dcfcdfe.png
5.2.3 通过文件验证TOP-K
实在用数组的方法,并不能有效地模仿,我们可以尝试用文件的方式来验证
void CreateNDate()
{
// 造数据
int n = 10000;
srand((unsigned int)time(NULL));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (size_t i = 0; i < n; ++i)
{
int x = rand() % 1000000;
fprintf(fin, "%d\n", x);//将随机数写进文件
}
fclose(fin);
}
void PrintTopK(int k)
{
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL)
{
perror("malloc fail");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &kminheap);//从文件读取数据
}
// 建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
int val = 0;
while (!feof(fout))//feof是文件结束的标识,如果返回1,则说明文件结束
{
fscanf(fout, "%d", &val);//fscaf的光标闪动到原先的位置,所以会从k的位置开始读
if (val > kminheap)
{
kminheap = val;
AdjustDown(kminheap, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap);
}
printf("\n");
}
int main()//该方法实现堆的顺序打印
{
CreateNDate();
PrintTopK(5);
return 0;
} 友友们上述代码有不明白的,看看博主关于文件操作里的函数介绍:
C语言:文件操作详解-CSDN博客
https://i-blog.csdnimg.cn/blog_migrate/d36f4f372abd69fa8d85d76683c6fb51.png 不太好找,所以我们可以先解释创造数据的文件,然后再文件中修该出5个最大数,然后再实行一次函数
https://i-blog.csdnimg.cn/blog_migrate/47d02446e882255dc8e2b73f73159080.pnghttps://i-blog.csdnimg.cn/blog_migrate/a9974703a73a971aa6061441a1f105ea.png
以上就是通过数组验证top和利用文件验证tok的方法!!
https://i-blog.csdnimg.cn/blog_migrate/38834eb7b5eff83976f518b637ccd57a.jpeg
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]