二叉树顺序存储结构

目录

1.二叉树顺序存储结构

2.堆的概念及结构

3.堆的相关接口实现

3.1 堆的插入及向上调整算法

3.1.1 向上调整算法

3.1.2 堆的插入

3.2 堆的删除及向下调整算法

3.2.1 向下调整算法

3.2.2 堆的删除

3.3 其它接口和代码实现

4.建堆或数组调堆的两种方式及复杂度分析

4.1 向上调整建堆

4.1.1 建堆步骤

4.1.2 代码实现

4.1.3 时间复杂度分析 --- O(N*logN)

4.2 向下调整建堆

4.2.1 建堆步骤

4.2.2 代码实现

4.2.3 时间复杂度分析 --- O(N)

5.堆的应用

5.1 堆排序(假设升序)

5.1.1 堆排序步骤

5.1.2 代码实现

5.2 TopK问题

5.2.1 TopK解决步骤

5.2.2 代码实现(数据从文件读取)


1.二叉树顺序存储结构

顺序存储结构就是用数组来存储,一般是用数组只适合来表示完全二叉树,因为不是完全二叉树会有空间浪费的现象。

二叉树的顺序存储结构在物理上是一个数组,在逻辑上是一个二叉树。

现实中我们通常把堆使用顺序存储结构的数组来存储,而什么又是堆呢?

需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

2.堆的概念及结构

其实堆就是一个完全二叉树,堆中的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并且满足:堆中某个结点的值总是不大于其父节点的值(大堆)或者堆中某个结点的值总是不小于其父节点的值(小堆)。

总结来说:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树;

注意:所有的数组都可以表示成完全二叉树,但是他并不一定是堆。

3.堆的相关接口实现


补充:

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

(叶子就是没有左孩子,也就是叶子结点的左孩子下标2i+1>=n越界)
3.若2i+2 < n,右孩子序号:2i+2; 若2i+2>=n(数组越界), 无右孩子


3.1 堆的插入及向上调整算法

  1. 先将元素插入到堆的末尾,即最后一个数组元素之后
  2. 插入之后如果堆的性质遭到破坏,插入的结点就根据向上调整算法找到合适位置即可

3.1.1 向上调整算法

那么什么是向上调整算法呢?如何实现?

例如: 堆:[4, 27, 11, 28, 35, 19, 15, 89, 2] Push:2

图片展示:

代码实现:

void AdjustUp(int* 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;}}
}

代码注意事项:

  • 除了插入的元素,堆中的其他元素都符合堆的性质,所以调整到 待调整节点和父节点 符合堆的性质即可退出调整。
  • 循环退出条件:待调整节点调整到根节点 或者 待调整节点和父节点的大小关系符合堆的性质。
  • while语句中的条件不能写parent >= 0,因为 (0-1)/ 2 之后依旧是0,并不符合预期的循环退出条件。
  • 如果调大堆,比较符号就用>;如果调小堆,比较符号就用<;方便以后更改。

3.1.2 堆的插入

注意:堆的物理结构是一个数组,也就是用顺序表实现的,插入时容量不够要记得扩容。

void HeapPush(Heap* php, HPDataType x)
{assert(php);if (php->capacity == php->size){int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("HeapPush:");exit(-1);}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}

3.2 堆的删除及向下调整算法

这里堆的删除是删除堆顶数据,因为只有堆顶数据才有意义(堆顶数据都是最值,删除堆顶后能获得次大或者次小的数)

这里我们能直接删除堆顶数据吗?很明显不可以,根据顺序表删除数据的特点,后面的元素会依次覆盖前面的元素,删除堆顶数据后,堆的结构就被破坏了。

其实删除思想是这样的:

  1. 将堆顶元素与堆中最后一个元素交换
  2. 删除堆中最后一个元素
  3. 将堆顶元素向下调整直到满足堆的结构

这种思想很巧妙,在后面的堆排序中也会用到这种思想。

3.2.1 向下调整算法

代码实现:

void AdjustDown(int* a, int size, int parent)
{int child = parent * 2 + 1;while (child < size){if (child + 1 < size && a[child + 1] > a[child])child++;if (a[child] > a[parent]){Swap(a + child, a + parent);parent = child;child = child * 2 + 1;}else{break;}}
}

代码注意事项:

  • 除了堆顶元素,堆中的其他元素都符合堆的性质,所以调整到 待调整节点和左右孩子 符合堆的性质即可退出调整。
  • 循环退出条件:待调整节点调整到叶子节点 或者 待调整节点和左右孩子节点的大小关系符合堆的性质。
  • 因为要判断越界,函数参数中要有数组大小size。
  • 向下调整时,我们要和该结点的左右孩子中较小的那个交换。代码设计时我们可以先默认左孩子小,再和右孩子进行比较得出较小的那个节点。

3.2.2 堆的删除

void HeapPop(Heap* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}

3.3 其它接口和代码实现

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap;void Swap(int* a, int* b);
void AdjustUp(int* a, int child);//a:要调整的孩子结点所在的数组  child:要调整的孩子节点的下标
void AdjustDown(int* a, int size, int parent);//a:要调整的父亲结点所在的数组   size:数组的size   parent:要调整的父亲节点的下标void HeapPrint(Heap* php);
void HeapInit(Heap* php);
void HeapDestory(Heap* php);
void HeapPush(Heap* php, HPDataType x);
void HeapPop(Heap* php);
HPDataType HeapTop(Heap* php);
int HeapSize(Heap* php);
int HeapEmpty(Heap* php);
#include "Heap.h"void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}//调da堆
void AdjustUp(int* 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;}}
}//调da堆
void AdjustDown(int* a, int size, int parent)
{int child = parent * 2 + 1;while (child < size){if (child + 1 < size && a[child + 1] > a[child])child++;if (a[child] > a[parent]){Swap(a + child, a + parent);parent = child;child = child * 2 + 1;}else{break;}}
}void HeapInit(Heap* php)
{assert(php);php->a = NULL;php->capacity = php->size = 0;
}void HeapDestory(Heap* php)
{free(php->a);php->capacity = php->size = 0;
}void HeapPrint(Heap* php)
{for (int i = 0; i < php->size; i++){printf("%d ", php->a[i]);}printf("\n");
}void HeapPush(Heap* php, HPDataType x)
{assert(php);if (php->capacity == php->size){int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("HeapPush:");exit(-1);}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}//这里删除的是堆顶数据,只有堆顶数据才有意义
//1.swap(堆顶数据,最后一个数据)
//2.删除最后一个数据
//3.堆顶数据AdjustDown
void HeapPop(Heap* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}HPDataType HeapTop(Heap* php)
{assert(php);assert(php->size > 0);return php->a[0];
}int HeapSize(Heap* php)
{assert(php);return php->size;
}int HeapEmpty(Heap* php)
{assert(php);return php->size == 0;
}

4.建堆或数组调堆的两种方式及复杂度分析


前面我们说过,所有的数组都可以表示成完全二叉树,但是他并不一定是堆。那么我们如何将这个数组调整成堆呢?

我们首先会想到:把数组的值依次push到堆中,再把堆中数据依次赋值给数组,这样就把数组调整成了堆。但是实际应用中我们不会再写一个堆这样的数据结构,其次这种方式会有空间复杂度的消耗,所以我们不提倡这么做。

调堆方式有两种:向上调整建堆和向下调整建堆


4.1 向上调整建堆

4.1.1 建堆步骤

参考堆插入的思想,数组中的每个元素都可以看做新插入的节点。

从根结点开始调整,一直调整到最后一个结点。

想要调成成小堆:如果该结点小于父节点,就一直向上交换,直到不小于其父节点或者调整到根结点。

4.1.2 代码实现

int main()
{int a[] = { 27,15,19,28,35,11,4,89,2 };int size = sizeof(a) / sizeof(a[0]);//这里我们建小堆//方法一:向上调整算法for (int i = 0; i < size; i++)//从根结点开始调整,一直调整到最后一个结点。{AdjustUp(a, i);}for (int i = 0; i < size; i++){printf("%d ",a[i]);}return 0;
}

4.1.3 时间复杂度分析 --- O(N*logN)

​​

4.2 向下调整建堆

4.2.1 建堆步骤

在解释向下调整算法之前,先说明一下:

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

那么我们如何调整呢?

这里我们可以利用递归思想来解决:先从倒数第一个非叶子结点的子树开始调整,一直调整到根结点的树。也就是倒着调整。

4.2.2 代码实现

int main()
{int a[] = { 27,15,19,28,35,11,4,89,2 };int size = sizeof(a) / sizeof(a[0]);//这里我们建小堆//方法二:向下调整算法for (int i = (size - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, size, i);}for (int i = 0; i < size; i++){printf("%d ",a[i]);}return 0;
}

代码注意:

size - 1是最后一个数组元素的下标,对他减1除2后,就是他父节点的下标,也就是倒数第一个非叶子结点;

4.2.3 时间复杂度分析 --- O(N)

总结:向下调整算法建堆要比向上调整算法建堆要高效一些,并且向下调整算法要更常用(通常对堆顶数据进行向下调整操作),所以我们一般使用向下调整来建堆。

5.堆的应用


那么我们为什么要学堆呢?为什么要设计堆这种数据结构呢?

主要用于解决两个问题:

  • 排序
  • Topk问题:在N个数据中找最大或者最小的前k个(这里的N一般非常大)

注意:这里堆的应用问题和前面数组调堆的问题是同一个道理,我们不能使用堆数据结构的相关接口,需要在原生数组上进行操作。


5.1 堆排序(假设升序)

5.1.1 堆排序步骤

首先建堆这里就有一个坑了,正常思维来看,我们升序是建小堆,因为小堆的堆顶是最小值。

我们来看看升序建小堆的效率如何:

  1. 选出最小的数,放在第一个位置
  2. 最小的数删除后,剩下的看做一个堆。但是之前建好的关系都乱了,只能重新建堆,才能选出次小的数。

此时的时间复杂度:建堆的时间复杂度O(n),建了n次堆,时间复杂度O(n*n),这种效率还不如暴力遍历排序来的直接。

这里花里胡哨的建堆选堆顶的最值进行排序,结果效率和冒泡差不多,显然不是我们想要的结果。

那么堆排序到底是怎么排的呢,下面给出步骤

1.建堆

升序:建大堆

降序:建小堆

2.利用堆删除思想进行排序

(1)升序建的大堆,堆顶是最大元素,

(2)把堆顶(最大元素)和最后一个元素交换,

(3)最后一个元素(最大值)不看做堆中元素,堆顶元素向下调整,堆顶元素就变成了次大值,

(4)依次类推,重复(2)~(3)

可以计算一下这里的时间复杂度来和上面的建小堆方法来比较一下:

建大堆:n次向下调整,每次调整时间复杂度为O(logn),所以时间复杂度为:O(n*logn)

建小堆的时间复杂度O(n*n),很显然,数据非常多时,这两种方法的效率是天差地别

5.1.2 代码实现

//堆排序,升序
void HeapSort(int* a, int n)
{//第一步:建大堆for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}//第二步:堆删除思想进行排序(依次选数,调堆)for (int i = n - 1; i > 0; i--)//最后一步交换后就一个堆顶元素(最小值),AdjustDown没有进行调整{//将堆顶元素和最后一个元素交换,交换后最后一个元素不计入堆内Swap(&a[0], &a[i]);AdjustDown(a, i , 0);//这里第二个参数是数据个数,最后一个元素不计入堆内,正好是i}
}

代码注意事项:

  • 以后我们建堆都用向下调整建堆。首先高效,其次堆排序的选数调堆也用的向下调整。
  • 这里的循环变量i是最后一个元素的下标,也正好是交换后数组的元素个数(交换后最后一个元素不计入数组内)
  • 最后一步i == 1时,交换后就一个堆顶元素(最小值),AdjustDown没有进行调整,但这一步也要执行,因为a[0]和a[1]要进行交换。

5.2 TopK问题

5.2.1 TopK解决步骤

Topk问题就是在N个数中找最大或者最小的前k个。(这里的N一般非常大,大到内存装不下)

第一次碰到这个问题,我们的惯性思维会去怎么解决呢?

  • 方法一:先排降序,前k个最大
  • 方法二:N个数依次插入大堆,pop k次,每次取的都是堆顶数据

但是当N非常大时,甚至内存都放不下,很显然这两种方法不靠谱。

我们可以算一下时间复杂度:

方法一:O(N*logN)

方法二:O(N+klogN) 建堆:N,k次pop :klogN

我们直接来说说Topk问题的实际解决办法:

  1. 用数据集合的前k个元素来建堆

前k个最大的元素:建小堆

前k个最小的元素:建大堆

2.用剩余的N-k个元素依次与对顶元素比较,找最大(小)的k个:比堆顶大(小),替换,向下调整。

3.最后堆中的k个元素就是最大(最小)的k个数

计算时间复杂度:O(k+(N-k)logk)~O(Nlogk)

5.2.2 代码实现(数据从文件读取)

int* TopK(int k)
{int* retArr = (int*)malloc(sizeof(int) * k);//打开文件FILE* pf = fopen("data,txt", "r");if (pf == NULL) {perror("TopK:");exit(-1); }//前k个数据读入数组for (int i = 0; i < k; i++){fscanf(pf, "%d", &retArr[i]);}//数组建堆(小堆)for (int i = (k - 2) / 2; i >= 0; i--){AdjustDown(retArr, k, i);}//剩余N-k个数据,依次和堆顶数据比较for (int i = 0; i < N - k; i++) {int x;fscanf(pf, "%d", &x);if (x > retArr[0]){retArr[0] = x;AdjustDown(retArr, k, 0);}}fclose(pf);return retArr; 
}void testTopK()
{int* arr = TopK(10);for (int i = 0; i < 10; i++)printf("%d ", arr[i]);printf("\n");free(arr);
}

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

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

相关文章

使用 Python 来创建一个基本的命令行密码管理器

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 密码管理器项目简介…

长亭雷池社区版本安装与使用

0x01 雷池介绍 一款足够简单、足够好用、足够强的免费 WAF。基于业界领先的语义引擎检测技术&#xff0c;作为反向代理接入&#xff0c;保护你的网站不受黑客攻击。核心检测能力由智能语义分析算法驱动&#xff0c;专为社区而生&#xff0c;不让黑客越雷池半步。 官方网址&…

【Linux】多线程互斥与同步

文章目录 一、线程互斥1. 线程互斥的引出2. 互斥量3. 互斥锁的实现原理 二、可重入和线程安全三、线程和互斥锁的封装1. 线程封装1. 互斥锁封装 四、死锁1. 死锁的概念2. 死锁的四个必要条件3. 避免死锁 五、线程同步1. 线程同步的理解2. 条件变量 一、线程互斥 1. 线程互斥的…

卷积网络:实现手写数字是识别50轮准确率97.3%

卷积网络&#xff1a;实现手写数字是识别50轮准确率 1 导入必备库2 torchvision内置了常用数据集和最常见的模型3 数据批量加载4 绘制样例5 创建模型7 设置是否使用GPU8 设置损失函数和优化器9 定义训练函数10 定义测试函数11 开始训练12 绘制损失曲线并保存13 绘制准确率曲线并…

机器人连续位姿同步插值轨迹规划—对数四元数、b样条曲线、c2连续位姿同步规划

简介&#xff1a;Smooth orientation planning is benefificial for the working performance and service life of industrial robots, keeping robots from violent impacts and shocks caused by discontinuous orientation planning. Nevertheless, the popular used quate…

学习记忆——方法篇——连锁拍照、情景故事和逻辑故事法

三大方法速记这些内容 1、连锁拍照法速记重要事件 2、情景故事速记速记购物信息 3、逻辑故事法速记客户档案 一、连锁拍照法速记重要事件 例&#xff1a;女朋友在出差之前嘱咐男朋友几件事 1、把房间收拾干净&#xff0c;最重要的是要把书架整理了&#xff0c;垃圾倒了 2、记…

Spring+MyBatis使用collection标签的两种使用方法

目录 项目场景&#xff1a; 实战操作&#xff1a; 1.创建菜单表 2.创建实体 3.创建Mapper 4.创建xml 属性描述&#xff1a; 效率比较&#xff1a; 项目场景&#xff1a; 本文说明了Spring BootMyBatis使用collection标签的两种使用方法 1. 方法一: 关联查询 2. 方法…

学习Bootstrap 5的第九天

目录 列表组 基础的列表组 实例 活动的列表项 实例 禁用的列表项 实例 链接列表项 实例 移除列表边框 实例 带编号的列表组 实例 水平列表组 实例 多种颜色列表项 实例 多种颜色的链接列表项 实例 带徽章的列表组 实例 列表组案例 实例一 实例二 列表组…

连nil切片和空切片一不一样都不清楚?那BAT面试官只好让你回去等通知了。

连nil切片和空切片一不一样都不清楚&#xff1f;那BAT面试官只好让你回去等通知了。 问题 package mainimport ("fmt""reflect""unsafe" )func main() {var s1 []ints2 : make([]int,0)s4 : make([]int,0)fmt.Printf("s1 pointer:%v, s2 p…

NLP机器翻译全景:从基本原理到技术实战全解析

目录 一、机器翻译简介1. 什么是机器翻译 (MT)?2. 源语言和目标语言3. 翻译模型4. 上下文的重要性 二、基于规则的机器翻译 (RBMT)1. 规则的制定2. 词典和词汇选择3. 限制与挑战4. PyTorch实现 三、基于统计的机器翻译 (SMT)1. 数据驱动2. 短语对齐3. 评分和选择4. PyTorch实现…

本地MQTT服务器搭建(EMQX)

一、下载EMQX 下载地址&#xff1a;EMQ (emqx.com) 打开官网后&#xff0c;选择右边的免费试用按钮 然后单击EMQX Enterprise标签&#xff0c;然后选择下面的EMQX开源版&#xff0c;选择开源版的系统平台为Windows&#xff0c;单击免费下载。 在新页面下单击立即下载 二、安装…

Kotlin(六) 类

目录 创建类 调用类 类的继承------open 构造函数 创建类 创建类和创建java文件一样&#xff0c;选择需要创建的目录New→Kotlin File/Class Kotlin中也是使用class关键字来声明一个类的&#xff0c;这一点和Java一致。现在我们可以在这个类中加入字段和函数来丰富它的功…

循环语句详解

文章目录 循环语句详解1. 循环使用 v-for 指令2. v-for 还支持一个可选的第二个参数&#xff0c;参数值为当前项的索引3. 模板template 中使用 v-for4. v-for 迭代对象-第一个参数为value5. v-for的第二个参数为键名6. v-for的第三个参数为索引7. v-for迭代整数8. computed计算…

leetcode 第454题.四数相加II

给你四个整数数组 nums1、nums2、nums3 和 nums4 &#xff0c;数组长度都是 n &#xff0c;请你计算有多少个元组 (i, j, k, l) 能满足&#xff1a; 0 < i, j, k, l < nnums1[i] nums2[j] nums3[k] nums4[l] 0 454. 四数相加 II - 力扣&#xff08;LeetCode&#xf…

大型语言模型,第 1 部分:BERT

一、介绍 2017是机器学习中具有历史意义的一年&#xff0c;当变形金刚模型首次出现在现场时。它在许多基准测试上都表现出色&#xff0c;并且适用于数据科学中的许多问题。由于其高效的架构&#xff0c;后来开发了许多其他基于变压器的模型&#xff0c;这些模型更专注于特定任务…

rust编译出错:error: failed to run custom build command for `ring v0.16.20`

安装 Visual Studio&#xff0c;确保选择 —.NET 桌面开发、使用 C 的桌面开发和通用 Windows 平台开发。显示已安装的工具链rustup show。然后通过运行更改和设置工具链rustup default stable-x86_64-pc-windows-msvc。 另外是想用clion进行调试rust 需要你按下面配置即可解…

Mental Poker- Part 2

在part-1中&#xff0c;我们梳理了去中心纸牌游戏所面临的挑战&#xff0c;也介绍了一种改进的Barnett-Smart协议&#xff0c;part-2将深入了解该协议背后涉及的算法。 Discrete-log VTMF VTMFs包含4部分&#xff1a;key generation, mask, remask and unmask&#xff0c;这些…

NFTScan 正式上线 TON NFTScan 浏览器!

2023 年 9 月 12 号&#xff0c;NFTScan 团队正式对外发布了 TON NFTScan 基础设施&#xff0c;将为 TON 生态的 NFT 开发者和用户提供简洁高效的 NFT 数据搜索查询服务。NFTScan 作为全球领先的 NFT 数据基础设施服务商&#xff0c;TON 是继 Bitcoin、Ethereum、BNBChain、Pol…

interview3-微服务与MQ

一、SpringCloud篇 &#xff08;1&#xff09;服务注册 常见的注册中心&#xff1a;eureka、nacos、zookeeper eureka做服务注册中心&#xff1a; 服务注册&#xff1a;服务提供者需要把自己的信息注册到eureka&#xff0c;由eureka来保存这些信息&#xff0c;比如服务名称、…

Unity Animation、Animator 的使用(超详细)

文章目录 1. 添加动画2. Animation2.1 制作界面2.2 制作好的 Animation 动画2.3 添加和使用事件 3. Animator3.1 制作界面3.2 一些参数解释3.3 动画参数 4. Animator中相关类、属性、API4.1 类4.2 属性4.3 API4.4 几个关键方法 5. 动画播放和暂停控制 1. 添加动画 选中待提添加…