原创不易,如果你觉得对你有用,哪怕一点点也好,请留下一个赞再走,谢谢啦啦啦!!
一、前期知识储备
- 堆排序是对于完全二叉树而言
完全二叉树的那些事:
- 定义:对于一个树高为h的二叉树,如果其第0层至第h-1层的节点都满。如果最下面一层节点不满,则所有的节点在左边的连续排列,空位都在右边。这样的二叉树就是一棵完全二叉树(不理解没关系,是我的错,不许喷我)
- 性质:如果n个节点的完全二叉树的节点按照层次并按从左到右的顺序从0开始编号,对于每个节点都有:
- 序号为0的节点是根对于i>0,
- 其父节点的编号为(i-1)/2。
- 若2·i+1<n,其左子节点的序号为2·i+1,否则没有左子节点
- 若2·i+2<n,其右子节点的序号为2·i+2,否则没有右子节点。
是不是很枯燥?是不是看不下去?小伙子,别着急,磨刀不误砍柴工。,我给你上个图,你就懂上面所说的一切了。
2. 那堆又是什么呢?
- 堆的定义:
- 每个非叶子结点的值都大于或等于其左孩子和右孩子结点的值,称之为大根堆
- 每个结点的值都小于或等于其左孩子和右孩子结点的值,称之为小根堆
- 大根堆常用于升序操作
- 小根堆常用于降序操作
千言万语不胜一图,看图哈
二、堆如何跟排序挂上钩的?
理由很简单,因为堆这种数据结构可以用于排序(虽然感觉是废话,哈哈哈哈,不管了,先说人话)不信你往下看。
- 根据大顶堆的特性,我们可以知道大根堆的根节点就是所有节点的最大值
- 所以我们可以将最大值的根节点取下(人话:替换)与最后一个节点的值进行互换,然后将剩余的节点继续构造成大根堆
三、堆排序思想
非常重要呀呀呀,不懂也没关系,下面有图有真相
-
首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端(根节点)
-
将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
-
将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
四、如何进行堆排序(附亲手画的图)
上面说的是不是还是很懵逼???,没关系,既然是说人话,那我就上个图来解释:
说明:以下画的树必须满足完全二叉树,因为这是堆这个数据结构的前提
- 给定一个数组,如何知道非哪些元素是非叶子节点个数呢?
- 根据一条公式即可知道了,
total = arry.length-1
(下标从0开始)- 看下图哈
- 构建大顶堆顺序
- 依次处理非叶子节点
- 顺序是从上往下、从右往左
- 堆排序
简要说一下人话:
- 给定一个待排序的数组,我们要将它变成大顶堆数组,这里之所以要给出完全二叉树来,是因为这样便于理解哈
- 我们是直接操作数组使其变成大顶堆,而不是树,但是原理是一样的,因为我们操作数组的时候就是用到上面说的完全二叉树的性质(下标索引找孩子节点)
- 这里用树来演示,就是为了便于理解,真正操作是对数组直接操作。
再放两个图:
仔细品,不懂可以留言交流呀!图丑也欢迎留言喷哈哈哈
调整好大顶堆后,根(arry[0])节点值跟倒二节点(arry[5])值又互换,看图
五、撸代码(详细注释,不怕你看不懂)
纸上得来终且浅,得知此时要躬行,看了再多还是得撸代码
如果你看到这里了,要个赞不过分吧,哈哈哈哈
代码:
package suanFa;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
/**
* 堆排序的学习:
* 时间复杂度:O(nlogn),远比冒泡,简单选择,直接插入的O(n^2)好很多,但是它是个不稳定的算法
* @公众号 放牛娃学编程
*
*/
public class HeapSort {
public static void main(String[] args) {
// int[] arry = {20};
// //测试一波
// Heapsort(arry);
// System.out.println("数组已经排序完了:"+Arrays.toString(arry));
//性能测试一波,8千万数据排序
int[] arry = new int[80000000];
for(int i = 0; i < 80000000; i++)
{
arry[i] = (int) (Math.random()*80000000);
}
Date start = new Date();
Heapsort(arry);
Date end = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String startTime = simpleDateFormat.format(start);
String endTime = simpleDateFormat.format(end);
System.out.println("排序前的时间:"+startTime);
System.out.println("排序后的时间:"+endTime);
}
//构建大顶堆
public static void Heapsort(int[] arry)
{
//构建一棵大顶堆,策略:依次处理非叶子节点,从右往左,从上往下
for(int i = arry.length / 2 - 1; i >= 0; i--)
{
HeapAdjust(arry, i, arry.length);
}
//开始排序(取大顶堆的根节点,即使最大值,跟数组最后一个调换位置,然后将剩余的数重新调整为大顶堆)
for(int j = arry.length - 1; j > 0 ; j--)
{
//交换位置
swap(arry,0, j);
//将剩余的节点(这里是数组)调整为大顶堆
HeapAdjust(arry, 0, j);
}
}
private static void swap(int[] arry, int i, int j) {
// TODO Auto-generated method stub
int temp = arry[i];
arry[i] = arry[j];
arry[j] = temp;
}
//调整堆
/**
*
* @param arry 需要调整的数组
* @param index 非叶子节点下标索引
* @param length 需要调整的数组长度
*/
public static void HeapAdjust(int[] arry, int index, int length)
{
//1.0 用一个临时变量将该节点值暂时保存起来
int temp = arry[index];
int k;
//下标为什么要这样变化,根据完全二叉树的父节点与子节点的关系得出来的(编号从0开始编号)
for(k=2*index + 1; k < length; k = 2*k+1)
{
if(k+1 < length && arry[k] < arry[k+1])
{
//指向右子节点(较大的下标)
++k;
}
/**如果父节点已经是大于等于孩子节点(那直接退出循环,为什么可以这样呢?
因为我们采取的策略是:处理非叶子节点时,从右往左,从上往下。假设非叶子节点编号为:4,3,2,1)
**/
if(temp >= arry[k])
{
break;
}
//替换
arry[index] = arry[k];
index = k;
}
//插入
arry[index] = temp;
}
}
六、 八千万数据测试堆排序性能
上图程序中,我用8千万的数据进行堆排序,我的电脑用时:
- 可能我的电脑配置比较差劲,但是比冒泡、简单排序好太多了,不信你自己玩玩,我这破电脑就不试了哈
- 欢迎留言看看你的运行时间是多少,看是不是吊打我电脑呢,哈哈哈哈哈
- 看在我熬夜肝图的份上就点个赞吧,图不好也可以喷哈哈哈
都已经看到这了,再不给赞,那就说不过去了吧
七、分享交流
最后有兴趣一起交流的,可以关注我的公众号:这里你能够学到很实用的技巧,不是常用的我不说,公众号回复提取码即可获取以下学习资料啦啦啦啦,喜欢就拿去吧!!
(链接时常会失效,若出现此类情况,可以加我微信:17722328325(加时请备注:学习资料))
-
Java web从入门到精通电子书
-
Python机器学习电子书
-
Python400集(北京尚学堂)
-
JavaScript项目案例、经典面试题
-
Java300集(入门、精通)
-
Java后端培训机构录集(同事培训内部提供)
-
java重要知识pdf文档(价值连城呀呀,不收藏你会后悔的)
额外一堆电子书:
喜欢就关注吧,点个赞吧
这是分享生活、电影、资料、书籍的一个公众号,有需要的也可以看看哟!