C++ 不知树系列之二叉堆排序(递归和非递归实现上沉、下沉算法)

news2025/3/2 4:08:44

1. 前言

什么是二叉堆?

二叉堆有序完全二叉树,在完全二叉树的基础上,二叉堆 提供了有序性特征

  • 二叉堆根结点上的值是整个堆中的最小值最大值

  • 根结点上的值是整个堆结构中的最小值时,此堆称为最小堆。最小堆中,任意节点的值大于父结点的值。

  • 根结点上的值是整个堆结构中的最大值时,则称堆为最大堆。最大堆中,任意节点的值小于父结点的值。

根据完全二叉树的特性,二叉堆的父结点与子结点之间满足下面的关系:

  • 如果知道了一个结点的位置 i,则其左子结点在 2*i 位置,右子结点在 2*i+1 位置。

    Tips: 前提是存在有子结点。

  • 如果知道了一个结点的位置 i,则其父结点在 i 除以 2 的位置。

    Tips: 根结点没有父结点。

tree02.png

如上图所示:

值为 5 的结点在 2 处,则其左结点 12 的位置应该在 2*2=4 处,而实际情况也是在 4 位置。其右子结点 13 的位置应该在 2*2+1=5 的位置,实际位置也是在 5 位置。

值为 19 的结点现在 7 位置,其父结点的根据公式 72 等于 3(取整),应该在 3 处,而实际情况也是在 3 处(位置在 3、 值为 8 的结点是其父结点)。

2 堆的数据结构

2.1 二叉堆的抽象数据结构

当谈论某种数据结构的抽象数据结构时,最基本的 API 无非就是增、删、改、查。

二叉堆的基本抽象数据结构:

  • Heap() :创建一个新堆。
  • insert(data): 向堆中添加新节点(数据)。
  • getRoot(): 返回最小(大)堆的最小(大)元素。
  • removeRoot() :删除根节点。
  • isEmpty():判断堆是否为空。
  • findAll():查询堆中所有数据。

根据二叉堆的特性,顺序存储应该成为堆的首选方案。

如有数列=[8,5,12,15,19,13,1],可以先创建一个一维数组。

tree03.png

数组第 0 位置初始为 0,从第 2 个位置也就是索引号为 1 的地方开始存储堆的数据。如下图,二叉堆中的数据在数组中的对应存储位置。

tree08.png

2.2 基础 API 实现

设计一个 Heap 类封装对二叉堆的操作方法,类中方法用来实现最小堆。

#include <iostream>
using namespace std;
/* 
* 堆类 
*/ 
template<typename T>
class Heap{
    private:
    	
    	//数组
    	T heapList[100];
    	//实际大小
		int size=0; 
		
	public:
		
		/*
		*构造函数 
		*/ 
		Heap(){
		} 
		
		/*
		*返回根结点的值 
		*/
		T getRoot();
		
		/*
		*删除根结点 
		*/
		T removeRoot();
    	
    	/*
		*递归删除
		*/
		T removeRoot_();
		void removeRootByRecursion(int parentIdx );
		
		/*
		*初始化根结点 
		*/ 
		void setRoot(T val);
		
		/*
		*添加新结点,返回存储位置 
		*/
		int insert(T  val);
		
		/*
		*堆是否为空 
		*/ 
		bool isEmpty();
    
    	/*
		* 递归插入 
		*/
		int insert_(T  val);
		int insertByRecursion(int  pos);
		
		/*
		*输出所有结点
		*/
		void findAll() {
			for(int i=0; i<=size; i++)
				cout<<this->heapList[i]<<"\t";
			cout<<endl;
		}	 
}; 

Heap 类中的属性详解:

  • heapList:使用数组存储二叉堆的数据,初始时,列表的第 0 位置初始为默认值 0

    Tips: 为什么要设置列表的第 0 位置的默认值为 0

    这个 0 也不是随意指定的,有其特殊数据含义:用来描述根结点的父结点编号或者说根结点没有父结点。

  • size:用来存储二叉堆中数据的实际个数。

Heap 类中的方法介绍:

isEmpty:检查是不是空堆。逻辑较简单。

/*
*当 size 为 0 时,堆为空 
*/
template<typename T>
bool Heap<T>::isEmpty(){
	return Heap::size==0;
}

setRoot:创建根结点。保证根节点始终存储在列表索引为 1 的位置。

/*
*初始化根结点
*/
template<typename T>
void Heap<T>::setRoot(T val) {
	if( Heap<T>::heapList[1]==0  )
		Heap<T>::heapList[1]=val;
		Heap<T>::size++;
}

getRoot:如果是最大堆,则返回二叉堆的最大值,如果是最小堆,则返回二叉堆的最小值。

/*
*返回根结点
*/
template<typename T>
T Heap<T>::getRoot() {
	if( !Heap<T>::isEmpty  )
		return	Heap<T>::heapList[1];
}

Tips: 使用数组存储二叉堆数据时,根结点始终保存在索引号为 1 的位置。

前面是几个基本方法,现在实现添加新结点,编码之前,先要知道如何在二叉堆中添加新结点:

2.3 上沉算法

添加新结点采用上沉算法。如下演示上沉算法的实现过程。

tree09.png

  • 新结点添加到已有的二叉堆的最后面。如下图,添加值为 4 的新结点,存储至索引号为 7 的位置。

tree10.png

  • 查找新结点父结点,并与父结点的值比较大小,如果比父结点的值小,则和父结点交换位置。如下图,值为 4 的结点小于值为 8 的父结点,两者交换位置。

tree11.png

  • 交换后再查询是否存在父结点,如果有,同样比较大小、交换,直到到达根结点或比父结点大为止。值为 4 的结点小于值为 5 的父结点,继续交换。交换后,新结点已经达到了根结点位置,整个添加过程可结束。观察后会发现,遵循此流程添加后,没有破坏二叉堆的有序性。

tree12.png

编码实现 insert 方法

/*
*添加新结点
*/
template<typename T>
T Heap<T>::insert(T val) {
	//存储在最后一个位置
	int pos= ++Heap<T>::size;
	Heap<T>::heapList[pos]=val;
	int temp=0;
	//上沉算法
	while(1) {
		//找到父结点位置
		int parentIdx=  pos / 2;
		if(parentIdx==0)
			//出口一,没有父结点
			break;
		if( Heap<T>::heapList[pos]>Heap<T>::heapList[parentIdx] )
			//出口二:大于父结点
			break;
		else {
			//和父亲结点交换
			temp=Heap<T>::heapList[pos];
			Heap<T>::heapList[pos]=Heap<T>::heapList[parentIdx];
			Heap<T>::heapList[parentIdx]=temp;
			pos=parentIdx
		}
	}
}

测试向二叉堆中添加数据。

int main(int argc, char** argv) {
	//实例化堆
	Heap<int> heap;
	//初始化根结点
	heap.setRoot(5);
	//检查根结点是否创建成功
	int rootVal=heap.getRoot();
	cout<<"根结点的值:"<<rootVal<<endl;
	//添加值为 12和值为  13 的 2个新结点,检查添加新结点后整个二叉堆的有序性是否正确。
	heap.insert(12);
	heap.insert(13);
	cout<<"测试一:"<<endl;
	heap.findAll();
	return 0;
}

输出结果:

tree18.png

tree13.png

添加值为 1 的新结点,并检查二叉堆的有序性。

int main(int argc, char** argv) {
	//省略……
    //添加值为 1 的结点
	heap.insert(1);
	cout<<"测试二:"<<endl;
	heap.findAll();
	return 0;
}

tree19.png

tree14.png

继续添加值为 151983 个新结点,并检查二叉堆的状况。

int main(int argc, char** argv) {
	//省略……
	heap.insert(15);
	heap.insert(19);
	heap.insert(8);
	cout<<"测试三:"<<endl;
	heap.findAll();
	return 0;
}

tree20.png

tree15.png

上沉算法同样可以使用递归实现。

/*
*递归实现插入
*/
template<typename T>
int Heap<T>::insert_(T  val) {
	//存储在最后一个位置
	int pos= ++Heap<T>::size;
	Heap<T>::heapList[pos]=val;
	//调用
	Heap<T>::insertByRecursion(pos);
}
template<typename T>
int Heap<T>::insertByRecursion(int  pos) {
//找到父结点位置
	int parentIdx=  pos / 2;
	if(parentIdx==0)
		//出口一,没有父结点
		return pos;
	if( Heap<T>::heapList[pos]>Heap<T>::heapList[parentIdx] )
		//出口二:大于父结点
		return pos;
	else {
		//和父亲结点交换
		int temp=Heap<T>::heapList[pos];
		Heap<T>::heapList[pos]=Heap<T>::heapList[parentIdx];
		Heap<T>::heapList[parentIdx]=temp;
		//递归 
		Heap<T>::insertByRecursion(parentIdx);
	}
}

2.4 下沉算法

介绍完添加方法后,再来了解一下,如何使用下沉算法删除二叉堆中的结点。

二叉堆的删除操作从根结点开始,如下图删除根结点后,空出来的根结点位置,需要在整个二叉堆中重新找一个结点充当新的根结点。

tree04.png

二叉堆中使用下沉算法选择新的根结点:

  • 找到二叉堆中的最后一个结点,移到到根结点位置。如下图,把二叉堆中最后那个值为 19 的结点移到根结点位置。

tree05.png

  • 最小堆中,如果新的根结点的值比左或右子结点的值大,则和子结点交换位置。如下图,在二叉堆中把 195 的位置进行交换。

Tips: 总是和最小的子结点交换。

tree06.png

  • 交换后,如果还是不满足最小二叉堆父结点小于子结点的规则,则继续比较、交换新根结点直到下沉到二叉堆有序为止。如下,继续交换 1219 的值。如此反复经过多次交换直到整个堆结构符合二叉堆的特性。

tree07.png

removeoot 方法的具体实现:

/*
* 下沉算法,删除结点
*/
template<typename T>
T Heap<T>::removeRoot() {
	if(Heap<T>::size==0)return NULL;
	T root=Heap<T>::heapList[1];
	if(Heap<T>::size==1) {
		Heap<T>::size--;
		return root;
	}
	//堆中最后一个结点移动根结点
	Heap<T>::heapList[1]=Heap<T>::heapList[Heap<T>::size];
	Heap<T>::size--;

	//下沉算法
	int parentIdx=1;
	//子结点值
	T minChild;
	//子结点位置
	int idx;
	while(1) {
		//左结点位置
		int leftIdx=parentIdx*2;
		//右结点位置
		int rightIdx=parentIdx*2+1;
		if( leftIdx<=Heap<T>::size && rightIdx<=Heap<T>::size ) {
			//记录较小的结点值和位置
			minChild=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?Heap<T>::heapList[leftIdx]:Heap<T>::heapList[rightIdx];
			idx=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?leftIdx:rightIdx;
		} else if( leftIdx<=Heap<T>::size) {
			minChild=Heap<T>::heapList[leftIdx];
			idx=leftIdx;
		} else if( rightIdx<=Heap<T>::size ) {
			minChild=Heap<T>::heapList[rightIdx];
			idx=rightIdx;
		}else{
			//没有子结点 
			break;
		}
		//是否交换
		if( Heap<T>::heapList[parentIdx]>minChild ) {
			Heap<T>::heapList[idx]=Heap<T>::heapList[parentIdx];
			Heap<T>::heapList[parentIdx]=minChild;
			parentIdx=idx;
		} else {
			break;
		}
	}
	return root;
} 

测试在二叉堆中删除结点:

int main(int argc, char** argv) {
    //省略……
	cout<<"测试删除一:"<<endl;
	heap.removeRoot();
	heap.findAll();
	return 0;
}

tree16.png

可以看到最后二叉堆的结构和有序性都得到了完整的保持。

“下沉算法” 同样可以使用递归实现。

/*
*递归实现下沉算法
*/
template<typename T>
T Heap<T>::removeRoot_() {
	if(Heap<T>::size==0)return NULL;
	//根结点值
	T root=Heap<T>::heapList[1];
	//
	if(Heap<T>::size==1) {
		Heap<T>::size--;
		return root;
	}
	//堆中最后一个结点移动根结点
	Heap<T>::heapList[1]=Heap<T>::heapList[Heap<T>::size];
	Heap<T>::size--;
	//调用
	Heap<T>::removeRootByRecursion(1);
	return root;
}

template<typename T>
void Heap<T>::removeRootByRecursion(int parentIdx ) {
	//子结点值
	T minChild;
	//子结点位置
	int idx;
	//左结点位置
	int leftIdx=parentIdx*2;
	//右结点位置
	int rightIdx=parentIdx*2+1;
	if( leftIdx<=Heap<T>::size && rightIdx<=Heap<T>::size ) {
		//记录较小的结点值和位置
		minChild=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?Heap<T>::heapList[leftIdx]:Heap<T>::heapList[rightIdx];
		idx=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?leftIdx:rightIdx;
	} else if( leftIdx<=Heap<T>::size) {
		minChild=Heap<T>::heapList[leftIdx];
		idx=leftIdx;
	} else if( rightIdx<=Heap<T>::size ) {
		minChild=Heap<T>::heapList[rightIdx];
		idx=rightIdx;
	} else {
		//没有子结点
		return;
	}
	//是否交换
	if( Heap<T>::heapList[parentIdx]>minChild ) {
		Heap<T>::heapList[idx]=Heap<T>::heapList[parentIdx];
		Heap<T>::heapList[parentIdx]=minChild;
        //递归
		Heap<T>::removeRootByRecursion(idx);
	} else {
		return;
	}
}

3. 堆排序

堆排序指借助堆的有序性对数据进行排序。

  • 需要排序的数据以堆的方式保存。
  • 然后再从堆中以根结点方式取出来,无序数据就会变成有序数据 。

如有数列=[4,1,8,12,5,10,7,21,3],现通过堆的数据结构进行排序。

int main(int argc, char** argv) {
	//实例化堆
	Heap<int> heap;
	int nums[] = {4,1,8,12,5,10,7,21,3};
	int size=sizeof(nums)/4;
    // 创建根节点
	heap.setRoot(nums[0]);
    // 其它数据添加到二叉堆中
	for (int i=1; i<size; i++) {
		heap.insert(nums[i]);
	}
	cout<<"堆中数据:"<<endl;
	heap.findAll();
    // 获取堆中的数据
	for(int i=0; i<size; i++ ) {
		nums[i]= heap.removeRoot();
		heap.findAll();
	}
	for(int i=0; i<size; i++)
		cout<<nums[i]<<"\t";
	return 0;
}

输出结果:

tree22.png

本例中的代码还有优化空间,本文试图讲清楚堆的使用,优化的地方交给有兴趣者。

4. 后记

在树结构上加上一些新特性要求,树会产生很多新的变种,如二叉树,限制子结点的个数,如满二叉树,限制叶结点的个数,如完全二叉树就是在满二叉树的“满”字上做点文章,让这个’'满"变成"不那么满"。

在完全二叉树上添加有序性,则会衍生出二叉堆数据结构。利用二叉堆的有序性,能轻松完成对数据的排序。

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

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

相关文章

xpath获取标签之间的文本内容

目前在学习xpath&#xff0c;需要取一个package信息&#xff0c;如图&#xff1a; 标题 "package"在span里面&#xff0c;比较方便取&#xff0c;用这个xpath就行&#xff1a; //div[idartikeltabelle]/table/tbody/tr[1]/td[2]/span[classinsertlabela and text()pa…

/mmcv/_ext.cpython-36m-x86_64-linux-gnu.so: undefined symbol: _ZN3c107Warning

报/mmcv/_ext.cpython-36m-x86_64-linux-gnu.so: undefined symbol: _ZN3c107Warning 就是mmcv或者mmcv-full编译有问题&#xff1b; 尝试了mmcv_full-1.7.1-cp36-cp36m-manylinux1_x86_64.whl pip 安装&#xff0c;都不是对应版本&#xff1b; 在安装mmcv或者mmcv-full都是无…

如何用PPT画出好看的科研图

前言 好看的科研图可以从前人的作品中进行借鉴&#xff0c;今天介绍2副精美的科研图以及他们在PPT中的绘制方法&#xff0c;话不多说&#xff0c;先摆上标准科研图 1. 黑色粗体边框以及淡填充颜色 黑色粗体边框和淡填充颜色真的让矩形一下子变得很有质感&#xff0c;在学习的…

VMware Fusion设置静态IP+端口转发(macOS)+内网穿透

很少有资料提到配置macOS上VMware Fusion的端口转发&#xff0c;因此我在这里进行了一些记录打开网络设置解锁并添加新的网络适配器只有新的网络适配器才允许配置转发&#xff0c;默认的不允许这样做。设置NAT转发虚拟机选择使用新创建的虚拟网络适配器Ubuntu配置静态IP/DNS服务…

linux xshell用户免密登录设置

最重要准备工作 ​编辑linux xshell用户免密登录设置步骤 1.在xshel进行密钥获取操作 2.创建mkdir ~/.ssh目录&#xff08;检查有没有这个目录&#xff0c;没有自己添加&#xff09; 3.在该目录创建authorized_keys目录。注意&#xff08;目录权限为600&#xff09; 4.将刚…

Android Studio arctic Fox(北极狐)导入openCV

Android studio arctic Fox在引入opencv的时候按照正常的File->New->Import Module操作时&#xff0c;出现无法点击“Next”和“Finish”的情况。如下图 所以我们使用另外的方法进行引入。 准备工作&#xff1a; 1&#xff0c;Android Studio 2&#xff0c;下载opencv…

纪念2022年11月软考高项(信息系统项目管理师)一次通过的经验随笔

一、备考背景2022年5月的浙江软考因为疫情防控原因临时取消&#xff0c;我离杭州买房又远了几分。众所周知&#xff0c;杭州是炒房投资客的香饽饽&#xff0c;即使主城区购房摇号制度不停打补丁&#xff0c;也难济于事。自住客为了与投资客抗衡&#xff0c;一靠社保&#xff0c…

新品做软文推广发布在哪些平台好?

新产品上市前后&#xff0c;会经历开发期、介绍期、成长期、成熟期、衰退期五个阶段&#xff0c;每个阶段都需要软文推广的助力&#xff01; 当一款新品上市前后往往会面临着无法打开市场、产品卖不出去等问题&#xff0c;这个时候软文推广就要做到位&#xff0c;除了文章撰写…

在centos7安装KubeSphere

节点准备 设置hostname hostnamectl set-hostname ks-m4 关闭防火墙 启动&#xff1a; systemctl start firewalld 关闭&#xff1a; systemctl stop firewalld 查看状态&#xff1a; systemctl status firewalld 开机禁用 &#xff1a; systemctl disable firewalld 开机…

SAP 通过 OData Service 反查 CDS View 位置 视图「Workaround」

前言 使用场景&#xff1a;目前已知 OData Service 由 CDS View 发布&#xff0c;但是想要查看该 CDS 的具体内容 根据 OData Service 获取关键信息盲猜对应底表通过 SE11 反查引用位置 猜底表 目前已知 OData Service URL: http://xxx.xxx.com:xxxx/sap/opu/odata/sap/ZCD…

栉风沐雨砥砺行,春华秋实满庭芳——华秋电子2022年度大事记

岁序更替&#xff0c;春华秋实 转眼间&#xff0c;2022年已是过去 回首2022年&#xff0c;华秋肩负使命 持续为电子产业增效降本。 我们加大研发投入&#xff0c;提升全球交付保障能力&#xff1b; 我们以创新引领发展&#xff0c;以实干笃定前行&#xff1b; 以品质为基…

JetBrains 学生和教师认证教程

目录一、学生和教师授权申请方式二、申请网址三、激活JetBrains产品&#xff08;如PyCharm、IDEA&#xff09;一、学生和教师授权申请方式 详见官方教程&#xff1a; JetBrains 学生和教师授权申请方式 二、申请网址 学生和教师可以使用在学校注册的邮箱账号来申请。 新申请…

「EZEC-4」可乐(2种方法:差分+位运算 | 枚举+字典树)

「EZEC-4」可乐 洛谷&#xff1a;「EZEC-4」可乐 题目背景 很久以前&#xff0c;有一个 pigstd&#xff0c;非常迷恋美味的可乐。为了得到美味的可乐&#xff0c;他几乎用尽了所有的钱&#xff0c;他甚至对自己的 npy 也漠不关心其实是因为他没有npy&#xff0c;更不爱好看戏…

C语言实现烟花表白,内含源码!!

虽然现在看烟花有一定难度&#xff0c;但代码式烟花可以随时随地看&#xff01; 烟花的代码很多&#xff0c;实际上是可以用 Python、HTML5 等语言写烟花&#xff0c;但今天主要想和大家分享用C语言写的烟花代码&#xff0c;非常细致和实用。 同学们一定要亲自敲一遍&#xf…

机器视觉(八):图像特征提取

目录&#xff1a; 机器视觉&#xff08;一&#xff09;&#xff1a;概述 机器视觉&#xff08;二&#xff09;&#xff1a;机器视觉硬件技术 机器视觉&#xff08;三&#xff09;&#xff1a;摄像机标定技术 机器视觉&#xff08;四&#xff09;&#xff1a;空域图像增强 …

理解文件系统

之前的东西&#xff0c;全部是在内存中的。但是我们知道&#xff1a;不是所有的文件&#xff0c;都被打开了。大量的文件&#xff0c;就在磁盘上放着&#xff0c;什么都不做。这些文件非常多&#xff0c;杂&#xff0c;乱。而磁盘级别的文件管理&#xff0c;本质工作就能够快速…

maven第三篇:maven 简单命令

上一篇聊了在IDE中如何创建maven项目&#xff0c;后面说到了maven会有一些常用的命令&#xff0c;现在就对命令进简单演示。 注意&#xff1a; 1&#xff1a;命令需要在windows桌面系统下的dos窗口运行。 2&#xff1a;下面演示的命令除了查看版本信息的命令外&#xff0c;很…

科技创未来!中国低代码平台公司流辰信息用实力护您增产又增收!

流辰信息科技&#xff0c;是一家以研发低代码开发平台为使命的中国低代码平台公司&#xff0c;坚持以客户需求为主导&#xff0c;以开源促创新&#xff0c;努力做强做大流辰信息&#xff0c;争创宏伟未来。 在多年的奋斗耕耘中&#xff0c;流辰信息坚持研发&#xff0c;让产品的…

磨金石教育分享||想转行学习插画,应该怎么做,需要什么基础?

近年来&#xff0c;随着互联网行业的快速崛起&#xff0c;来自广告、自媒体、影视、游戏、动漫等多个行业对插画师这一职业求贤若渴。招聘网站信息不觉&#xff0c;特别是一些经验丰富&#xff0c;水平较高的插画师&#xff0c;在业内享有较高的声誉&#xff0c;是各个公司的主…

LaTeX 进阶语法

文章目录LaTeX进阶语法一、 样式排版1、 字体和字号1.1 字体样式1.2 字号1.3 ctex宏包更改中文字体1.4 文字装饰2、 段落格式和间距2.1 长度和长度变量2.2 行距2.3 段落格式2.4 水平间距2.5 垂直间距3、 页面和分栏3.1 页面介绍3.2 利用 geometry 宏包设置页面参数3.3 页面内容…