当前位置: 首页 > news >正文

【数据结构】顺序结构二叉树详解

一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。

一、堆的概念与结构

如果有一个关键码的集合 K={k0​,k1​,k2​,…,kn−1​},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Ki​ <= K(2∗i + 1)​(Ki ​>= K(2 ∗ i + 1)​且Ki​ <= K(2 ∗ i + 2)​,i = 0、1、2...,则称为小堆 (或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆

堆具有以下性质

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。
  • 堆顶是最值(最大值 / 最小值)

二叉树性质

  • 对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:
    1. 若 i>0,i 位置结点的双亲序号:(i−1)/2;i=0,i 为根结点编号,无双亲结点
    2. 若 2i+1<n,左孩子序号:2i+1,2i+1>=n 否则无左孩子
    3. 若 2i+2<n,右孩子序号:2i+2,2i+2>=n 否则无右孩子

这里我们给出手绘的示意图来加强理解

二、堆的实现

2.1、Heap.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//定义堆结构
typedef int HPDataType;typedef struct Heap {HPDataType* arr;int size;//有效数据个数int capacity;//空间大小
}HP;//堆的初始化
void HPInit(HP* php);//堆的销毁
void HPDesTroy(HP* php);//交换数据
void Swap(int* x, int* y);//向上调整算法
void AdjustUp(HPDataType* arr, int child);//向下调整算法
//n是堆里面的结点个数,如果越界了就不需要调整了
void AdjustDown(HPDataType* arr, int parent, int n);//打印堆
void HPPrint(HP* php);//往堆里插入数据
void HPPush(HP* php, HPDataType x);//删除数据
void HPPop(HP* php);//判断堆是否为空
bool HPEmpty(HP* php);//取堆顶
HPDataType HPTop(HP* php);
2.2、Heap.c
#include"Heap.h"//堆的初始化
void HPInit(HP* php)
{assert(php);php->arr = NULL;php->size = php->capacity = 0;
}//堆的销毁
void HPDesTroy(HP* php)
{assert(php);if (php->arr)free(php->arr);php->arr = NULL;php->size = php->capacity = 0;
}//打印堆
void HPPrint(HP* php)
{for (int i = 0; i < php->size; i++){printf("%d ", php->arr[i]);}printf("\n");
}//交换数据
void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{int parent = (child - 1) / 2;while (child > 0){//建大堆: >//建小堆: <if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);child = parent;parent = (child - 1) / 2;}else{//不用调整符合情况break;}}
}//向下调整算法
//n是堆里面的结点个数,如果越界了就不需要调整了
void AdjustDown(HPDataType* arr, int parent, int n)
{int child = parent * 2 + 1;while (child < n){//建大堆: <//建小堆: >if (child + 1 < n && arr[child] < arr[child + 1]){child++;}//孩子和父亲比较//建大堆: >//建小堆: <if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}//往堆里插入数据
void HPPush(HP* php, HPDataType x)
{assert(php);//空间不够要增容if (php->size == php->capacity){//增容int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* tmp = (HPDataType*)realloc(php->arr, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}php->arr = tmp;php->capacity = newcapacity;}//空间足够php->arr[php->size] = x;php->size++;//向上调整//数组下标是从0开始的,所以要size - 1AdjustUp(php->arr, php->size - 1);
}//判断堆是否为空
bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}//出堆顶
//先让堆顶与最后一个数据交换,然后size--
//找child中较大的一个与parent交换,以此类推
void HPPop(HP* php)
{assert(!HPEmpty(php));Swap(&php->arr[0], &php->arr[php->size - 1]);--php->size;//堆顶数据需要向下调整AdjustDown(php->arr, 0, php->size);
}//取堆顶
HPDataType HPTop(HP* php)
{assert(!HPEmpty(php));return php->arr[0];
}
2.3、test.c
#include"Heap.h"void test01()
{HP hp;HPInit(&hp);HPPush(&hp, 25);HPPush(&hp, 15);HPPush(&hp, 10);HPPush(&hp, 80);HPPrint(&hp);while (!(HPEmpty(&hp))){int top = HPTop(&hp);printf("%d ", top);HPPop(&hp);}HPPop(&hp);HPPrint(&hp);HPDesTroy(&hp);
}//冒泡排序
void BubbleSort(int* arr, int n)
{for (int i = 0; i < n; i++){for (int j = 0; j < n - 1 - i; j++){if (arr[j] > arr[j + 1]){Swap(&arr[j], &arr[j + 1]);}}}
}//堆排序 -- 这不是真的堆排序
//这直接使用了数据结构中的堆
void HeapSort01(int* arr, int n)
{HP hp;//-------使用了数据结构 -- 堆HPInit(&hp);//调用push将数组中的数据放入到堆中for (int i = 0; i < n; i++){HPPush(&hp, arr[i]);}//得到了一个堆结构int i = 0;while (!HPEmpty(&hp)){int top = HPTop(&hp);arr[i++] = top;HPPop(&hp);}HPDesTroy(&hp);
}//堆排序 -- 使用堆结构的思想来进行堆排序
void HeapSort02(int* arr, int n)
{//乱序数组 -- 建堆//先从最后一个结点开始开始调整,假设有6个数据,数组第一个元素下标是0//最后一个数据的下标便是5,即n-1,我们要根据孩子求父亲便是(k - 1)/2,这里的k是n - 1for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(arr, i, n);}////向上调整算法//for (int i = 0; i < n; i++)//{//	AdjustUp(arr, i);//}//拿堆顶和最后一个数据进行交换,然后向下调整//这里的例子是排降序,建的小堆//如果要排升序,我们要去建大堆int end = n - 1;while (end > 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end);end--;}
}int main()
{//test01();int arr[6] = { 25,15,10,56,70,30 };printf("排序之前: \n");for (int i = 0; i < 6; i++){printf("%d ", arr[i]);}printf("\n");HeapSort02(arr, 6);printf("排序之后: \n");for (int i = 0; i < 6; i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

这里我们重点关注Heap.c文件,我们来详解一下向上调整算法和向下调整算法:

2.4、向上调整算法

这个我们是在"堆的插入"里使用到的:

将新数据插入到数组的尾上,再进行向上调整算法,直到满足堆。

向上调整算法

  • 先将元素插入到堆的末尾,即最后一个孩子之后
  • 插入之后如果堆的性质遭到破坏,将新插入结点顺着其双亲往上调整到合适位置即可

void AdjustUp(HPDataType* a, int child) {int parent = (child - 1) / 2;while (child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}
}
void HPPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}

计算向上调整算法建堆时间复杂度:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果)

由此可得:

向上调整算法建堆时间复杂度为:O(n ∗ log ​n)(log是以2为底)

2.5、向下调整算法

这个我们是在"堆的删除"里使用到的:

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

向下调整算法

  • 将堆顶元素与堆中最后一个元素进行交换
  • 删除堆中最后一个元素
  • 将堆顶元素向下调整到满足堆特性为止

void AdjustDown(HPDataType* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){// 假设法,选出左右孩⼦中⼩的那个孩⼦if (child + 1 < n && a[child + 1] > a[child]){++child;}if (a[child] > a[parent])Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
void HPPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}

计算向下调整算法建堆时间复杂度:

向下调整算法建堆时间复杂度为:O(n)

三、堆的应用

3.1、堆的排序
版本一:基于已有数组建堆、取堆顶元素完成排序版本
// 1、需要堆的数据结构
// 2、空间复杂度 O(N)
void HeapSort(int* a, int n)
{HP hp;for (int i = 0; i < n; i++){HPPush(&hp, a[i]);}int i = 0;while (!HPEmpty(&hp)){a[i++] = HPTop(&hp);HPPop(&hp);}HPDestroy(&hp);
}

该版本有一个前提,必须提供有现成的数据结构堆

版本二:数组建堆,首位交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次大的数据
// 升序,建⼤堆
// 降序,建⼩堆
// O(N*logN)
void HeapSort(int* a, int n)
{// a数组直接建堆 O(N)for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}// O(N*logN)int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}

堆排序时间复杂度计算:

堆排序时间复杂度为:O(n * logn)

3.2、TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 

假设现在有10亿个整数,需要申请多大内存?

1G = 1024 MB = 1024 * 1024 KB = 1024 * 1024 *1024 byte,10亿 * 4 = 4G

假设只有1G内存怎么办?

这时候我们可能会想到采用循环的方式来利用那1G内存,那假设现在只有1KB内存怎么办?

图解:创建一个堆,堆中只有k个数据,我们建小堆,遍历剩下的n - k个数据,比堆顶要大就和堆顶交换。

要是找最小的前k个数据,建大堆,遍历剩下的数据和堆顶比,比堆顶要大就和堆顶交换。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 (可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1)用数据集合中前 K 个元素来建堆前 k 个最大的元素,则建小堆前 k 个最小的元素,则建大堆

2)用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素

#include<stdio.h>
void CreateNDate()
{// 造数据int n = 100000;srand(time(0));const char* file = "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) % 1000000;fprintf(fin, "%d\n", x);}fclose(fin);
}
void topk()
{printf("请输⼊k:>");int k = 0;scanf("%d", &k);const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen error");return;}int val = 0;int* minheap = (int*)malloc(sizeof(int) * k);if (minheap == NULL){perror("malloc error");return;}for (int i = 0; i < k; i++){fscanf(fout, "%d", &minheap[i]);}// 建k个数据的⼩堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(minheap, k, i);}int x = 0;while (fscanf(fout, "%d", &x) != EOF){// 读取剩余数据,⽐堆顶的值⼤,就替换他进堆if (x > minheap[0]){minheap[0] = x;AdjustDown(minheap, k, 0);}}for (int i = 0; i < k; i++){printf("%d ", minheap[i]);}fclose(fout);
}

时间复杂度:O(N)= k + (n − k)log k(log以2为底) 

http://www.dtcms.com/a/544341.html

相关文章:

  • 魔法自助建站网站建设需要准备什么软件
  • 酒吧网站设计大连网站建设ewaylife
  • Oracle_E_Business 远程命令执行漏洞复现(CVE-2025-61882)
  • P1228 地毯填补问题(分治)
  • 苍穹外卖 —— 文件上传和菜品的CRUD
  • 高性能MySql阅读
  • 第3章、MySQL乱码的解决-字符集
  • Ubuntu 安装 Rancher
  • 什么是慢SQL
  • 【人工智能数学基础】多元高斯分布
  • 做网站加入广告联盟做网站的前端是做什么
  • 郑州网页网站制作汕头网站推广优化
  • python电影票房数据可视化分析系统 不同档期电影票房Flask框架 艺恩电影票房网站 requests爬虫(建议收藏)✅
  • webrtc弱网-VivaceUtilityFunction源码分析与算法原理
  • 科技行业ERP系统选择指南:Oracle NetSuite的全面解析
  • 第一个程序HelloWorld
  • 数据分析过程中,发现数值缺失,怎么办?
  • 电商网站设计图海口网站建设好
  • 【自动化测试函数 (上)】Web自动化测试实战精要:定位、操作与窗口管理三部曲
  • 超越传统管理:迈向无感衔接、全域协同的医美运营新范式
  • SUB设备电子狗加密狗开发
  • 1.1 神经网络基本组成
  • HarmonyOS 应用开发:Scroll滚动容器的深度性能优化
  • Java支付对接策略模式详细设计
  • 项目实践6—全球证件智能识别系统(Qt客户端开发+FastAPI后端人工智能服务开发)
  • 微软重磅发布开源引擎Microsoft Agent Framework
  • Qt 高级进阶-MVC架构实现客户端和插件交互(串口案例)
  • 本地部署开源物联网平台 ThingsBoard 并实现外部访问( Windows 版本)
  • leetcode--hot100--思路+知识点(I)
  • 兑吧集团总部大楼乔迁新址 焕新起航开启发展新篇