堆树
1 简介
1.1 什么是堆树
定义:堆树是一种特殊的完全二叉树,其中每个节点的值都遵循一定的堆属性。具体来说,堆分为最大堆和最小堆。
- 最大堆:在最大堆中,每个父节点的值都大于或等于其任何子节点的值。这意味着根节点是树中的最大值。
- 最小堆:在最小堆中,每个父节点的值都小于或等于其任何子节点的值。这意味着根节点是树中的最小值。
完全二叉树:除了最后一层外,每一层都被完全填满,且所有节点都尽可能地向左对齐。这种结构使得堆可以用数组来高效地表示和实现。
1.2 如何存储
数组表示:堆通常使用数组来表示,其中根节点位于数组的第一个位置(索引为0或1,取决于具体实现)。对于数组中的任何节点i(假设根节点位于索引1),其左子节点的索引为2i,右子节点的索引为2i+1,父节点的索引则为i/2(整数除法)。
1.3 下标计算
- 父节点的下标
对于任意节点,其下标为i
(注意,这里的下标可以从0开始,也可以从1开始,具体取决于实现方式),其父节点的下标可以通过以下公式计算:
- 如果数组下标从0开始:父节点下标 =
(i - 1) / 2
(整数除法) - 如果数组下标从1开始:父节点下标 =
i / 2
(整数除法)
例如,如果节点下标为5(数组下标从0开始),则其父节点下标为(5 - 1) / 2 = 2
。
- 左子节点的下标
对于任意节点,其下标为i
,左子节点的下标可以通过以下公式计算:
- 如果数组下标从0开始:左子节点下标 =
2 * i + 1
- 如果数组下标从1开始:左子节点下标 =
2 * i
例如,如果节点下标为2(数组下标从0开始),则左子节点下标为2 * 2 + 1 = 5
。
- 右子节点的下标
类似地,对于任意节点,其下标为i
,右子节点的下标可以通过以下公式计算:
- 如果数组下标从0开始:右子节点下标 =
2 * i + 2
- 如果数组下标从1开始:右子节点下标 =
2 * i + 1
例如,如果节点下标为2(数组下标从0开始),则右子节点下标为2 * 2 + 2 = 6
。
2 大顶堆
2.1 堆化图解
2.1.1 基础步骤
- 确定堆化的起始点
- 在从下往上堆化的过程中,堆化的起始点通常是最后一个非叶子节点。对于一个包含n个元素的数组表示的大顶堆,其最后一个非叶子节点的索引是
(n/2) - 1
(这里的除法是整数除法,即向下取整)。
- 在从下往上堆化的过程中,堆化的起始点通常是最后一个非叶子节点。对于一个包含n个元素的数组表示的大顶堆,其最后一个非叶子节点的索引是
- 进行堆化操作
- 从最后一个非叶子节点开始,向上依次对每个节点进行堆化。
- 对于每个节点,比较它与它的子节点(如果有的话)的值。在大顶堆中,父节点的值应该大于或等于其子节点的值。
- 如果父节点的值小于任何一个子节点的值,那么需要将父节点与较大的子节点交换。
- 交换后,可能会破坏下一层的堆性质,因此需要对交换后的子节点继续进行堆化操作,直到堆性质被完全恢复。
- 重复堆化
- 对每个非叶子节点重复上述堆化过程,直到到达根节点。
- 堆化过程是自下而上的,因为每次堆化都是从一个非叶子节点开始,向上调整堆的性质。
2.1.2 示例图解说明
假设有一个数组arr [8 4 20 7 3 1 25 14 17]
,我们想要将其调整为大顶堆。
- 确定堆化的起始点
- 数组有9个元素,最后一个非叶子节点的索引是
(9/2) - 1 = 3
。
- 数组有9个元素,最后一个非叶子节点的索引是
- 需要进行堆化的下标集合:[0, 1, 2, 3]
- 说明我们需要按顺序对 7,20,4,8这四个元素进行堆化
- 进行堆化操作
- 从索引3开始,即元素【arr[3] = 7】,向上进行堆化。
- 比较【arr[3] = 7】与其子节点【arr[7] = 14】和【arr[8] =17】,发现【arr[3] = 7】小于【arr[7] = 14】和【arr[8] =17】,且【arr[7] = 14】小于【arr[8] =17】,将【arr[8] =17】与【arr[3] = 7】交换。
- 交换后,新的堆变为
[8 4 20 17 3 1 25 14 7]
,此时交换后的7已经是叶子节点,不需要进一步堆化。 - 继续对索引2的节点(即元素
1
)进行堆化 - 重复上述过程,直到根节点被堆化。
2.2 实现
package cn.zxc.demo.leetcode_demo.advanced_data_structure.tree_heap;
import java.util.LinkedList;
import java.util.Queue;
/**
* 大顶堆
* 堆树使用的是完全二叉树实现,所以可以使用数组存储数据
* 完全二叉树的特性:
* 左子树下标 = 父节点下标 * 2 + 1
* 右子树下标 = 父节点下标 * 2 + 2
* 父节点下标 = (子节点下标 - 1) / 2
*/
public class LargeTopHead {
// 数组存储数据
private int[] arr;
// 堆的大小
private int size;
public LargeTopHead(int capacity) {
arr = new int[capacity + 1];
size = -1; // 索引从0开始
}
public void add(int value){
if (size + 1 > arr.length){
throw new RuntimeException("堆已满");
}
arr[++size] = value;
// 进行堆化
for (int i = (size - 1) / 2; i >= 0; i--) {
maxHeap(arr, i, size);
}
}
public void maxHeap(int[] arr, int start, int end){
int parent = start;
int son = 2 * parent + 1;
while (son <= end){
int temp = son;
if (son + 1 <= end && arr[son] < arr[son + 1]){
temp = son + 1;
}
if (arr[parent] < arr[temp]){
int temp1 = arr[parent];
arr[parent] = arr[temp];
arr[temp] = temp1;
parent = temp;
son = parent * 2 + 1;
continue;
}
// 因为我们堆化是从下到上,所以如果父节点大于子节点,因为子节点已经完成堆化,那么就不用交换了
return;
}
}
public void printCompleteBinaryTree() {
if (this.arr == null || size == -1) return;
Queue<Integer> queue = new LinkedList<>();
// 初始化,将根节点加入队列
queue.offer(0);
int height = (int) Math.ceil(Math.log(size + 1) / Math.log(2)) * 2;
while (!queue.isEmpty()) {
int levelSize = queue.size(); // 当前层的节点数
StringBuffer str = new StringBuffer();
for (int i = 0; i < height; i++) {
str.append(" ");
}
for (int i = 0; i < levelSize; i++) {
int currentNode = queue.poll(); // 从队列中取出一个节点
System.out.print(str.toString() + arr[currentNode] + " "); // 打印节点值
// 如果存在左子节点,则将其加入队列
if (2 * currentNode + 1 < size) {
queue.offer(2 * currentNode + 1);
}
// 如果存在右子节点,则将其加入队列
if (2 * currentNode + 2 < size) {
queue.offer(2 * currentNode + 2);
}
}
height/=2;
// 当前层打印完毕,换行以便打印下一层
System.out.println();
}
}
}
3 堆排序
3.1 图解
3.1.1 基础步骤
- 建堆(Heapify):对原始数组进行堆化,创建大顶堆或小顶堆【在建堆完成后,堆顶(即数组的第一个元素)就是当前的最大值。】
- 交换堆顶与末尾元素:将堆顶元素(即当前最大值)与数组的最后一个元素交换。这样,最大值就被放到了数组的最后,而堆的末尾则变成了一个待排序的“无效”元素。
- 调整剩余元素为堆:将剩余的n-1个元素重新调整为大顶堆。这个过程是通过堆化操作来实现的,但此时堆化的范围已经缩小了(因为末尾元素已经是一个“无效”元素,不再参与堆的调整)。
- 重复交换与调整:重复上述的交换和调整过程,每次都将新的堆顶元素与当前堆的末尾元素交换,并重新调整剩余的元素为大顶堆。随着过程的进行,堆的大小逐渐减小,而数组的有序部分则逐渐增大。
- 排序完成:当堆的大小减为1时,排序过程结束。此时,整个数组已经变成了一个有序数组。
3.1.2 示例图解说明
对2.1.2示例中已完成的大顶堆进行堆排序
堆化后的数组:[25, 17, 20, 14, 3, 1 8, 4, 7]
- 将arr[0] = 25与arr[8] = 7进行交换
- 对arr[0]节点进行堆化(注意已交换过的节点(即最后一个节点)不参与对话)
- 重复1~2步骤,知道未交换过的节点数为1
- 排序完成
3.2 实现
package cn.zxc.demo.leetcode_demo.advanced_data_structure.tree_heap;
import java.util.Arrays;
/**
* 堆排序
* 思想:
* 1.将数组构建成大顶堆
* 2.将堆顶元素和最后一个元素交换
* 3.将已经交换的元素之外的元素重新构建成大顶堆
* 4.重复2-3步骤直到可交换元素数组为1
* 时间复杂度:O(nlogn)
* 核心思想:每一次堆化完成之后,得到堆顶是当前最大的元素,我们将当前最大的元素拿出放在堆尾部,然后重新堆化,重复这个过程,直到数组为空
*/
public class HeadSortDemo {
public static void main(String[] args) {
int[] arr = {1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
HeadSortDemo headSortDemo = new HeadSortDemo();
for (int i = (arr.length - 1) / 2; i >= 0; i--) {
headSortDemo.maxHeap(arr, i, arr.length - 1);
}
// headSortDemo.headSort(arr);
System.out.println(Arrays.toString(arr));
}
public void maxHeap(int[] arr, int start, int end) {
int parent = start;
int son = parent * 2 + 1; // 完全二叉树的特性:左子节点下标 = 父节点下标 * 2 + 1
while (son <= end){
int temp = son;
if (son + 1 <= end && arr[son] < arr[son + 1]){
temp = son + 1;
}
if (arr[parent] >= arr[temp]){
return;
}else{
int temp1 = arr[parent];
arr[parent] = arr[temp];
arr[temp] = temp1;
parent = temp;
son = parent * 2 + 1;
}
}
}
public void headSort(int[] arr){
// 从最后一个非叶子节点开始堆化,得到一个大顶堆,但是没有完全排序
// 说明:
// 1、最后一个叶子节点的下标 = arr.length
// 2、根据完全二叉树的特性:左子节点下标 = 父节点下标 * 2 + 1 -> 父节点下标 = (子节点下标 - 1) / 2
// 3、结论:最后一个非叶子节点的下标 = (数组长度 - 1) / 2
for (int i = (arr.length - 1) / 2; i >= 0; i--) {
maxHeap(arr, i, arr.length - 1);
}
for (int i = arr.length - 1; i >= 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
maxHeap(arr, 0, i - 1);
}
}
}