文章目录
- 一、 什么是二叉树
- 二、 二叉树的存储结构
- 顺序存储视图
- 三、 堆
- 堆的结构及概念
- 大堆和小堆
- 四、 建堆
- 五、 堆排序
- 六、 topk问题
一、 什么是二叉树
二叉树,作为一种重要的数据结构,由节点组成,每个节点可以有两个子节点,通常称为左子节点和右子节点。二叉树是有序的,树中包含的各个节点的度不能超过2,即只能是0、1或者2。
特殊二叉树
- 满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是
说,如果一个二叉树的层数为K,且结点总数是 2的k次方-1,则它就是满二叉树。- 完全二叉树
完全二叉树,作为一种效率很高的数据结构,是由满二叉树衍生出来的。一棵深度为K且有n个结点的二叉树,如果其每个节点都与深度为K的满二叉树中编号从1至n的节点一一对应,则这棵二叉树被称为完全二叉树。
二、 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
- 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树, 因为不是完全二叉树会有空间的浪费。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
用数组存储的方式更方便查找根和子树。 - 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是
链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
顺序存储视图
三、 堆
堆的结构及概念
如果有一个关键码的集合K ={ k0,k1,k2……,k(n-1)}【0,1,2,……,n-1这些都是下标】,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=k(2i+1)且Ki<=k2i+2【Ki>=k(2i+1)且Ki>=k(2i+2)】i=0,1,2…,则称为小堆【或大堆】。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
大堆和小堆
大堆:树中父亲的数据都大于等于孩子;
小堆:树中父亲的数据都小于等于孩子
四、 建堆
小堆为例
创建两个文件:
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 HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
bool HeapEmpty(Heap* hp);
// 对数组进行堆排序
void HeapSort(int* a, int n);
//交换位置
Swap(HPDataType* p1, HPDataType* p2);
//堆排序
void HeapSort(int* a, int n);
//向下交换
void AdjustDown(HPDataType* a, int n, int parent);
//向上查找
void Adjustup(HPDataType* a, HPDataType child);
Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//堆的初始化
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
assert(hp->size > 0);
return hp->a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
// 堆的判空
bool HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
//交换位置
Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向上调整
Adjustup(HPDataType* a, HPDataType 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 HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
//扩容
if (hp->capacity == hp->size)
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
Adjustup(hp->a, hp->size - 1);//插入完数据后向上调整
}
//向下调整
AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;//假设左孩子小
while (child < n)//child >= n,说明孩子已经调整到叶子了
{
//防止越界访问,找到小的那个孩子再向下调
if (child + 1 < n && a[child] > a[child + 1])
{
++child;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;//继续往下调整
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 堆的删除
void HeapPop(Heap* hp)
{
//Pop删除堆顶的数据(根位置)
assert(hp);
assert(hp->size > 0);
//不能直接暴力删除,否则关系会乱套,兄弟变父子
Swap(&hp->a[0], &hp->a[hp->size-1]);//第一个数据和最后一个数据交换
hp->size--;
AdjustDown(hp->a, hp->size, 0);//删除完需要用到向下调整算法
}
五、 堆排序
void HeapSort(int* a, int n)
{
// 降序,建小堆
// 升序,建大堆
//建小堆
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);//将最小的数和最后的数进行交换
AdjustDown(a, end, 0);
--end;
}
}
int main()
{
int a[] = { 4,2,8,1,5,6,9,7 };
HeapSort(a, sizeof(a) / sizeof(0));
}
调试更能理解这个过程
六、 topk问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
假如我们现在想要找到100亿个整数中,取出前K个最大的数,如果我们直接对100亿个整数进行排序至少需要40G的内存,这会造成空间上巨大的浪费。
我们可以先取K个数建立一个小堆,再将后100亿-K个数依次与堆顶元素相比较,如果比堆顶元素大就将其替换后重新向下调整为一个小堆,再接着与下一个数相比,这样最终就可以找到前K个最大的整数了。(节约了大量的空间)
该方法的时间复杂度为:O(N*㏒⑵K)
空间复杂度为O(K)
![void CreateNDate()
{
// 造数据
int n = 100000;
srand(time(0));
const char* file = "F:\\vs2022\\二叉树_堆\\二叉树_堆\\data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; i++)
{
int x = (rand() + i);
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void PrintTopK()
{
int k = 0;
printf("请输入要查找的前K个值>:");
scanf("%d", &k);
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL)
{
printf("malloc fali");
return;
}
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
printf("fopen error");
return;
}
//读取前k个数
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &kminheap[i]);
}
//建堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
//读取剩下的N-K个数
int x = 0;
while (fscanf(fout, "%d", &x) > 0)
{
if (x > kminheap[0])
{
kminheap[0] = x;
}
AdjustDown(kminheap, k, 0);
}
for (int i = 0; i < k; i++)\
{
printf("%d ", kminheap[i]);
}
printf("\n");
}
——————————————————————————————————————————
希望这篇博客对你有所帮助!!!