前言
在我的的专栏中,其余排序陆陆续续都已经写了,而堆排序迟迟没有写,趁着国庆假期的尾声,把堆排序也写一写。
插入类排序—(折半)插入排序、希尔排序java
两分钟搞懂桶排序数据结构
对于常见的快排、归并这些O(nlogn)的排序算法,我想大部分人可能很容易搞懂,可是堆排序大部分人可能比较陌生,或许在Java的comparator接口中可能了解一点。但堆排序在应用中好比优先队列此类维护动态数据效率比较高,有着很是普遍的应用。函数
而堆排序能够拆分红堆和排序,其中你可能对堆比较陌生,对排序比较熟悉,下面就带你完全了解相关内容。
学习

堆
什么是堆?spa
谈起堆,不少人第一联想到的是土堆,而在数据结构中这种土堆与彻底二叉树更像,而堆就是一类特殊的数据结构的统称。堆一般是一个能够被看作一棵树(彻底)的数组对象。且老是知足如下规则:.net
堆是一棵彻底二叉树设计
每一个节点老是大于(或小于)它的孩子节点。
彻底二叉树
我想什么是彻底二叉树大部分人也是知道:最后一层以上都是满的,最后一层节点从左到右能够排列(有任何空缺即不知足彻底二叉树)。

看做树的数组对象
咱们都知道咱们排序的对象通常都是对数组之类的序列进行排序,若是转成抽象数据结构再实现可能成本比较大。
咱们正常在构造一棵二叉树的时候一般采用链式left,right节点,但其实二叉树的表示方式用数组也能够实现,只不过普通的二叉树若是用数组储存可能空间利用 效率会很低而不多采用,但咱们的堆是一颗彻底二叉树。使用数组储存空间使用效率也比较高,因此在形式上咱们把这个数组当作对应的彻底二叉树,而操做上能够直接操做数组也比较方便。

大根堆 VS 小根堆
上面还有一点就是在这个彻底二叉树中全部节点均大于(或小于)它的孩子节点,因此这里就分为两种状况
若是全部节点大于孩子节点值,那么这个堆叫作大根堆,堆的最大值在根节点。
若是全部节点小于孩子节点值,那么这个堆叫作小根堆,堆的最小值在根节点。

堆排序
经过上面的介绍,我想你对堆应该有了必定的认识,堆排序确定是借助堆实现的某种排序,其实堆排序的总体思路也很简单,就是
构建堆,取堆顶为最小(最大)。
将剩下的元素从新构建一个堆,取堆顶,一直到元素取完为止。
建堆
若是给一个无序的序列,首先要给它建成一个堆,咱们如何实现这个操做呢?如下拿一个小根堆为例进行分析。
对于二叉树(数组表示),咱们从下往上进行调整,从第一个非叶子节点开始向前调整,对于调整的规则以下:
①对于小根堆,当前节点与左右孩子比较,若是均小于左右孩子节点,那么它自己就是一个小根堆,它不须要作任何改变,若是左右有孩子节点比它还小,那么就要和最小的那个进行替换。

②可是普通节点替换可能没问题,对于某些和子节点替换有可能改变子树成堆,因此须要继续往下判断交换(最差判断到叶子节点)。

分析构造堆的这个过程,每一个非叶子节点都须要判断比较是否交换,这样一层就是O(n),而每一个节点可能替换以后影响子节点成堆须要再往下判断遍历,你可能会认为它是一个O(nlogn),但其实你看看二叉树性值,大部分都是在底部的,上面的只有不多个数,若是你用数学方法去求得最终的复杂度它仍是一个O(n)级别,这里就不做详细介绍了。
一个大根堆创建过程也是同样的:

堆排序
上面的一个堆建造完毕以后,咱们怎么去利用这个堆实现排序呢?答案也是很简单的,咱们知道堆有一个特性就是堆顶是最小(或最大),而咱们建造这个若是去除第一个元素,剩余左右孩子依然知足堆的性质。

将最后一个元素放置堆顶,因为第一个元素的存在使得整个不知足堆的性质。分析这个结构,和咱们前面构造堆的过程当中构造到第一个元素的操做相同:
判断左右孩子,若是须要交换则交换,交换后再次考虑交换子节点是否须要交换。一直到不须要考虑。

这样到最后,堆排序便可完成,最终获得的序列即为堆排序序列。
一个大根堆的排序过程以下:
具体实现
有了上述的思想以后,如何具体的实现这个堆排序的代码呢?
从细致的流程来看,大概流程是以下的:
给定数组建堆(creatHeap)
从第一个非叶子节点开始判断交换下移(shiftDown),使得当前节点和子孩子可以保持堆的性值
若是交换打破子孩子堆结构性质,那么就要从新下移(shiftDown)被交换的节点一直到中止。
堆构造完成,取第一个堆顶元素为最小(最大),剩下左右孩子依然知足堆的性值,可是缺个堆顶元素,若是给孩子调上来,可能会调动太多而且可能破坏堆结构。
因此索性把最后一个元素放到第一位。这样只须要判断交换下移(shiftDown),不过须要注意此时整个堆的大小已经发生了变化,咱们在逻辑上不会使用被抛弃的位置,因此在设计函数的时候须要附带一个堆大小的参数。
重复以上操做,一直堆中全部元素都被取得中止。
而堆算法复杂度的分析上,以前建堆时间复杂度是O(n)。而每次删除堆顶而后须要向下交换,每一个个数最坏为logn个。这样复杂度就为O(nlogn).总的时间复杂度为O(n)+O(nlogn)=O(nlogn).
具体实现的代码以下:
import java.util.Arrays;
public class 堆排序 {
static void swap(int arr[],int m,int n)
{
int team=arr[m];
arr[m]=arr[n];
arr[n]=team;
}
//下移交换 把当前节点有效变换成一个堆(小根)
static void shiftDown(int arr[],int index,int len)//0 号位置不用
{
int leftchild=index*2+1;//左孩子
int rightchild=index*2+2;//右孩子
if(leftchild>=len)
return;
else if(rightchild<len&&arr[rightchild]<arr[index]&&arr[rightchild]<arr[leftchild])//右孩子在范围内而且应该交换
{
swap(arr, index, rightchild);//交换节点值
shiftDown(arr, rightchild, len);//可能会对孩子节点的堆有影响,向下重构
}
else if(arr[leftchild]<arr[index])//交换左孩子
{
swap(arr, index, leftchild);
shiftDown(arr, leftchild, len);
}
}
//将数组建立成堆
static void creatHeap(int arr[])
{
for(int i=arr.length/2;i>=0;i--)
{
shiftDown(arr, i,arr.length);
}
}
static void heapSort(int arr[])
{
System.out.println("原始数组为 :"+Arrays.toString(arr));
int val[]=new int[arr.length]; //临时储存结果
//step1建堆
creatHeap(arr);
System.out.println("建堆后的序列为 :"+Arrays.toString(arr));
//step2 进行n次取值建堆,每次取堆顶元素放到val数组中,最终结果即为一个递增排序的序列
for(int i=0;i<arr.length;i++)
{
val[i]=arr[0];//将堆顶放入结果中
arr[0]=arr[arr.length-1-i];//删除堆顶元素,将末尾元素放到堆顶
shiftDown(arr, 0, arr.length-i);//将这个堆调整为合法的小根堆,注意(逻辑上的)长度有变化
}
//数值克隆复制
for(int i=0;i<arr.length;i++)
{
arr[i]=val[i];
}
System.out.println("堆排序后的序列为:"+Arrays.toString(arr));
}
public static void main(String[] args) {
int arr[]= {14,12,16,8,9,1,14,9,6 };
heapSort(arr);
}
}
执行结果:

固然,代码为了成章节我把它命名为中文,还有些不规范的地方请注意甄别。
结语
对于堆排序就先介绍到这里了,固然堆的强大之处不止这么一点,优先队列一样也是用到堆可是这里就不详细介绍了,我相信优秀的你确定又掌握了一门O(nlogn)级别的排序算法啦。若是写的有啥不确切地方还请指正。
最后,若是感受不错一键三联哦!,欢迎关注公众号:bigsai
,更多精彩等你来看。期待你的关注,而且笔者也准备了一波pdf学习资源分享给你。

点赞、在看二连
本文分享自微信公众号 - bigsai(bigsai)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。