C++ 不知树系列之二叉堆排序(递归和非递归实现上沉、下沉算法)
创始人
2024-05-07 04:03:32
0

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 
using namespace std;
/* 
* 堆类 
*/ 
template
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<heapList[i]<<"\t";cout<

Heap 类中的属性详解:

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

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

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

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

Heap 类中的方法介绍:

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

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

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

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

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

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

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

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

2.3 上沉算法

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

tree09.png

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

tree10.png

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

tree11.png

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

tree12.png

编码实现 insert 方法

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

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

int main(int argc, char** argv) {//实例化堆Heap heap;//初始化根结点heap.setRoot(5);//检查根结点是否创建成功int rootVal=heap.getRoot();cout<<"根结点的值:"<

输出结果:

tree18.png

tree13.png

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

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

tree19.png

tree14.png

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

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

tree20.png

tree15.png

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

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

2.4 下沉算法

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

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

tree04.png

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

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

tree05.png

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

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

tree06.png

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

tree07.png

removeoot 方法的具体实现:

/*
* 下沉算法,删除结点
*/
template
T Heap::removeRoot() {if(Heap::size==0)return NULL;T root=Heap::heapList[1];if(Heap::size==1) {Heap::size--;return root;}//堆中最后一个结点移动根结点Heap::heapList[1]=Heap::heapList[Heap::size];Heap::size--;//下沉算法int parentIdx=1;//子结点值T minChild;//子结点位置int idx;while(1) {//左结点位置int leftIdx=parentIdx*2;//右结点位置int rightIdx=parentIdx*2+1;if( leftIdx<=Heap::size && rightIdx<=Heap::size ) {//记录较小的结点值和位置minChild=Heap::heapList[leftIdx]::heapList[rightIdx]?Heap::heapList[leftIdx]:Heap::heapList[rightIdx];idx=Heap::heapList[leftIdx]::heapList[rightIdx]?leftIdx:rightIdx;} else if( leftIdx<=Heap::size) {minChild=Heap::heapList[leftIdx];idx=leftIdx;} else if( rightIdx<=Heap::size ) {minChild=Heap::heapList[rightIdx];idx=rightIdx;}else{//没有子结点 break;}//是否交换if( Heap::heapList[parentIdx]>minChild ) {Heap::heapList[idx]=Heap::heapList[parentIdx];Heap::heapList[parentIdx]=minChild;parentIdx=idx;} else {break;}}return root;
} 

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

int main(int argc, char** argv) {//省略……cout<<"测试删除一:"<

tree16.png

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

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

/*
*递归实现下沉算法
*/
template
T Heap::removeRoot_() {if(Heap::size==0)return NULL;//根结点值T root=Heap::heapList[1];//if(Heap::size==1) {Heap::size--;return root;}//堆中最后一个结点移动根结点Heap::heapList[1]=Heap::heapList[Heap::size];Heap::size--;//调用Heap::removeRootByRecursion(1);return root;
}template
void Heap::removeRootByRecursion(int parentIdx ) {//子结点值T minChild;//子结点位置int idx;//左结点位置int leftIdx=parentIdx*2;//右结点位置int rightIdx=parentIdx*2+1;if( leftIdx<=Heap::size && rightIdx<=Heap::size ) {//记录较小的结点值和位置minChild=Heap::heapList[leftIdx]::heapList[rightIdx]?Heap::heapList[leftIdx]:Heap::heapList[rightIdx];idx=Heap::heapList[leftIdx]::heapList[rightIdx]?leftIdx:rightIdx;} else if( leftIdx<=Heap::size) {minChild=Heap::heapList[leftIdx];idx=leftIdx;} else if( rightIdx<=Heap::size ) {minChild=Heap::heapList[rightIdx];idx=rightIdx;} else {//没有子结点return;}//是否交换if( Heap::heapList[parentIdx]>minChild ) {Heap::heapList[idx]=Heap::heapList[parentIdx];Heap::heapList[parentIdx]=minChild;//递归Heap::removeRootByRecursion(idx);} else {return;}
}

3. 堆排序

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

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

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

int main(int argc, char** argv) {//实例化堆Heap 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

输出结果:

tree22.png

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

4. 后记

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

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

相关内容

热门资讯

没有尾巴的狗高一作文(最新3... 没有尾巴的狗高一作文 篇一没有尾巴的狗狗是人类最忠诚的朋友,它们陪伴我们、保护我们,给我们带来无尽的...
不该错过的风景600字中考优... 不该错过的风景600字中考优秀作文 篇一夕阳下的海滩夏天的海滩,总是吸引着无数的游客前来欣赏海的美丽...
那天以后高一作文(精彩3篇) 那天以后高一作文 篇一传统文化与现代生活的碰撞那天以后,我进入了高一阶段,迎来了新的学习生活。与以往...
庄子与鱼中考作文范文(通用6... 庄子与鱼中考作文范文 篇一庄子与鱼的故事在我们中考作文中常常被引用,它给我们启示了很多关于人生和生活...
中考数学答题技巧【精简5篇】 中考数学答题技巧 篇一中考数学是许多学生都比较头疼的一门科目,因为它需要运用一定的计算能力和逻辑思维...
遇见中考满分作文600字【推... 遇见中考满分作文600字 篇一:勇敢面对挑战,迎接中考中考,是每个初中生都无法绕过的一座大山。它如同...
中考英语必背作文(通用6篇) 中考英语必背作文 篇一How to Maintain a Healthy LifestyleA he...
河北中考政策解读(优质3篇) 河北中考政策解读 篇一近年来,河北省中考政策一直备受广大学生和家长的关注。中考是学生人生中的重要节点...
倡议书中考作文范文(优质6篇... 倡议书中考作文范文 篇一倡议书中考作文范文尊敬的校领导、老师们、亲爱的同学们:大家好!我是XX班的学...
期中考试后的反思与总结【优选... 期中考试后的反思与总结 篇一期中考试已经结束,作为学生,我们应该对自己的表现进行反思与总结,从而找到...
台阶500字作文【最新3篇】 台阶500字作文 篇一走过的每一步都是人生的台阶人生就像一座楼梯,每个人都要一步一步地往上走。在这个...
中考满分作文精彩句子(优秀5... 中考满分作文精彩句子 篇一:热爱阅读,开启智慧之门1. 阅读是一扇开启智慧之门的钥匙,它能够拓宽我们...
中考满分作文记叙文600字 中考满分作文记叙文600字大全  在平时的学习、工作或生活中,大家总免不了要接触或使用作文吧,作文是...
期中考试【优质3篇】 期中考试 篇一期中考试是学生们在学期中进行的一次重要的考核。它不仅能够检验学生们对于所学知识的掌握程...
中考议论文写作指导:引议联接... 中考议论文写作指导:引议联接法 篇一引议联接法是一种常用的写作手法,特别适用于中考议论文的写作。它通...
中考家长寄语 中考家长寄语(精选215句)  中考不可怕,因为它是我们自己第一个奋力的战场,所以才显得那么与众不同...
中考作文文化类范文(推荐6篇... 中考作文文化类范文 篇一中国传统节日的意义中国是一个拥有悠久历史和丰富文化的国家,拥有着许多传统节日...
西游记中考语文阅读复习重点【... 西游记中考语文阅读复习重点 篇一西游记是中国古代四大名著之一,也是中学语文教材的重点阅读内容之一。通...
生命的意义中考作文(通用3篇... 生命的意义中考作文 篇一生命,是一种无比珍贵的存在。它给予了我们无尽的可能性和机会,让我们能够感受到...
中考作文(推荐6篇) 中考作文 篇一:如何做好中考准备中考是每个初中生都要面临的一场考试,它对于我们的学习和未来的发展都有...