开门见山,本文讲述堆排序。html
就我自身对于排序的了解来看,其实堆排序是诸多排序中最难写的,光是理解起来都有点费劲,本文旨在于用通俗易懂的话,把堆排序娓娓道来。java
下面,开始!数组
1:堆spa
毫无疑问,排序两个字不必去死磕,这里的重点,在于排序的方式,堆排序,就是以堆的形式去排序,毫无疑问,了解堆很重要。code
那么,什么是堆呢?htm
这里,必须引入一个彻底二叉树的概念,而后过渡到堆的概念。blog
上图,就是一个彻底二叉树,其特色在于:排序
那么,彻底二叉树与堆有什么关系呢?索引
咱们假设有一棵彻底二叉树,在知足做为彻底二叉树的基础上,对于任意一个拥有父节点的子节点,其数值均不小于父节点的值;这样层层递推,就是根节点的值最小,这样的树,称为小根堆。ip
同理,又有一棵彻底二叉树,对于任意一个子节点来讲,均不大于其父节点的值,如此递推,就是根节点的值是最大的,这样的数,称为大根堆。
如上图,左边就是大根堆;右边则是小根堆,这里必需要注意一点,只要求子节点与父节点的关系,两个节点的大小关系与其左右位置没有任何关系。
明确下大根堆,小根堆的概念,继续说堆排序。
如今对于堆排序来讲,咱们先要作的是,把待排序的一堆无序的数,整理成一个大根堆,或者小根堆,下面讨论以大根堆为例子。
给定一个列表array=[16,7,3,20,17,8],对其进行堆排序(使用大根堆)。
接下来内容是转载部分,本身绘图功底太差:其中绿色部分为本身的注解。
步骤一 构造初始堆。将给定无序序列构形成一个大顶堆(通常升序采用大顶堆,降序采用小顶堆)。
a.假设给定无序序列结构以下
2.此时咱们从最后一个非叶子结点开始(叶结点天然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
此处必须注意,咱们把6和9比较交换以后,必须考量9这个节点对于其子节点会不会产生任何影响?由于其是叶子节点,因此不加考虑;可是,必定要熟练这种思惟,写代码的时候就比较容易理解为何会出现一次很是重要的交换了。
4.找到第二个非叶节点4,因为[4,9,8]中9元素最大,4和9交换。
在真正代码的实现中,这时候4和9交换事后,必须考虑9所在的这个节点位置,由于其上的值变了,必须判断对其的两个子节点是否形成了影响,这么说不合适,实际上就是判断其做为根节点的那棵子树,是否还知足大根堆的原则,每一次交换,都必需要循环把子树部分判别清楚。
这时,交换致使了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
牢记上面说的规则,每次交换都要把改变了的那个节点所在的树从新断定一下,这里就用上了,4和9交换了,变更了的那棵子树就必须从新调整,一直调整到符合大根堆的规则为截。
此时,咱们就将一个无序序列构形成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。而后继续调整堆,再将堆顶元素与末尾元素交换,获得第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
这里,必须说明一下,所谓的交换,实际上就是把最大值从树里面拿掉了,剩下参与到排序的树,其实只有总结点的个数减去拿掉的节点个数了。因此图中用的是虚线。
b.从新调整结构,使其继续知足堆定义
c.再将堆顶元素8与末尾元素5进行交换,获得第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
下面,附上个人代码,也是从文末连接中模仿过来的,可是亲自敲过一遍,印象深入。
public class HeapSort { public static void main(String[] args) { int[] array = new int[] { 2, 1, 4, 3, 6, 5, 8, 7 }; // 接下来就是排序的主体逻辑 sort(array); System.out.println(Arrays.toString(array)); } /** * * @description 本方法只有一个参数,那就是待排序的array * @author * @param * @return * @time 2018年3月9日 下午2:24:45 */ public static void sort(int[] array) { // 按照彻底二叉树的特色,从最后一个非叶子节点开始,对于整棵树进行大根堆的调整 // 也就是说,是按照自下而上,每一层都是自右向左来进行调整的 // 注意,这里元素的索引是从0开始的 // 另外一件须要注意的事情,这里的建堆,是用堆调整的方式来作的 // 堆调整的逻辑在建堆和后续排序过程当中复用的 for (int i = array.length / 2 - 1; i >= 0; i--) { adjustHeap(array, i, array.length); } // 上述逻辑,建堆结束 // 下面,开始排序逻辑 for (int j = array.length - 1; j > 0; j--) { // 元素交换 // 说是交换,其实质就是把大顶堆的根元素,放到数组的最后;换句话说,就是每一次的堆调整以后,都会有一个元素到达本身的最终位置 swap(array, 0, j); // 元素交换以后,毫无疑问,最后一个元素无需再考虑排序问题了。 // 接下来咱们须要排序的,就是已经去掉了部分元素的堆了,这也是为何此方法放在循环里的缘由 // 而这里,实质上是自上而下,自左向右进行调整的 adjustHeap(array, 0, j); } } /** * * @description 这里,是整个堆排序最关键的地方,正是由于把这个方法抽取出来,才更好理解了堆排序的精髓,会尽量仔细讲解 * @author * @param * @return * @time 2018年3月9日 下午2:54:38 */ public static void adjustHeap(int[] array, int i, int length) { // 先把当前元素取出来,由于当前元素可能要一直移动 int temp = array[i]; // 能够参照sort中的调用逻辑,在堆建成,且完成第一次交换以后,实质上i=0;也就是说,是从根所在的最小子树开始调整的 // 接下来的讲解,都是按照i的初始值为0来说述的 // 这一段很好理解,若是i=0;则k=1;k+1=2 // 实质上,就是根节点和其左右子节点记性比较,让k指向这个不超过三个节点的子树中最大的值 // 这里,必需要说下为何k值是跳跃性的。 // 首先,举个例子,若是a[0] > a[1]&&a[0]>a[2],说明0,1,2这棵树不须要调整,那么,下一步该到哪一个节点了呢?确定是a[1]所在的子树了, // 也就是说,是以本节点的左子节点为根的那棵小的子树 // 而若是a[0}<a[2]呢,那就调整a[0]和a[2]的位置,而后继续调整以a[2]为根节点的那棵子树,并且确定是从左子树开始调整的 // 因此,这里面的用意就在于,自上而下,自左向右一点点调整整棵树的部分,直到每一颗小子树都知足大根堆的规律为止 for (int k = 2 * i + 1; k < length; k = 2 * k + 1) { // 让k先指向子节点中最大的节点 if (k + 1 < length && array[k] < array[k + 1]) { k++; } // 若是发现子节点更大,则进行值的交换 if (array[k] > temp) { swap(array, i, k); // 下面就是很是关键的一步了 // 若是子节点更换了,那么,以子节点为根的子树会不会受到影响呢? // 因此,循环对子节点所在的树继续进行判断 i = k; // 若是不用交换,那么,就直接终止循环了 } else { break; } } } /** * 交换元素 * * @param arr * @param a * 元素的下标 * @param b * 元素的下标 */ public static void swap(int[] arr, int a, int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } }