您的位置:首页 > 编程语言 > Java开发

堆排序

2016-03-24 20:41 603 查看
简单认识

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。

完全二叉树:除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点。



理解

如图所示,我们要排序的数据时存在数组中的,只是,我们像访问二叉树一样访问”节点“。因为完全二叉树的父节点与左右孩子节点的坐标是可以相互计算出来的。下面是按照从0开始计数的计算公式。从1开始计数的公式会有所不同,但也不难得到,画个简单的完全二叉树试一下就可以了。

计算当前节点的左孩子节点:
(current * 2) + 1


很明显右孩子节点:
(current * 2) + 2


计算当前节点的父节点:
(current - 1) / 2;


基本思想

堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。

(1)用大根堆排序的基本思想:

① 先将初始数组R[0..n]建成一个大根堆,此堆为初始的无序区

② 再将关键字最大的记录R[0](即堆顶)和无序区的最后一个记录R
交换,由此得到新的无序区R[0..n-1]和有序区R
,且满足R[0..n-1].keys≤R
.key

③由于交换后新的根R[0]可能违反堆性质,故应将当前无序区R[0..n-1]调整为堆。然后再次将R[0..n-1]中关键字最大的记录R[0]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[0..n-2]和有序区R[n-1..n],且仍满足关系R[0..n-2].keys≤R[n-1..n].keys,同样要将R[0..n-2]调整为堆。

……

直到无序区只有一个元素为止。

(2)大根堆排序算法的基本操作:

①建堆,建堆是不断调整堆的过程,从(len-1)/2处开始调整,一直到第一个节点,此处len是堆中元素的个数。建堆的过程是线性的过程,从(len-1)/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(h(len-1)/2) 其中h表示节点的深度,(len-1)/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。

②调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,那边交互节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,因为是沿着深度方向进行调整的。

③堆排序:堆排序是利用上面的两个过程来进行的。首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。堆排序过程的时间复杂度是O(nlgn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn)。

示例(来自堆排序原理及算法实现(最大堆)

给定一个整形数组a[]={16,7,3,20,17,8},对其进行堆排序。

首先根据该数组元素构建一个完全二叉树,得到


然后需要构造初始堆,则从最后一个非叶节点开始调整:







20和16交换后导致16不满足堆的性质,因此需重新调整

这样就得到了初始堆。

即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。有了初始堆之后就可以进行排序了。


此时3位于堆顶不满堆的性质,则需调整继续调整。




















这样整个区间便已经有序了。

从上述过程可知,堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1…n]中选择最大记录,需比较n-1次,然后从R[1…n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。

代码(来自百度百科:堆排序

public class HeapSort {
private static int[] sort = new int[] { 1, 0, 10, 20, 3, 5, 6, 4, 9, 8, 12,
17, 34, 11 };

public static void main(String[] args) {
// 建初始最大堆
buildMaxHeapify(sort);
// 最大堆排序
heapSort(sort);
// 打印结果
print(sort);
}

/**
*
* @param data建堆的数组
*/
private static void buildMaxHeapify(int[] data) {
// 没有子节点的才需要创建最大堆,从最后一个的父节点开始
int startIndex = getParentIndex(data.length - 1);
// 从尾端开始创建最大堆
for (int i = startIndex; i >= 0; i--) {
maxHeapify(data, data.length, i);
}
}

/**
* 创建最大堆
*
* @param data
* @param heapSize需要创建最大堆的大小
*            ,一般在sort的时候用到,因为最大值放在末尾,末尾就不再归入最大堆了
* @param index当前需要创建最大堆的位置
*            ,即父节点位置
*/
private static void maxHeapify(int[] data, int heapSize, int index) {
// 当前点与左右子节点比较
int left = getChildLeftIndex(index);
int right = getChildRightIndex(index);

int largest = index;
// >heapSize的节点是已经排好序的,不能再动了
if (left < heapSize && data[index] < data[left]) {
largest = left;
}
if (right < heapSize && data[largest] < data[right]) {
largest = right;
}
// 得到最大值后可能需要交换,如果交换了,其子节点可能就不是最大堆了,需要重新调整
if (largest != index) {
swap(data, index, largest);
// 不能忽略这一步
maxHeapify(data, heapSize, largest);
}
}

/**
* 排序,最大值放在末尾
*
* @param data
*/
private static void heapSort(int[] data) {
// 末尾与头交换,交换后调整最大堆
for (int i = data.length - 1; i > 0; i--) {
swap(data, 0, i);
// 由于节点0变动了,所以需要调整最大堆,注意这里的i是指需要调整的最大堆的大小,是从1开始计算的
maxHeapify(data, i, 0);
}
}

private static void swap(int[] data, int i, int j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}

/**
* 父节点位置
*
* @param current
* @return
*/
private static int getParentIndex(int current) {
// return (current - 1) / 2;
// 移位运算,等同于上面
return (current - 1) >> 1;
}

/**
* 左子节点position注意括号,加法优先级更高
*
* @param current
* @return
*/
private static int getChildLeftIndex(int current) {
// return (current * 2) + 1;
return (current << 1) + 1;
}

/**
* 右子节点position
*
* @param current
* @return
*/
private static int getChildRightIndex(int current) {
// return (current * 2) + 2;
return (current << 1) + 2;
}

private static void print(int[] data) {
int pre = -2;
for (int i = 0; i < data.length; i++) {
if (pre < (int) getLog(i + 1)) {
pre = (int) getLog(i + 1);
System.out.println();
}
System.out.print(data[i] + "|");
}
}

/**
* 计算以2为底的对数
*
* @param param
* @return
*/
private static double getLog(double param) {
return Math.log(param) / Math.log(2);
}
}


输出结果:

0|
1|3|
4|5|6|8|
9|10|11|12|17|20|34|
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息