什么是堆
堆分为大顶堆和小顶堆,堆需要满足以下条件:
- 完全二叉树
- 父节点>子节点(大顶堆)
- 父子点<子节点(小顶堆)
例如下图:
父节点比它的子节点都要大的完全二叉树就是大顶堆。(左右孩子的大小就没有顺序)
相反,父节点比字节点都小的完全二叉树是小顶堆(左右孩子的大小就没有顺序)
堆排序思路
了解完什么是堆之后,我们了解下如何采取堆这样的结构进行排序。
首先堆结构我们可以采取数组的形式存储。我们从根节点开始从左往右标号的话,对应数组下标有这样的规律:
对于节点i
父节点:(i-1)/2
左孩子:2i+1
右孩子2i+2
这样我们就可以将一个堆存储在数组里面了,然后利用堆的特性进行排序。
以大顶堆为例,根节点是整个堆里面值最大的,我们将最大值从堆里面移除,同时将最后一个节点放在根节点上面。此时堆变成这样:
这时的“堆”就不符合父节点>子节点了,所以需要对其进行重新构建:
此时的堆就符合条件了,根节点又是最大值了,我们重复上面的步骤,将根节点移除,最后一个节点放在根节点上:
然后一直重复以上操作,最后就可以得到一个排序好的数组了。
代码实现
我们捋一下堆排序的步骤:
- 构建堆
- 移除根节点,将最后一个节点放在根节点上
- 重构堆
其中步骤2,3一直重复,直至堆的元素都移除完
重构堆
因为堆的构建也是基于堆重构的,所以我们先了解堆怎么重构
假设有怎么一个堆需要重建,
我们发现被圈出来的部分不满足父节点最大的条件,所以需要将父节点和左右节点比较,选出最大的值和根节点交换。
交换完成之后,依旧存在不符合条件的,继续交换
这样就完成了一次重构,这就是一个完整的堆结构了。
代码实现
/**
* 重构堆
* @param tree
* @param n 堆的大小
* @param i 对节点i进行重构
*/
private void heapify(int[] tree, int n, int i) {
if (i >= n) {
return;
}
int c1 = 2 * i + 1;//左孩子
int c2 = 2 * i + 2;//右孩子
//找到最大值,放在父节点
int max = i;
if (c1 < n && tree[c1] > tree[max])
max = c1;
if (c2 < n && tree[c2] > tree[max])
max = c2;
//交换
if (max != i) {
swap(tree, i, max);
//注意这里需要继续往下递归,因为将i和max交换了,此时的i可能会比它的子节点要小,所以需要继续往下递归检查
heapify(tree, n, max);
}
}
//交换元素
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
建立堆
从上面我们可以得知如何重构一个堆了,那么如何将初始的数组构建成一个堆呢
我们是从k-1层往上开始构建堆的。例如初始数组是{2,1,5,3,4,10},按照与堆对应的位置,此时结构应该为:
我们找到最后一个节点的父节点对其进行重建
完成之后,再对下标为1进行重构,以此类推
这样就将初始数组构建成一个堆结构了。
代码实现
/**
* 建立堆
* @param tree
* @param n tree大小
*/
private void buildHeap(int[] tree, int n) {
int lastNode = n - 1; //最后一个节点
int parentNode = (lastNode - 1) / 2; //其父节点
for (int i = parentNode; i >= 0; i--) {
heapify(tree, n, i);//对i节点进行重构
}
}
排序
前面已经了解了堆重构和堆构建,下面就是我们需要的堆排序了。
排序的理论和步骤上面讲过了,这里就直接给代码了
代码实现
public void sort(int[] arr) {
//1.构建堆
buildHeap(arr, arr.length);
for (int i = arr.length - 1; i >= 0; i--) {
//将根节点与最后一个节点交换,实现将最后一个节点放在根节点上
swap(arr, i, 0);
//重构堆,注意此时我们已经将最大值放在数组的后面了,我们重构堆的时候就不带上它了(相当于移除根节点),所以第二个参数用的是i而不是arr的长度
heapify(arr, i, 0);
}
}