二叉堆
二叉堆是一种特殊的树,有两种特性:
- 二叉堆是一个完全二叉树
- 二叉堆中任意一个父节点的值都大于等于(或小于等于)其左右孩子节点的值。
根据第二条特性,可以将二叉堆分为两类:
-
最大堆:父节点的值总是大于或等于左右孩子节点的值。
-
最小堆:父节点的值总是小于或等于左右孩子节点的值。
二叉堆的实现
1.往堆中插入一个节点
由于二叉堆具有完全二叉树的特性,所以我们插入节点时,应该保证它任然是一个完全二叉树。所以,在插入的时候,我们把新节点插入二叉堆的最后一个位置。例如,在下图中插入7。
然后在对二叉堆进行调整,使其满足任意一个父节点都大于等于它的左右孩子节点。对此,我们可以对新插入的节点进行上浮操作,即和父节点交换位置。
7和2相比,7大于2,上浮。
7和5比,7大于5,上浮。
7与8比,7小于8,插入完成。
2.删除节点
对于二叉堆,删除节点,我们一般是删除根节点。和插入操作一样,由于要保证二叉堆完全二叉树的特性,在删除根节点后,将二叉堆的最后一个元素替换到根节点上。
把8删除后,将2替换上来。
由于要保证最大堆任意父节点都大于等于其左右孩子节点的值,对根节点进行下沉操作。
首先比较根节点的左右孩子节点的值,7比6大,在将2与7比较,2小于7,下沉。
比较左右孩子节点的值,4大于2,父节点2的值小于4,2与4交换。
节点2已经不存在左右孩子节点,删除操作结束。
3.构建二叉堆
二叉树的构建一般是基于链表实现,但是二叉堆是采用数组的方式来存储。
这个二叉堆对应的数组是:
所以如果知道一个节点的位置,就能推导出其左右孩子节点的位置。假如一个节点的下标是n,则其左孩子节点的下标为:2n+1,右孩子节点的下标为:2n+2。
最大堆的代码实现:
public class BinaryHeap {
/**
* 上浮操作,对插入的节点进行上浮
*
* @param arr
* @param length 数组的长度
*/
public static void upAdjust(int[] arr, int length) {
// 标记插入的节点
int child = length - 1;
// 插入节点的父节点
int parent = (child - 1) / 2;
// 保存插入节点的值
int temp = arr[child];
// 进行上浮
while (child > 0 && temp < arr[parent]) {
// 单向赋值
arr[child] = arr[parent];
// 孩子节点和父节点向上移
child = parent;
parent = (child - 1) / 2;
}
// 循环结束,child的下标即时要插入的位置
arr[child] = temp;
}
/**
* 下沉操作
*
* @param arr
* @param parent 要下沉元素的下标
* @param length 数组的长度
*/
public static void downAdjust(int[] arr, int parent, int length) {
// 存储下沉元素的值
int temp = arr[parent];
// 下沉节点的孩子节点
int child = parent * 2 + 1;
while (child < length) {
// 如果右孩子节点比左孩子节点大,定位到右孩子节点
if (child + 1 < length && arr[child] < arr[child + 1]) child++;
// 如果父节点大于等于孩子节点,退出循环
if (temp >= arr[child]) break;
// 单向赋值
arr[parent] = arr[child];
// 父节点和孩子节点向下移
parent = child;
child = parent * 2 + 1;
}
// 退出循环表示找到正确的位置
arr[parent] = temp;
}
// 构建二叉堆
public static void buildHead(int[] arr) {
// 从最后一个非叶子节点开始下沉
for (int i = (length - 2) / 2; i >= 0; i--) {
downAdjust(arr, i, arr.length);
}
}
}
堆排序
堆排序是基于二叉堆实现的,要实现堆排序,需要先构造二叉堆。在大小为n的二叉堆构建完成后,将堆顶的元素与最后一个元素交换,将剩下的n-1个元素看成一个新的二叉堆,然后在对根节点进行下沉操作,重复此过程,即能完成堆排序。图解如下:
将堆顶元素9和最后一个元素2交换。
对2进行下沉操作。
将7与1进行交换,对1进行下沉操作。
将6与2进行交换,对2进行下沉操作。下沉操作时,若左右孩子节点相同时,选取左右孩子节点交换都可以。
将5与3进行交换,对3进行下沉操作。
以此类推…
这样,堆排序就完成了。
代码实现如下:
import java.util.Arrays;
public class HeapSort{
/**
* 下沉操作
*
* @param arr
* @param parent 要下沉元素的下标
* @param length 数组的长度
*/
public static void downAdjust(int[] arr, int parent, int length) {
// 保存要下沉元素的值
int temp = arr[parent];
// 定位左孩子节点的位置
int child = parent * 2 + 1;
while (child < length) {
// 如果右孩子节点比左孩子节点大,则定位到右孩子节点
if (child + 1 < length && arr[child] < arr[child + 1]) child++;
// 如果父节点的值大于等于孩子节点的值,结束下沉操作
if (temp >= arr[child]) break;
// 单向赋值
arr[parent] = arr[child];
// 将父节点和孩子节点向下移
parent = child;
child = parent * 2 + 1;
}
// 循环结束表示定位到了正确位置
arr[parent] = temp;
}
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) return;
// 构建二叉堆,从最后一个非叶子节点开始下沉
for (int i = (arr.length - 2) / 2; i >= 0; i--) {
downAdjust(arr, i, arr.length);
}
// 进行堆排序
for (int i = arr.length - 1; i > 0; i--) {
// 将堆顶的元素和最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 下沉操作,每次数组的长度减一
downAdjust(arr, 0, i);
}
}
// 测试类
public static void main(String[] args) {
int[] a = new int[10];
for (int i = 0; i < a.length; i++) {
a[i] = (int) (Math.random() * 20);
}
System.out.println("排序前:" + Arrays.toString(a));
heapSort(a);
System.out.println("排序后:" + Arrays.toString(a));
}
}
运行结果
排序前:[15, 19, 14, 8, 11, 4, 17, 16, 7, 19]
排序后:[4, 7, 8, 11, 14, 15, 16, 17, 19, 19]
性质:①时间复杂度分析:在建堆的过程中,时间复杂度是O(n),排序过程中的时间复杂度是O(nlogn),所以,堆排序的整体时间复杂度是O(nlogn)。
②空间复杂度:O(1) ③原地排序 ④非稳定排序