二叉树的存储结构(顺序存储)—— 数据结构与算法

在这里插入图片描述
请添加图片描述

😶‍🌫️Take your time ! 😶‍🌫️
💥个人主页:🔥🔥🔥大魔王🔥🔥🔥
💥代码仓库:🔥🔥魔王修炼之路🔥🔥
💥所属专栏:🔥魔王的修炼之路–数据结构🔥
如果你觉得这篇文章对你有帮助,请在文章结尾处留下你的点赞👍和关注💖,支持一下博主。同时记得收藏✨这篇文章,方便以后重新阅读。


文章目录

  • 一、前言
  • 二、二叉树的顺序存储结构
    • 1、二叉树采取顺序存储的原因
    • 2、堆的概念及结构
  • 三、堆的实现
    • 1、介绍
    • 2、向上调整建堆
      • 说明
      • 实现
    • 3、向下调整建堆
      • 说明
      • 实现
    • 4、向上向下调整时间复杂度
    • 5、堆的插入
    • 6、堆的删除
    • 7、总代码
      • Heap.h
      • Heap.c
      • Test.c
  • 四、堆的应用
    • 堆排序
      • 介绍
      • 实现
    • TOP-K问题
  • 五、总结

一、前言

上一篇博客详细讲述了二叉树的概念,那么接下来这篇博客将讲述二叉树的存储方式的一种:顺序存储。

二、二叉树的顺序存储结构

1、二叉树采取顺序存储的原因

  • 普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
    在这里插入图片描述

2、堆的概念及结构

  • 堆可以这么理解:它是一个完全二叉树,并且完全二叉树都可以转变为堆,为什么说堆是特殊的二叉树呢?因为堆其实就是把这个完全二叉树变得有规律了。

堆分为大根堆和小根堆。
大根堆:所有父节点都不小于子节点。
小根堆:所有父节点都不大于子节点。
需要注意的是:兄弟节点之间没有什么联系,可以大于可以小于也可以相等。

在这里插入图片描述

三、堆的实现

1、介绍

实现堆的方法:有两种,分别为向上调整建堆和向下调整建堆。

  • 需要注意的是,无论是向上调整还是向下调整,我们都需要保证的前提是除了要调整的这个元素外,其余节点本身就是一个堆(大根堆或小根堆),也就是说是在原本堆的基础上进行的向下和向上调整。

2、向上调整建堆

说明

向上调整建堆:就像刚才说的,我们进行调整时其本身必须为一个堆,因此我们只能一点一点的调整,所以从第二个开始向上调整(因为第一个没有比较的东西,所以从第二个开始调整),使每次比较都能是在原本就是堆的基础上进行。如图:从红色位置开始向上调整建堆。
在这里插入图片描述

实现

  • 建大堆
    在这里插入图片描述
//向上调整建大堆
#include <stdio.h>void AdjustUp(int* a,int n)
{int child = n;int parent = (child - 1) / 2;while (child > 0)//判断条件不能为parent,因为parent为整型,但是最后那个结果是-0.5,对于整型来说,浮点数小数点后的忽略,那么此时parent还是0,child也变成0了,它们的值会相等然后return,虽然这样程序照样过了,但是这种想法其实是错的,所以我们通过判断child是否>0来作为是否结束的标志。{if (a[child] > a[parent]){int temp = a[child];a[child] = a[parent];a[parent] = temp;}elsereturn;//如果子节点小于父亲节点那么本次循环就不用比了,因为再向上比的那些之前已经调整过了。child = parent;parent = (parent - 1) / 2;}
}int main()
{int a[7] = { 2,5,6,7,4,9,3 };int size = sizeof(a) / sizeof(int);int n = 1;while (n < size){AdjustUp(a, n);n++;}for (int n = 0; n < size; n++){printf("%d ", a[n]);}return 0;
}

3、向下调整建堆

说明

向下调整建堆:和向上调整很像,也必须一点一点的建,只不过是从下面开始,我们跳过最后一层,层倒数第二层开始逐个向下调整(确保除了要调整的这个,其余参与的节点本身构成一个堆)。如图:
在这里插入图片描述

实现

  • 大根堆

在这里插入图片描述

//向下调整建大堆
#include <stdio.h>
void AdjustDown(int* a, int n,int size)
{int parent = n;int child = n * 2 + 1;//先假设左子树是孩子中较大的,然后再进行判断,看是否替换为右子树(右子树需要存在)。while (child < size){if (child + 1 < size && a[child] < a[child + 1])//第一个判断的原因是首先右子树要存在才能比较。{child = child + 1;}if (a[parent] < a[child]){int temp = a[parent];a[parent] = a[child];a[child] = temp;}parent = child;child = child * 2 + 1;}
}
int main()
{int a[7] = { 2,5,6,7,4,9,3};int size = sizeof(a) / sizeof(int);int n = (size - 2) / 2;while (n >= 0){AdjustDown(a, n,size);n--;}n = 0;while (n < size){printf("%d ", a[n]);n++;}return 0;
}
  • 就像上面两幅图,虽然相同的数采用向上和向下调整建堆的方法建出来的堆不太一样,但是都是大根堆。

4、向上向下调整时间复杂度

  • 向上调整建堆时间复杂度:O(N*logN) --向上调整建堆时层的结点个数少时向上的层数少(也就相当于每个遍历的次数少),层的结点个数多时向上的层数多(每个节点遍历的次数多),因此时间复杂度大。
  • 向下调整建堆时间复杂度:O(N) --向下调整建堆时层的结点个数多时向下的层数少,层的结点个数少时向下的层数多,所以会比向上调整效率高。
  • 向下调整时间复杂度证明
    在这里插入图片描述

向上调整与这个证明过程类似。

5、堆的插入

先将这个元素插入到尾部,然后让这个数据进行向上调整就行了。

6、堆的删除

让根与最后一个元素交换位置,然后让数组删除最后一个元素(size–),然后让这个取代根位置的元素进行向下调整即可。

7、总代码

Heap.h

#pragma oncetypedef int HeapDateType;#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef struct Heap
{HeapDateType* a;int size;int capacity;
}Heap;//堆的初始化
void HeapCreat(Heap* php);//堆的销毁
void HeapDestory(Heap* php);//堆的插入
void HeapPush(Heap* php, HeapDateType x);//堆的删除
void HeapPop(Heap* php);//取堆顶的数据
HeapDateType HeapTop(Heap* php);//堆的数据个数
int HeapSize(Heap* php);//堆的判空
int HeapEmpty(Heap* php);

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1#include "Heap.h"//堆的初始化
void HeapInit(Heap* php)
{assert(php);php->a = (HeapDateType*)malloc(sizeof(HeapDateType) * 4);if (php->a == NULL){perror("malloc error");assert(php);}php->size = 0;php->capacity = 4;
}//堆的销毁
void HeapDestory(Heap* php)
{assert(php);free(php->a);php->capacity = 0;php->size = 0;
}//数据交换
void Swap(HeapDateType* a, HeapDateType* b)
{int temp = 0;temp = *b;*b = *a;*a = temp;
}void AdjustUp(HeapDateType* a, int n)
{assert(a);int child = n - 1;int parent = (child - 1) / 2;while (child > 0)//判断条件不能为parent,因为parent为整型,但是最后那个结果是-0.5,对于整型来说,浮点数小数点后的忽略,那么此时parent还是0,child也变成0了,它们的值会相等然后return,虽然这样程序照样过了,但是这种想法其实是错的,所以我们通过判断child是否>0来作为是否结束的标志。{if (a[child] > a[parent]){Swap(&a[child], &a[parent]);//后面还会用到交换,所以封装成一个函数。}elsereturn;child = parent;parent = (parent - 1) / 2;}
}//堆的插入
void HeapPush(Heap* php,HeapDateType x)
{assert(php);if (php->capacity == php->size){HeapDateType* inspect = realloc(php->a, sizeof(HeapDateType) * php->capacity * 2);if (inspect == NULL){perror("realloc error");assert(inspect);}php->a = inspect;inspect = NULL;php->capacity *= 2;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size);
}//向上向下调整的前提是必须调整前是一个堆(大堆或者小堆)
void AdjustDown(HeapDateType* a, int n,int parent)
{assert(a);int child = parent*2+1;//默认向下调整是跟左孩子交换,如果下面比较左右孩子大小时右孩子大,那么让child+1即可表示与右孩子交换。while (child <= n){if (child < n&& (a[child] < a[child + 1]))//前提是要有右兄弟,不然就会越界{child = child + 1;}if (a[parent] < a[child]){Swap(&a[parent], &a[child]);}elsereturn;parent = child;child = child * 2 + 1;}
}//堆的删除(删除的是堆顶,也就是根)
void HeapPop(Heap* php)
{assert(php);assert(!HeapEmpty(php));Swap(&php->a[php->size - 1], & php->a[0]);//直接让最后一个覆盖根也行,因为根是要删掉的,就算这样交换了,下面size--后,根还是没有了。php->size--;AdjustDown(php->a, php->size,0);
}//取堆顶的数据
HeapDateType HeapTop(Heap* php)
{assert(php);return php->a[0];
}//堆的数据个数
int HeapSize(Heap* php)
{assert(php);return php->size;}//堆的判空
int HeapEmpty(Heap* php)
{assert(php);return php->size == 0;
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1#include "Heap.h"int main()
{Heap hp;HeapInit(&hp);HeapPush(&hp, 0);HeapPush(&hp, 47);HeapPush(&hp, 26);HeapPush(&hp, 48);HeapPush(&hp, 52);HeapPush(&hp, 79);HeapPush(&hp, 6);HeapPush(&hp, 83);HeapPush(&hp, 38);HeapPush(&hp, 24);HeapPop(&hp);HeapPop(&hp);HeapPop(&hp);HeapPop(&hp);return 0;
}

四、堆的应用

堆排序

介绍

  • 虽然我们知道堆分为大根堆和小根堆,子节点和父节点有明确的大小关系,但因为兄弟节点的大小关系不确定,随意实际在数组中并不是顺序存放的,我们利用堆里删除元素的思想对堆进行排序。
  • 规律:升序建大堆,降序建小堆!
  • 原因如下:

对于升序建大堆来说:我们采用的方法是每次将根与最后一个元素交换位置,然后让这个堆忽略掉末尾元素(size–)(也就是原来的根,因为它已经到了应该的位置了),然后一直这样,直到这个堆只剩下一个元素,那么此时就排好了。
在这里插入图片描述

  • 注意:堆排序首先是要建堆

实现

//堆排序实现
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//数据交换
void Swap(int* a, int* b)
{int temp = 0;temp = *b;*b = *a;*a = temp;
}void AdjustUp(int* a, int n)
{assert(a);int child = n - 1;int parent = (child - 1) / 2;while (parent >= 0)//判断条件不能为parent,因为parent为整型,但是最后那个结果是-0.5,对于整型来说,浮点数小数点后的忽略,那么此时parent还是0,child也变成0了,它们的值会相等然后return,虽然这样程序照样过了,但是这种想法其实是错的,所以我们通过判断child是否>0来作为是否结束的标志。{if (a[child] > a[parent]){Swap(&a[child], &a[parent]);//后面还会用到交换,所以封装成一个函数。}elsereturn;child = parent;parent = (parent - 1) / 2;}
}//向上向下调整的前提是必须调整前是一个堆(大堆或者小堆)
void AdjustDown(int* a, int n, int parent)
{assert(a);int child = parent * 2 + 1;//默认向下调整是跟左孩子交换,如果下面比较左右孩子大小时右孩子大,那么让child+1即可表示与右孩子交换。while (child < n){if (child < n - 1 && (a[child] < a[child + 1]))//前提是要有右兄弟,不然就会越界{child = child + 1;}if (a[parent] < a[child]){Swap(&a[parent], &a[child]);}elsereturn;parent = child;child = child * 2 + 1;}
}void HeapSort(int* arr, int size)//假设要求升序排列
{向上调整建大堆--时间复杂度为N*logN//for (int n = 2; n < size; n++)//{//	AdjustUp(arr, n);//}//向下调整建堆--时间复杂度为Nfor (int n = (size - 2) / 2; n >= 0; n--)//size-1是指最后一个元素的下标,另一个-1是公式套的{AdjustDown(arr, size, n);}//排序--第一个和最后一个替换,然后让新根向下调整即可,这样就让该堆中最大的数放到了该放的位置。while (size--){Swap(&arr[0], &arr[size]);AdjustDown(arr, size, 0);}
}int main()
{int arr[] = { 45,26,91,34,82,41,67,81,47,35 };HeapSort(arr, sizeof(arr) / sizeof(int));for (int i = 0; i < 10; i++){printf("%d ", arr[i]);}return 0;
}

TOP-K问题

  • TOP-K问题:即当求数据结合中前K个最大或者最小元素,一般情况下数据量会很大。比如:世界500强,富豪榜等等。你可能会想,直接排序不就行了,但是如果当数据量特别大时,整个内存都放不下,这时候最佳方法就是使用堆排序。

求最大的前k个:建小堆。
求最小的前k个,建大堆。

  • 解释:因为TOP-K一般是求前k个最值数,所以我们需要将其找出来,但是当数据特别多时,内存放不下,所以就不能使用堆排序(内存放不下),因此我们用另外的方法:假设求最大的前K个,我们先建一个k个数的小堆,然后我们将剩下的数放进文件(磁盘)里,然后分别与小堆的根(最小的数比较),如果大于这个根,那么直接覆盖上去,并让新根进行向下调整,使得覆盖后仍然是一个小堆。
  • 注意:不能与我们建的堆的最后一个元素比,因为它不一定是最大或最小,只有根我们才能确定它一定是一个最值。

五、总结

  • 升序建大堆,降序建小堆原因:假如升序建小堆,那么第一个当然就是我们要的最小的数,但是之后就不行了,因为虽然这个位置是最小的不用动了,但是当我们找第二小的时,我们就要忽略掉第一个已经定了的这个,那么这次谁当根呢,直接父子兄弟什么的全乱了,我们只能再次通过(N-1)个数进行一次堆排序建立一个新堆然后新堆的根就是第二小的,但是这样就太麻烦了,因为建一次堆的时间复杂度就是O(N*logN)(向上调整建堆)或者O(N)(向下调整建堆)而如果建成相反的(大堆),然后根和最后一个元素换一下再向下调整,这样就能确定最大的数,一次时间复杂度是logN。
  • 关于堆(顺序存储)的所有操作注意:就是尽量不破坏原本堆的结构,不让父子乱辈,不然就可能乱了很多,处理起来就很麻烦,只能重新建堆。
  • TOP-K问题总结:求最大的建小堆,求最小的建大堆。

  • 博主长期更新,博主的目标是不断提升阅读体验和内容质量,如果你喜欢博主的文章,请点个赞或者关注博主支持一波,我会更加努力的为你呈现精彩的内容。

🌈专栏推荐
😈魔王的修炼之路–C语言
😈魔王的修炼之路–数据结构初阶
😈魔王的修炼之路–C++
😈魔王的修炼之路–Linux
更新不易,希望得到友友的三连支持一波。收藏这篇文章,意味着你将永久拥有它,无论何时何地,都可以立即找到重新阅读;关注博主,意味着无论何时何地,博主将永久和你一起学习进步,为你带来有价值的内容。

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/88873.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零开始学习 Java:简单易懂的入门指南之抽象类接口内部类(十一)

面向对象进阶&#xff08;抽象类&接口&内部类&#xff09; 第一章 抽象类1.1 概述1.1.1 抽象类引入 1.2 abstract使用格式1.2.1 抽象方法1.2.2 抽象类1.2.3 抽象类的使用 1.3 抽象类的特征1.4 抽象类的细节1.5 抽象类存在的意义 第二章 接口2.1 概述2.2 定义格式2.3 接…

机器学习 | Python实现KNN(K近邻)模型实践

机器学习 | Python实现KNN(K近邻)模型实践 目录 机器学习 | Python实现KNN(K近邻)模型实践基本介绍模型原理源码设计学习小结参考资料基本介绍 一句话就可以概括出KNN(K最近邻算法)的算法原理:综合k个“邻居”的标签值作为新样本的预测值。更具体来讲KNN分类过程,给定一个训…

第二十一章 重要HL7操作场景 - HL7批量消息

文章目录 第二十一章 重要HL7操作场景 - HL7批量消息支持的批处理格式处理传入的批次文档批处理模式自定义出库批量处理 第二十一章 重要HL7操作场景 - HL7批量消息 Production品支持 HL7 中的嵌套子文档&#xff08;批处理格式&#xff09;。每个子文档本身就是一个虚拟文档。…

【设计模式】MVC 模式

MVC 模式代表 Model-View-Controller&#xff08;模型-视图-控制器&#xff09; 模式。这种模式用于应用程序的分层开发。 Model&#xff08;模型&#xff09; - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑&#xff0c;在数据变化时更新控制器。View&#xff…

Python爬虫——requests_cookie登陆古诗文网

寻找登陆需要的参数 __VIEWSTATE:aiMG0UXAfCzak10C7436ZC/RXoZbM2lDlX1iU/4wjjdUNsW8QUs6W2/3M6XIKagQZrC7ooD8Upj8uCnpQMXjDAp6fS/NM2nGhnKO0KOSXfT3jGHhJAOBouMI3QnlpJCQKPXfVDJPYwh169MGLFC6trY __VIEWSTATEGENERATOR: C93BE1AE from: http://so.gushiwen.cn/user/collect.…

泰卦-地天卦

前言&#xff1a;否极泰来&#xff0c;但在易经里是泰卦在前&#xff0c;让我们分析下在否所期待否极后的泰卦是什么样的&#xff1f;本篇博客分析泰卦的卦辞和爻辞。 卦辞 小往大来&#xff0c;吉&#xff0c;亨。 篆曰&#xff1a;泰&#xff0c;小往大来&#xff0c;吉亨。…

面试热题(合并两个有序列表)

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 合并链表这类型题也是比较经典的题了&#xff0c;因为链表是由指针相互指向而确定位置&#xff0c;所以我们只需要改变某些节点的指针便可以做到对链表进行排序 今天这个方法…

C++小游戏贪吃蛇源码

graphics.h是针对DOS下的一个C语言图形库 (c也可以) 目前支持下载此头文件的常用的有两种: 1. EGE (Easy Graphics Engine)2. EasyX Graphics LibraryEGE, 全名Easy Graphics Engine, 是windows下的简易绘图库&#xff0c;是一个类似BGI(graphics.h)的面向C/C语言新手的图形库…

APP外包开发的iOS开发语言

学习iOS开发需要掌握Swift编程语言和相关的开发工具、框架和技术。而学习iOS开发需要时间和耐心&#xff0c;尤其是对于初学者。通过坚持不懈的努力&#xff0c;您可以逐步掌握iOS开发技能&#xff0c;构建出功能丰富、优质的移动应用。今天和大家分享学习iOS开发的一些建议方法…

掌握Python的X篇_32_使用python编辑pdf文件_pdfrw

本篇介绍利用python操作pdf文件&#xff0c;我们平时也会有合并和拆分pdf的需求&#xff0c;此时我们就可以使用本节内容。 文章目录 1. pdfrw的安装2. 切分pdf文件3. pdfrw官网及实现一版四面的实例 1. pdfrw的安装 pip install pdfrw官网地址&#xff1a;https://github.co…

机器学习深度学习——常见循环神经网络结构(RNN、LSTM、GRU)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——RNN的从零开始实现与简洁实现 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章…

ETLCloud+MaxCompute实现云数据仓库的高效实时同步

MaxCompute介绍 MaxCompute是适用于数据分析场景的企业级SaaS&#xff08;Software as a Service&#xff09;模式云数据仓库&#xff0c;以Serverless架构提供快速、全托管的在线数据仓库服务&#xff0c;消除了传统数据平台在资源扩展性和弹性方面的限制&#xff0c;最小化用…

PyQt5的信号与槽函数

目录 一、介绍 二、一个信号连接一个槽 三、一个信号连接多个槽 四、多个信号连接一个槽 五、自定义信号 1、创建自定义信号 2、让自定义信号携带值 一、介绍 在下图中 &#xff08;1&#xff09;widget就是PyQt中的控件对象。其实就是组件&#xff08;2&#xff09;…

CNN之图像识别

文章目录 1. 图像识别1.1 模式识别1.2 图像识别的过程1.3 图像识别的应用 2. 深度学习发展2.1 深度学习为何崛起2.2 分类与检测2.3 常见的卷积神经网络 3. VGG3.1 VGG163.2 VGG16的结构&#xff1a;3.3 使用卷积层代替全连接3.4 1*1卷积的作用3.5 VGG16代码示例 4. 残差模型-Re…

MATLAB图论合集(一)基本操作基础

本帖总结一些经典的图论问题&#xff0c;通过MATLAB如何计算答案。近期在复习考研&#xff0c;以此来巩固一下相关知识——虽然考研肯定不能用MATLAB代码哈哈&#xff0c;不过在实际应用中解决问题还是很不错的&#xff0c;比C易上手得多~ 图论中的图&#xff08;Graph&#xf…

Offset Explorer

Offset Explorer 简介下载安装 简介 Offset Explorer&#xff08;以前称为Kafka Tool&#xff09;是一个用于管理和使Apache Kafka 集群的GUI应用程序。它提供了一个直观的UI&#xff0c;允许人们快速查看Kafka集群中的对象以及存储在集群主题中的消息。它包含面向开发人员和管…

深入理解MVVM架构模式

原文合集地址如下&#xff0c;有需要的朋友可以关注 本文地址 MVVM原理 MVVM是一种用于构建用户界面的软件架构模式&#xff0c;它的名称代表着三个组成部分&#xff1a;Model&#xff08;模型&#xff09;、View&#xff08;视图&#xff09;和ViewModel&#xff08;视图模…

tkinter+爬虫+pygame实现音乐播放器

文章目录 前文安装模块示意图爬虫完整代码pygametkinter完整代码结尾前文 本文将涉及爬虫(数据的获取),pygame(音乐播放器),tkinter(界面显示),将他们汇聚到一起制造一个音乐播放器,欢迎大家的订阅。 安装模块 pip install requests,parsel,lxpy,pygame 示意图

19. python从入门到精通——Web编程

HTTP协议 HTTP协议的常用方法 方法 描述 GET 请求指定的页面信息&#xff0c;并返回实体主体。 POST 向指定资源提交数据进行处理请求&#xff08;例如提交表单或者上传文件&#xff09;。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。 …

WebAPIs 第四天

1.日期对象 2.节点操作 3.M端事件 4.JS插件 一.日期对象 实例化时间对象方法时间戳 日期对象&#xff1a;用来表示时间的对象 作用&#xff1a;可以得到当前系统时间 1.1 实例化 ① 概念&#xff1a;在代码中发现了new关键字时&#xff0c;一般将这个操作称为实例化 …