基 础 算 法 普 及 之 堆 排 序 (中)

Posted 共振邀你来杂谈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基 础 算 法 普 及 之 堆 排 序 (中)相关的知识,希望对你有一定的参考价值。

下方二维码长按识别即可添加


以下是作者个人微信,有需要的可以添加

基 础 算 法 普 及 之 堆 排 序 (中)




本次推文将介绍堆这个数据结构以及堆排序。本次讲解的是大顶堆结构,之后可能会补充索引堆以及最小索引堆等内容。


话不多说,先介绍”堆“这个数据结构。


讲解堆之前我们可以先了解一下二叉树这个数据结构。


首先介绍二叉树的结点数据结构,如下图所示

基 础 算 法 普 及 之 堆 排 序 (中)


二叉树顾名思义是只有不超过两个叉的枝干的树形结构。如下图所示

基 础 算 法 普 及 之 堆 排 序 (中)

上图展示的就是一个二叉树的结构,同时它还是一棵满二叉树。何为满二叉树?请看下图解释。

基 础 算 法 普 及 之 堆 排 序 (中)

堆采用的就是类似二叉树的结构,但是堆一般是使用数组来实现的,一说到数组,索引便成了主要问题,如何将满二叉树转换为数组中的索引呢?这使用的便是满二叉树的特点,如果有分叉的话一定拥有左孩子。解释以及索引转换方法如下图所示,为了方便计算我们将浪费数组中的第0号空间。

基 础 算 法 普 及 之 堆 排 序 (中)

本推文以介绍大顶堆为主,什么是大顶堆?大顶堆就是父节点的值大于其子节点的值,像上图的例子即为一个大顶堆。(题外语:大顶堆允许8号结点的值小于七号结点的值,而对于二叉搜索树来说这是不允许的。)

接下来将要介绍堆的两个重要方法shiftUp和shiftDown,前者用于往堆中插入一个新的结点,后者用于从堆中取出一个节点。

下面模拟一下shiftUp,即插入结点时的操作

step1:

基 础 算 法 普 及 之 堆 排 序 (中)

step2:

基 础 算 法 普 及 之 堆 排 序 (中)

step3:

基 础 算 法 普 及 之 堆 排 序 (中)

step4:

因为51 小于 62,所以shiftUp过程结束。

构成的最大堆的二叉树形式如下图

基 础 算 法 普 及 之 堆 排 序 (中)

对应的堆就按每个结点右上角的索引构成

shiftUp代码如下(C++):

 template<typename Item>    //调用插入方法时用到shiftUp方法 void insert(Item item){        assert(count+1 <= capacity); data[count+1] = item; count++; shiftUp(count);    }    void shiftUp(int k){ while(k>1 && data[k]>data[k/2]){ swap(data[k],data[k/2]); k /= 2; } }

接下来再来介绍shiftDown方法。shiftDown方法用于从堆中取出节点操作中。

step1:

基 础 算 法 普 及 之 堆 排 序 (中)

step2:

基 础 算 法 普 及 之 堆 排 序 (中)

step3:

基 础 算 法 普 及 之 堆 排 序 (中)

step4:

 

基 础 算 法 普 及 之 堆 排 序 (中)

shiftDown后的堆的二叉树形结构如下图:

基 础 算 法 普 及 之 堆 排 序 (中)

代码如下:(C++实现)

 template<typename Item> Item extractMax(){ assert( count > 0); Item ret = data[1]; swap(data[1],data[count]);
count--; shiftDown(1); return ret; } void shiftDown(int k){ while(2*k<=count){ int j = 2*k; //判断有无右孩子,再比较左右孩子大小 if(j+1<=count&&data[j+1]>data[j]){           j++;         } if(data[k]>data[j]){          break;         }         swap(data[k],data[j]);            k = j; } }

有了这两个方法,我们就可以实现基础的最大排序

首先给定要排序的数组,再给定其长度,那么我们只需遍历一遍所给数组然后将其一个一个插入堆中即可,因为我们建立的是大顶堆即最大堆,假如我们想要得到一个从小到大的数组,那么我们就只需要将从堆中取出的结点从末尾放入待排序的数组,反之就从头往后放入。代码如下:

template<typename T>void heapSort1(T arr[],int n){ MaxHeap<T> maxHeap = MaxHeap<T>(n); for(int i=0;i<n;i++){ maxHeap.insert(arr[i]); }
for(int i=n-1;i>=0;i--){ arr[i] = maxHeap.extractMax(); }
return;}

之后是对heapSort1进行优化,优化的思路便是改进构建堆时不停的插入操作,解决方案为Heapify操作,如下图

基 础 算 法 普 及 之 堆 排 序 (中)

step1:

基 础 算 法 普 及 之 堆 排 序 (中)

step2:

基 础 算 法 普 及 之 堆 排 序 (中)

step3:

基 础 算 法 普 及 之 堆 排 序 (中)

Heapify结果如下图

基 础 算 法 普 及 之 堆 排 序 (中)

Heapify代码如下:(C++实现)

 MaxHeap(Item arr[], int n){ data = new Item[n+1]; count = n; this->capacity = n; for(int i=0;i<n;i++){ data[i+1] = arr[i]; }
for(int j=count/2;j>=0;j--){ shiftDown(j); } } template<typename T> void heapSort2(T arr[],int n){ MaxHeap<T> maxHeap = MaxHeap<T>(arr,n); for(int i=n-1;i>=0;i--){ arr[i] = maxHeap.extractMax(); } return; }

对于以上方法我们还发现了问题,就是空间浪费问题,在每次构建堆时我们都需要创建一个与原数组一样大小的数组,这样花费了额外的空间,此时我们可以寻找一个原地排序的方法。

step:

1、对待排序的数组进行一次Heapify操作

2、每次取出堆中第一个元素与堆中最后一个元素交换位置    

步骤如下图所示

基 础 算 法 普 及 之 堆 排 序 (中)

step:

基 础 算 法 普 及 之 堆 排 序 (中)

之后的步骤不再演示,大家应该都能明白我的意思

下一步便是将0号结点与3号结点交换位置,然后对交换位置后的0号结点进行shiftDown操作。

再下一步便是0号结点与2号结点交换位置,……

最后一步便是0号结点与1号结点交换位置,……

代码如下:(C++实现)

template<typename T>void heapSort(T arr[],int n){ for(int i=(n-2)/2;i>=0;i--){ __shiftDown2(arr, n ,i); }
for(int i=n-1;i>0;i--){ swap(arr[i],arr[0]); __shiftDown2(arr,i,0); } return;}template<typename T>void __shiftDown2(T arr[],int n,int k){ T e = arr[k]; while(2*k+1<n){ int j = 2*k+1; if(j+1<n&&arr[j+1]>arr[j]){ j++; } if(arr[j]<=e){ break; } arr[k] = arr[j]; k = j; } arr[k] = e; return;}


以上便是今天的全部内容,大家若有疑问欢迎评论区留言或者私信我


预告:下次将会和大家分享快速排序相关内容,如果有时间一并把归并排序给大家讲解了基 础 算 法 普 及 之 堆 排 序 (中)



编者不易,欢迎读者们打赏

以上是关于基 础 算 法 普 及 之 堆 排 序 (中)的主要内容,如果未能解决你的问题,请参考以下文章

阅T读I基N础A

重温基础算法内部排序之堆排序法

重温基础算法内部排序之堆排序法

八大排序算法(C语言实现)

二叉树算法小结

什么是字典排序