数据结构与算法-15高级数据结构_树论(堆树)

堆树

1 简介

1.1 什么是堆树

定义:堆树是一种特殊的完全二叉树,其中每个节点的值都遵循一定的堆属性。具体来说,堆分为最大堆和最小堆。

  • 最大堆:在最大堆中,每个父节点的值都大于或等于其任何子节点的值。这意味着根节点是树中的最大值。
  • 最小堆:在最小堆中,每个父节点的值都小于或等于其任何子节点的值。这意味着根节点是树中的最小值。

完全二叉树:除了最后一层外,每一层都被完全填满,且所有节点都尽可能地向左对齐。这种结构使得堆可以用数组来高效地表示和实现。

1.2 如何存储

数组表示 :堆通常使用数组来表示,其中根节点位于数组的第一个位置(索引为0或1,取决于具体实现)。对于数组中的任何节点i(假设根节点位于索引1),其左子节点的索引为2i,右子节点的索引为2i+1,父节点的索引则为i/2(整数除法)。

1.3 下标计算
  1. 父节点的下标

对于任意节点,其下标为i(注意,这里的下标可以从0开始,也可以从1开始,具体取决于实现方式),其父节点的下标可以通过以下公式计算:

  • 如果数组下标从0开始:父节点下标 = (i - 1) / 2(整数除法)
  • 如果数组下标从1开始:父节点下标 = i / 2(整数除法)

例如,如果节点下标为5(数组下标从0开始),则其父节点下标为(5 - 1) / 2 = 2

  1. 左子节点的下标

对于任意节点,其下标为i,左子节点的下标可以通过以下公式计算:

  • 如果数组下标从0开始:左子节点下标 = 2 * i + 1
  • 如果数组下标从1开始:左子节点下标 = 2 * i

例如,如果节点下标为2(数组下标从0开始),则左子节点下标为2 * 2 + 1 = 5

  1. 右子节点的下标

类似地,对于任意节点,其下标为i,右子节点的下标可以通过以下公式计算:

  • 如果数组下标从0开始:右子节点下标 = 2 * i + 2
  • 如果数组下标从1开始:右子节点下标 = 2 * i + 1

例如,如果节点下标为2(数组下标从0开始),则右子节点下标为2 * 2 + 2 = 6

2 大顶堆

2.1 堆化图解
2.1.1 基础步骤
  1. 确定堆化的起始点
    • 在从下往上堆化的过程中,堆化的起始点通常是最后一个非叶子节点。对于一个包含n个元素的数组表示的大顶堆,其最后一个非叶子节点的索引是(n/2) - 1(这里的除法是整数除法,即向下取整)。
  2. 进行堆化操作
    • 从最后一个非叶子节点开始,向上依次对每个节点进行堆化。
    • 对于每个节点,比较它与它的子节点(如果有的话)的值。在大顶堆中,父节点的值应该大于或等于其子节点的值。
    • 如果父节点的值小于任何一个子节点的值,那么需要将父节点与较大的子节点交换。
    • 交换后,可能会破坏下一层的堆性质,因此需要对交换后的子节点继续进行堆化操作,直到堆性质被完全恢复
  3. 重复堆化
    • 对每个非叶子节点重复上述堆化过程,直到到达根节点。
    • 堆化过程是自下而上的,因为每次堆化都是从一个非叶子节点开始,向上调整堆的性质。
2.1.2 示例图解说明

假设有一个数组arr [8 4 20 7 3 1 25 14 17],我们想要将其调整为大顶堆。

  1. 确定堆化的起始点
    • 数组有9个元素,最后一个非叶子节点的索引是(9/2) - 1 = 3
  2. 需要进行堆化的下标集合:[0, 1, 2, 3]
    • 说明我们需要按顺序对 7,20,4,8这四个元素进行堆化
  3. 进行堆化操作
    • 从索引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 实现
java 复制代码
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 基础步骤
  1. 建堆(Heapify):对原始数组进行堆化,创建大顶堆或小顶堆【在建堆完成后,堆顶(即数组的第一个元素)就是当前的最大值。】
  2. 交换堆顶与末尾元素:将堆顶元素(即当前最大值)与数组的最后一个元素交换。这样,最大值就被放到了数组的最后,而堆的末尾则变成了一个待排序的"无效"元素。
  3. 调整剩余元素为堆:将剩余的n-1个元素重新调整为大顶堆。这个过程是通过堆化操作来实现的,但此时堆化的范围已经缩小了(因为末尾元素已经是一个"无效"元素,不再参与堆的调整)。
  4. 重复交换与调整:重复上述的交换和调整过程,每次都将新的堆顶元素与当前堆的末尾元素交换,并重新调整剩余的元素为大顶堆。随着过程的进行,堆的大小逐渐减小,而数组的有序部分则逐渐增大。
  5. 排序完成:当堆的大小减为1时,排序过程结束。此时,整个数组已经变成了一个有序数组。
3.1.2 示例图解说明

对2.1.2示例中已完成的大顶堆进行堆排序

堆化后的数组:[25, 17, 20, 14, 3, 1 8, 4, 7]

  1. 将arr[0] = 25与arr[8] = 7进行交换
  2. 对arr[0]节点进行堆化(注意已交换过的节点(即最后一个节点)不参与对话)
  3. 重复1~2步骤,知道未交换过的节点数为1
  4. 排序完成
3.2 实现
java 复制代码
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);
        }
    }
}
相关推荐
m0_571957581 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
pianmian13 小时前
python数据结构基础(7)
数据结构·算法
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟5 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity6 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天6 小时前
java的threadlocal为何内存泄漏
java
caridle6 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^6 小时前
数据库连接池的创建
java·开发语言·数据库