【二叉树的顺序结构:堆 堆排序 TopK]

news2024/10/6 8:26:06

努力提升自己,永远比仰望别人更有意义


目录

 

1 二叉树的顺序结构

2 堆的概念及结构

3 堆的实现

3.1 堆向下调整算法

 3.2 堆向上调整算法

 3.3堆的插入

 3.4 堆的删除

3.5 堆的代码实现

4 堆的应用

4.1 堆排序

4.2 TOP-K问题

总结:


1 二叉树的顺序结构

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


2 堆的概念及结构

如果有一个关键码的集合 K = { k0 ,k1 ,k2 ,k3 ,} ,把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <=K2i+1 且 Ki<=K2i+2 ( i = 0 1 2… ),则称为小堆 (反之则大堆 ) 。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树


3 堆的实现

3.1 堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提: 左右子树必须是一个堆,才能调整。
int array [] = { 27 , 15 , 19 , 18 , 28 , 34 , 65 , 49 , 25 , 37 };

 具体代码:

void AdjustDown(int* a, int parent, int sz)
{
	assert(a);
	int child = parent * 2 + 1;
	while (child < sz)
	{
		if (child + 1 < sz && a[child + 1] > a[child])//建立小堆 a[child + 1] < a[child]
			child++;

		if (a[child] > a[parent])//建立小堆 <
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}

}

这里建立的是大堆,建立小堆代码中我给了注释.

 3.2 堆向上调整算法

堆的向上调整算法往往与push相搭配,push完一个数据就将该数据向上调整,这样就能够保证堆的结构不会被破坏。

具体图例:

代码实现:

void AdjustUp(int* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child>0)//用parent>=0也行,只是这样的话就不是正常结束的了
	{
		if (a[child] > a[parent])//建小堆 <
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

 我们不难发现一个数据向上调整或者向下调整的时间复杂度都为logN.

 3.3堆的插入

具体图例:

 代码实现:

void HeapPush(Heap* php, HeapDataType x)
{
	assert(php);
	if (php->capacity == php->sz)
	{
		int newcapacity = php->a == NULL ? 4 : php->capacity * 2;
		HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail:");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->sz] = x;
	php->sz++;
	//向上调整算法,保证建立的是堆(这里以建小堆为例)
	AdjustUp(php->a, php->sz - 1);//第二个参数传的是push这个数据的下标
}

 3.4 堆的删除

假设建小堆,要pop掉最小的一个数值(堆顶),要让下面的结构继续保持小堆结构就不能只将数据向前挪动一位,否则堆的结构将会被破坏。正确做法是将堆顶的数据与最后一个数据交换,然后重新向下建堆,再pop掉堆尾数据。

代码实现:

void HeapPop(Heap* php)
{
	assert(php);
	assert(php->sz > 0);
	//假设建小堆,要pop掉最小的一个数值(堆顶),要让下面的结构继续保持小堆结构就不能只将数据向前挪动一位,
	//否则堆的结构将会被破坏。正确做法是将堆顶的数据与最后一个数据交换,然后重新向下建堆,再pop掉堆尾数据。

	Swap(&php->a[0], &php->a[php->sz - 1]);
	php->sz--;
	AdjustDown(php->a, 0, php->sz);
}

3.5 堆的代码实现

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType* a;
	int sz;
	int capacity;
}Heap;

void HeapInit(Heap* php);
void HeapPush(Heap* php, HeapDataType x);
void HeapPop(Heap* php);
HeapDataType HeapTop(Heap* php);
int HeapSize(Heap* php);
bool HeapEmpty(Heap* php);
void HeapDestroy(Heap* php);
void HeapPrint(Heap* php);
void AdjustDown(int* a, int parent, int sz);
void AdjustUp(int* a, int child);
void Swap(HeapDataType* p1, HeapDataType* p2);

void HeapInit(Heap* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->sz = 0;
}


void Swap(HeapDataType* p1, HeapDataType* p2)
{
	HeapDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}


void AdjustUp(int* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child>0)//用parent>=0也行,只是这样的话就不是正常结束的了
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}


void HeapPush(Heap* php, HeapDataType x)
{
	assert(php);
	if (php->capacity == php->sz)
	{
		int newcapacity = php->a == NULL ? 4 : php->capacity * 2;
		HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail:");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->sz] = x;
	php->sz++;
	//向上调整算法,保证建立的是堆(这里以建小堆为例)
	AdjustUp(php->a, php->sz - 1);//第二个参数传的是push这个数据的下标
}


void AdjustDown(int* a, int parent, int sz)
{
	assert(a);
	int child = parent * 2 + 1;
	while (child < sz)
	{
		if (child + 1 < sz && 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 HeapPop(Heap* php)
{
	assert(php);
	assert(php->sz > 0);
	//假设建小堆,要pop掉最小的一个数值(堆顶),要让下面的结构继续保持小堆结构就不能只将数据向前挪动一位,
	//否则堆的结构将会被破坏。正确做法是将堆顶的数据与最后一个数据交换,然后重新向下建堆,再pop掉堆尾数据。

	Swap(&php->a[0], &php->a[php->sz - 1]);
	php->sz--;
	AdjustDown(php->a, 0, php->sz);
}


HeapDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->sz > 0);

	return php->a[0];
	
}


int HeapSize(Heap* php)
{
	assert(php);
	return php->sz;
}


bool HeapEmpty(Heap* php)
{
	assert(php);
	return php->sz == 0;
}


void HeapDestroy(Heap* php)
{
	assert(php);
	free(php->a);
	php->capacity = php->sz = 0;
}


void HeapPrint(Heap* php)
{
	assert(php);
	for (int i = 0; i < php->sz; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}


4 堆的应用

4.1 堆排序

在这里我们思考一个问题:排序是向上建堆还是向下建堆?

口说无凭,这里我们可以通过准确的计算来算出他们各自的时间复杂度:

1 向上建堆:

这里我们都以满二叉树为例,时间复杂度算的只是一个大概值所以可以用满二叉树来代替完全二叉树。(假设数的高度为h)

第一层有2^0个结点,要向上调整0次;

第二层有2^1个结点,要向上调整1次;

第三层有2^2个结点,要向上调整2次;

…………………………

第h-1层有2^(h-2)个结点,要向上调整(h-2)次;

第h层有2^(h-1)个结点,要向上调整(h-1)次;

所以可得:

T(h)=2^1*1+2^2*2+……2^(h-2)*(h-2)+2^(h-1)*(h-1)

利用错位相减法很容易算出:

T(h)=2^h*(h-2)+2;

由于h=logN(大概值就行,不用太精确)

所以求得向上建堆的时间复杂度大概在:

T(N)=N*logN  这个数量级。

2 向下建堆:

这个计算我在讲堆排序的时候计算过,大家可以跳转到堆排那里:

八大排序之插入和选择排序

通过计算我们可以知道向下建堆的时间复杂度大概在:

T(N)=N  这个数量级。

所以我们选用向下建堆。

那么第二个问题来了:排升序是建大堆还是建小堆?

如果建小堆,最小数已经被选出来了,但是不能够pop掉最小数,否则堆结构将被破环,那么又要重新建堆,这样就没有了效率,所以我们要建大堆,将堆顶元素与最后一个元素交换再--数据个数,然后向下调整。

具体代码:

void HeapSort(HeapDataType* a, int sz)
{
	//从最后一个结点的父亲开始建堆
	for (int i = (sz - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, sz);
	}

	for (int i = sz-1; i>0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown(a, 0, --sz);
	}
}

4.2 TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大

比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中 ) 。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前 K 个元素来建堆 :
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素 :
将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。

具体代码:

    //建立一个k个数的小堆,依次遍历数组,比堆顶元素大就替换,然后向下调整,最后堆中数据就是topk
	//时间复杂度为:N+N*logk  空间复杂度为O(k)
	int topk[5] = {0};
	int i;
	for (i = 0; i < 5; i++)
	{
		topk[i] = array[i];
	}
	//建小堆
	for (i = (5 - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(topk, i, 5);
	}
	//遍历替换
	for (i=5; i < sz; i++)
	{
		if (array[i] > topk[0])
		{
			topk[0] = array[i];
			AdjustDown(topk, 0, 5);
		}
	}

	for (i = 0; i < 5; i++)
		printf("%d ", topk[i]);
	//这种方法占据内存较小,比较优秀

总结:

文章中我们介绍了堆这种二叉树顺序结构,实现了堆并且将堆的两大比较重要的应用(堆排序和TopK问题)介绍了,这里面比较重要的就是向上/向下调整算法。后面链式二叉树以及相关OJ我们将放在下一篇文章来讲解,大佬们,我们下期再见!

 

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

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

相关文章

分享几招教会你怎么给图片加边框

大家平时分享图片的时候&#xff0c;会不会喜欢给照片加点装饰呢&#xff1f;比如加些边框、文字或者水印之类的。我就喜欢给图片加上一些边框&#xff0c;感觉加了边框的照片像裱在相框中的感觉似的&#xff0c;非常有趣。那么你知道如何给图片加边框吗&#xff1f;不知道的话…

【Nginx】01-什么是Nginx?Nginx技术的功能及其特性介绍

目录1. 介绍1.1 常见服务器的对比1&#xff09;IIS2&#xff09;Tomcat3&#xff09;Apache4&#xff09;Lighttpd1.2 Nginx的优点(1) 速度更快、并发更高(2) 配置简单、扩展性强(3) 高可靠性(4) 热部署(5) 成本低、BSD许可证2. Nginx常用功能2.1 基本HTTP服务2.2 高级HTTP服务…

华为数通2022年11月 HCIP-Datacom-H12-821 第二章

142.以下关于状态检测防火墙的描述&#xff0c;正确是哪一项&#xff1f; A.状态检测防火墙需要对每个进入防火墙的数据包进行规则匹配 B.因为UDP协议为面向无连接的协议&#xff0c;因此状态检测型防火墙无法对UDP报文进行状态表的匹配 C.状态检测型防火墙只需要对该连接的第一…

性能测试-CPU性能分析,IO密集导致系统负载高

目录 IO密集导致系统负载高 使用top命令-观察服务器资源状态 使用vmstat命令-观察服务器资源状态 使用pidstat命令-观察服务器资源状态 使用iostat命令-观察服务器资源状态 IO密集导致系统负载高 stress-ng -i 10 --hdd 1 --timeout 100-i :有多少个工作者进行&#…

函数的极限:如何通过 δ 和 ϵ 来定义一个连续的函数

连续的定义 维基百科给出的定义&#xff1a; 连续函数&#xff08;英语&#xff1a;Continuous function&#xff09;是指函数在数学上的属性为连续。直观上来说&#xff0c;连续的函数就是当输入值的变化足够小的时候&#xff0c;输出的变化也会随之足够小的函数。 所以不要直…

51单总线控制SV-5W语音播报模块

单总线控制SV-5W语音播报模块SV-5W语音播报模块SV-5W语音播报模块简介工作模式说明模块配置接线驱动部分代码效果展示SV-5W语音播报模块 SV-5W语音播报模块简介 DY-SV5W是一款智能语音模块&#xff0c;集成IO分段触发&#xff0c;UART串口控制&#xff0c;ONE_line单总线串口控…

macOS monterey 12.6.1安装homebrew + nginx + php + mysql

效果图 主要步骤 安装homebrew使用brew安装nginxphpmysql详细步骤 参考“Homebrew国内如何自动安装&#xff08;国内地址&#xff09;&#xff08;Mac & Linux&#xff09;”安装brew&#xff0c; 命令&#xff1a; /bin/zsh -c "$(curl -fsSL https://gitee.com/cu…

[附源码]java毕业设计网上学车预约系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

教你十分钟在Linux系统上快速装机并安装Ansible

PS:本教程建立在VMware软件上的使用上&#xff0c;Linux版本为centos7或者centos8都可以。 一、看发行版本 cat /etc/redhat-release 二、修改主机名 hostnamectl set-hostname centos8 三、自动获取IP地址 nmcli connection modify ens160 autoconnect yes 四、设置…

软件设计(一):统一建模语言基础知识

1.UML简介 1.1 UML简介 UML语言是一种可视化的标准建模语言&#xff0c;它是一种分析和设计语言&#xff0c;通过UML可以构造软件系统的蓝图。 1.2 UML的结构 1.2.1 视图&#xff08;view&#xff09; 1.2.2 图&#xff08;daigram&#xff09; 用例图 类图 对象图 包图…

C/C++ 语言怎么保留n位小数并且四舍五入

1、普通的printf输出打印 printf()函数的用例 float date=123.456; printf("date=%.2f\n", date);//保留2位 printf("date=%.1f\n", date);//保留1位 输出 2、获取四舍五入后的数据 1、使用round函数 C ++ round()函数 (C++ round() function) round(…

ELK技术栈简介

ELK技术栈简介ELK是什么ELK组件ElasticsearchES基本概念ES适用场景LogstashInput插件Filter插件Output插件CodecsKibanaBeatsELK是什么 ELK 即 Elasticsearch Logstash Kibana&#xff0c;是指Elastic公司开发的三种免费开源软件。其中&#xff0c;Elasticsearch是一个基于A…

基于PHP+MYSQL在线小说阅读网的设计与实现

随着互联网信息的发展,人们在闲暇的时候更多的原因选择小说来进行阅读,一方面扩展自己的阅读圈,另一方面消磨闲暇时光,但是当下的很多小说网站,要么是要收取高昂的阅读法,要么就是整个网站多充斥着大量的广告,为了给广大网友一个健康,免费的阅读空间我们开发了本系统 本在线小说…

【JS】数据结构之树结构

文章目录树结构二叉树二叉搜索树平衡树&#xff08;AVL树&#xff09;红黑树回顾其他数据结构&#xff08;每种数据结构都有自己特定的应用场景&#xff09;&#xff1a; 数组&#xff1a;通过下标查询很快&#xff0c;插入和删除数据的时候&#xff0c;效率会很低&#xff0c;…

新品上线 | 企企通推出达人管理系统,助力达人营销提效增速

01、直播市场发展迅速 企企通达人管理系统应运而生 近年来&#xff0c;直播凭借其即时性、互动性、多样化的优势&#xff0c;迅速在互联网占据一席之地&#xff0c;“直播”模式不断扩展&#xff0c;直播电商应运而生。 在技术发展与市场需求双重驱动下&#xff0c;中国直播市…

day04 springmvc

day04 springmvc 第一章 SpringMVC运行原理 第一节 启动过程 1. Servlet 生命周期回顾 生命周期环节调用的方法时机次数创建对象无参构造器默认&#xff1a;第一次请求 修改&#xff1a;Web应用启动时一次初始化init(ServletConfig servletConfig)创建对象后一次处理请求se…

嵌入式Linux系统中ARM汇编语言的使用方法

大家好&#xff0c;今天主要大家聊一聊&#xff0c;如何在ARM中使用汇编语言的方法。 目录 第一&#xff1a;汇编基础简介 第二&#xff1a;处理器内部数据传输指令 第三&#xff1a;存储器访问指令 第一&#xff1a;汇编基础简介 我们在学习嵌入式Linux开发的时候是绝…

【用户画像】Redis_Jedis测试、将人群包存放到Redis中、挖掘类标签处理过程、决策树、用SparkMLLib实现决策树

文章目录一 Redis_Jedis_测试1 Jedis所需要的jar包2 连接Redis注意事项3 测试相关数据类型&#xff08;0&#xff09;测试连接&#xff08;1&#xff09;Key&#xff08;2&#xff09;String&#xff08;3&#xff09;List&#xff08;4&#xff09;set&#xff08;5&#xff0…

shiro-第一篇-基本介绍及使用

shiro 概述 shior的话&#xff0c;在第一次听说的时候单纯的任务它就是一个安全框架&#xff0c;可以对访问接口的用户进行验证等工作&#xff0c;类似拦截器或过滤器的东西&#xff0c;但是在学习后&#xff0c;发现远远不止这些&#xff0c;它的灵活性和易用性让我震惊&…

408 | 【计网】第二章 物理层 回顾

自用冲刺笔记整理。 部分图片来自王道。 加油ヾ(◍∇◍)ノ゙ (一)通信基础 1.信道、信号、带宽、码元、波特、速率/数据率、信源与信宿等基本概念 单向通信、半双工通信(双方都可接发,不能同时)、全双工通信码元:用一个固定时长(码元宽度)的信号波形表示一位k进制数字。 …