本博客总结学习堆排序算法,以一个数组为例,采用大根堆进行升序排序,附有代码实现。算法
堆排序的逻辑是创建在彻底二叉树的基础上。数组
有两个概念必需要了解:函数
以大根堆为例,将根结点与最后一个结点交换,弹出根结点,便可获得整个树中的最大值。继续,将剩下的n-1个结点的树再调整为大根堆,再弹出根结点,以此类推,可获得一个有序序列。性能
问题的关键在于,如何进行堆调整?学习
咱们把二叉树中每一簇“父结点、左孩子、右孩子”当成一个三元组,从二叉树底层开始,由下往上,依次对每个三元组进行调整,套一两层循环,便可完成堆调整。这是直观的整体思路。优化
存在一个问题:如何根据父或子结点快速获取三元组?code
说白了就是须要创建父结点和孩子结点之间的联系。可经过彻底二叉树的性质来解决。彻底二叉树中,若按照层序遍历对每一个结点进行编号(从1开始),父节点为 k ,则左右孩子结点编号必定为 2 * k 和 2 * k + 1 。根据此性质可在父子结点之间快速互相访问。排序
把待排序的数组看作彻底二叉树层序遍历的结果,便可应用这个性质。以下图所示:
博客
先上代码:基础
private void heapSort(int[] arr) { int len = arr.length; //将乱序数组调整为大根堆 for (int i = len / 2 - 1; i > -1; --i) { heapAdjust(arr, i, len); } //元素出堆、循环堆调整 for (int i = len - 1; i > 0; --i) { //交换i和0两个元素,使用位运算完成 swap(arr, i, 0); //堆调整 heapAdjust(arr, 0, i); } //arr排序完毕 } /** * 交换数组中两个数,使用位运算 */ private void swap(int[] arr, int i, int j) { arr[i] ^= arr[j]; arr[j] ^= arr[i]; arr[i] ^= arr[j]; } /** * 堆调整 */ private void heapAdjustOld(int[] arr, int s, int length) { for (int i = 2 * s + 1; i < length; i = 2 * i + 1) { if (i + 1 < length && arr[i + 1] > arr[i]) { ++i; } if (arr[s] > arr[i]) break; swap(arr, s, i); s = i; } } /** * 堆调整优化方法 */ private void heapAdjust(int[] arr, int s, int length) { int temp = arr[s]; for (int j = 2 * s + 1; j < length; j = j * 2 + 1) { if (j + 1 < length && arr[j + 1] > arr[j]) { ++j; } if (temp > arr[j]) break; arr[s] = arr[j]; s = j; } arr[s] = temp; }
对于一个杂乱无章的数组而言,一层循环不足以将其调整为大根堆,须要两层。
交换根节点与最后一个结点,把最大值移到了数组的末尾。再对前 n-1 个数进行堆调整,再次将最大值移到末尾,依次循环,便可获得升序排序结果。
注意:此处的堆调整不须要第一步中的两层循环,只须要一层,调用heapAdjust便可。由于前 n-1 个数中,只有arr[0]这一个位置不正确,并非彻底乱序,只须要调整这一个位置便可。
堆调整是本算法中最核心的部分。即调整以 s 为根的三元组为正确的大根堆/小根堆,并对下层结点进行循环修正。
注意:此方法并不会遍历整颗二叉树,也不能将一棵杂乱的二叉树调整为大/小根堆
本部分代码很巧妙,须要细细品读。每次调整时,并非直接交换父结点值和子结点值,那样会徒增赋值次数。