1、概述。
HeapSort 堆排序:利用
二叉堆
这种数据结构来进行设计的一种排序算法,可以利用数组的特点快速定位指定索引的元素。
此处先了解几个基本概念:
二叉树:是树的一种,主要的特点是二叉树的所有节点
最多只有两个叶节点。
完全二叉树:就是在二叉树当中,除了最后一层之外,其它所有层的节点都是满的,且最后一层的节点也是从左到右,优
先填满左边的节点。
满二叉树:又是一种特殊的完全二叉树,满二叉树的最后一层也是满的。也就是说,除了最后一层的节点外所有的节点都
有两个子节点。
二叉树对于节点之间的大小关系都没有要求,节点之间的大小关系是随机的。
二叉堆:是一种近似的完全二叉树,除了具有完全二叉树的特性还对节点之间的大小关系有要求:
父节点的值不小于/不
大于任何一个子节点的
值。当不大于时为最小堆,相反为最大堆。 二叉堆的一种作用就是用于堆排序。
2、为什么有堆排序?
堆排序其实是优先队列的实现。那么什么是优先队列那?
普通队列:先进先出。
优先队列:出队的顺序和入队的顺序无关,和优先级相关。
这个优先级怎么定义就看怎么实现的了,我们很多时候都会用到优先队列,动态的取出队列中需要优先执行的元素进行任务的分配和执
行。为
什么要使用优先队列那?如:在N个元素中选出前M个元素。使用如快排的时候 时间复杂度:NlogN、使用优先队列可实现NlogM的时间复杂度。
优先队列的实现:
使用普通数组实现(就是入队时不排序,直接入队): 入队时间复杂度 O(1); 出队时间复杂度O(n)需要从n个元素中找到优先级高的。
使用有序数组实现(入队的时候排好顺序):
入队时间复杂度 O(n) 需要找到合适的位置; 出队时间复杂度O(1)。
使用二叉堆实现:
入队时间复杂度 O(logn) ;出队时间复杂度O(logn)。
对于n个元素的优先队列的请求就有:
使用普通数组或者顺序数组情况O(n^2)。
使用二叉堆情况是O(nlogn)。
3、实现:
先上一个慕课网的图:
此处的图设计是按照从索引1开始的,下面是从0开始的时候规律:
package com.zy.heap;
public class HeapSort {
// 肯定的有一个私有的变量来表示HeapSort中数的个数。
private static int mCount;
//堆的容量.
private int mCapacity;
//此处我们使用数组来设计这个Heap
private int[] mHeap;
//也可以在设计一个默认容量的Heap,想想Android系统中就有很多类似的实现。
public HeapSort(int capacity){
//由于索引是从1开始的
mHeap = new int[capacity+1];
mCapacity = capacity;
}
public HeapSort (int[] src,int capacity){
//直接对传入的数组进行heapify操作
//基本思想
// 1、找到最下层、最左边第一个非叶子的节点。因为叶子节点肯定是符合二叉堆的,我们从第一个非叶子节点开始操作,直到最顶部的节点。
// 2、找到节点以后,一直和父节点比较执行shiftUp的操作直到找到合适的位置。
// 3、执行完这一个后,执行它前一个索引的元素,重复 2 步骤。
// 4、直到顶部节点
mHeap = new int[capacity+1];
for(int i=0;i<src.length;i++){
mHeap[i+1] = src[i];
}
mCount = capacity;
mCapacity = capacity;
//找到第一个非叶子节点.归纳发现第一个非叶子节点是(从1开始时)count/2.
//如有11个元素的时候,二叉堆中第一个非叶子节点是11/2 = 5;
//当有10个元素时候,二叉堆中第一个非叶子节点10/2 = 5;
//当有2、3个元素的时候就只有顶点1,此时1/2 = 0.5不符合要求了。
for(int i= mCount/2;i>= 1;i--){
//从下往上进行shiftdown,挨个索引进行.
shiftDown(i);
}
}
//获取堆中的数据。
public int getCount() {
return mCount;
}
//判空
public boolean isEmpty(){
return mCount == 0;
}
//判满
public boolean isFull(){
return mCount == mCapacity;
}
//往队列中插入元素
public boolean insert(int value){
//也可弄成自动扩容的。
if (isFull()) {
return false;
}
//往二叉堆中插入数据,首先把数组放在最后一个元素该在的位置(最底层最右边)
//注意这块的设计是从索引1开始的
mHeap[mCount+1] = value;
mCount++;//队列中的数据跟随者++
//调动元素使其继续满足最大堆。
shiftUp(value);
return true;
}
/**
* 整理数组内元素使其满足最大堆。
* @param value 需要往堆中插入数据.
*
*/
private void shiftUp(int value){
//插入完数据以后有可能是不满足最大堆或者最小堆的。
//此处我们是按照最大堆设计。
//我们此时的二叉树设父节点的索引为i,那么左子节点的索引为2*i,右节点的索引是2*i+1。
//反过来我们知道子节点索引时,由于/的特殊性,无论此时是左子节点还是右子节点.父节点的索引
//就是(子节点/2)即可。
int index = mCount;
//此处可以用赋值来代替交换进行优化,如果不是最顶部的父类,并且父类小于子类的时候交换.
//while(index > 1 && mHeap[index/2] < mHeap[mCount]){
//注意index应该和index/2一直循环作比较而不是mCount.
while(index > 1 && mHeap[index/2] < mHeap[index]){
exechange(mHeap,index, index/2);
index/=2;
}
}
//获取优先级最高的元素
public int getMax(){
if(isEmpty()){
//可以做一些其它的处理。
//throw new Exception("堆中的元素数为空");
}
//最大堆的设计,理念是最上面的就是优先级最高的,直接取出索引为1的元素就行。
int result = mHeap[1];
//此处的设计思路是把最后一个元素和顶部的元素交换,然后去掉这个索引。最后在把顶部元素进行shiftDown.
exechange(mHeap, 1, mCount);
mCount--;
//取出后需要调动内部元素继续满足最大堆.
shiftDown(1);
return result;
}
/**
* 调整元素使其内部满足最大堆
* @param index
*/
private void shiftDown(int index){
//从上往下整理最大堆。
//从上往下整理的时候,它应该是和左右子节点那个进行交换那?
//由于二叉堆特性父节点应该大于等于每一个子节点。所以和两个子节点当中较大的那个子节点进行交换。
//
while(2*index <= mCount){
//必须的判断左右两个谁大啊?此时还有可能不存在右子节点哦.
//左边的子索引 = 2*index;
//右边的子索引 = 2*index+1;
int left = 2*index;
//右子树可能不存在,如果右子树存在并右子树大于左子树
if(left+1 <= mCount && mHeap[left] < mHeap[left+1]){
//此处应该是交换右子树的节点,此处我们直接
left++;//移动到右子树
}
//到此处mHeap[left]就是存放的左右两个子节点中比较大的那个数。
//如果父节点比两个当中比较大的节点还大,那么就证明我们找到位置了。
if(mHeap[index] >= mHeap[left])
break;
//否则的换,父节点和比较大的子节点交换位置。
exechange(mHeap, index, left);
//交换完毕后需要继续往下找合适的地方,也就是从较大的那个元素在往下找,所以
index =left;//实现循环往下执行。
}
}
public static void exechange(int src[],int i,int j){
int temp = src[i];
src[i] = src[j];
src[j] = temp;
}
public void show(){
for (int i = 0; i < mHeap.length; i++) {
System.out.print("mHeap: ["+i+"] = "+mHeap[i]+",");
if (i%5==0) {
System.out.println();
}
}
System.out.println();
}
/**
* 最基本的排序.缺点很明显,首先需要额外的二叉堆来进行存储,第二
* 还需要对二叉堆进行一个个的插入和读取。但复杂度还是在nlogn的级别。
* @param src 数据源
*/
public static void basicHeapSort(int[] src){
int len = src.length;
//对传过来的数组进行排序。
//遍历数组放入到二叉堆当中进行排序。
//新建一个二叉堆,//此时内部就会创建一个数组用来按照二叉堆的形式存放这个数组的内容。
HeapSort maxHeap = new HeapSort(len);
for(int i = 0;i < len;i++){
maxHeap.insert(src[i]);
}
//到此就排序完成了,但是是在二叉堆中,所以还需要一个个取出来再放入到数组中。
//此处为了保证是从小到大排序,逆序取出
for(int i = len-1;i >= 0;i--){
src[i] = maxHeap.getMax();
}
}
/**
* 初步优化的堆排序,此处和第一步不同的是,此处是通过对数组进行shiftDown的操作
* 而最基本的排序其实是做的shiftUp的操作。
* 至于为什么要新弄个构造再进行,因为insert是对应的shiftUp但是没有对应shiftdown的啊.
* 结论:这两种操作进行heapify的操作要比前面的一个个插入快。
* 将n个元素插入到一个空堆中,算法的复杂度是O(nlogn)
* heapify的过程,算法复杂度为O(n)
*
*/
public static void heapSortHeapify(int[] src){
//利用带数组的构造,拿到呢不利用shiftdown操作已经是最大堆的二叉堆.
int len = src.length;
HeapSort heapify = new HeapSort(src, len);
//再把数一步步放到数组当中
for(int i = len-1;i >= 0;i--){
src[i] = heapify.getMax();
}
}
/**
* 前面的所有的排序都用到了额外的数组空间来存储二叉堆。然后进行对应的存取等操作,
* 那么接下来我们尝试优化进行原地排序。
* 步骤:
* 1、先通过heapify使得数组成一个最大推,这样第一个元素肯定是最大的,但是那后面不一定是有序的。
* 2、接下来,把最大的那个元素(也就是0号位置的元素)和最后面的元素交换位置,这样最大的元素就到数组的最后面了.
* 3、然后再对0号元素进行sahifdown(注意已经把最后的换过来了),使这个数组重新成为最大堆。
* 4、然后最大元素和倒数第二个元素进行交换,以此类推重复2、3步。
* 5、直到元素索引为1,1和最大的也就是0交换,此时进行最后一次的操作。
*
* 这样整体下来就是在数组当中进行的操作,而且越到后面需要的操作数越小。
* 注意:此时数组的索引时从0开始的,所以我们要按照0的索引进行相关功能的设计。
* 相关的索引规律:假设当前节点索引为i
* 左子节点:2*i+1 、 右子节点:2*i+2
* 根据索引求它的父节点时候是:(i-1)/2
*
* @param src 数据源
*/
public static void betterHeapSort(int[] src){
//先把这个数组进行heapify
int len = src.length;
//最后一个元素的索引
int index = len-1;
//HeapSort max = new HeapSort(src, len);应该是直接从数组开始啊
//还是从下层的左边的第一个非叶子节点开始,注意此处索引是从0开始的
//此时算是Heapyfy操作.
for(int i = (index-1)/2;i >= 0;i--){
//依次经行shiftDown
betterShiftDown(src,len, i);
}
for( int i = index; i > 0 ; i-- ){
//交换二叉堆中最大的那个数和数组中的最后一个
exechange(src,0,i);
//此处要遍历的数组越来越短,所以应该把最后的一个索引传入
betterShiftDown(src,i, 0);
}
}
//相比平常的shiftDown只不过这个索引是从0开始的。
public static void betterShiftDown(int[] src,int len,int index){
//左子节点存在的时候
while((2*index+1) < len){
int left = 2*index+1;
//看看右子节点在不在,如果存在看看和右边比较的大小。毕竟谁大和谁交换,这样保证子节点小于父节点。
if((left+1) < len && src[left+1] > src[left]){
left++;
}
if(src[left] <= src[index])
break;
exechange(src, left, index);
//然后接着往下循环.
index = left;
}
}
}