数据结构之堆(Java\Python双语实现)

一.堆(heap)的概念

在Computer Science中,堆(Heap)是一个重要的概念,主要有两层含义

1.数据结构中的堆

1.是一种特殊的二叉树

堆是一种特殊的树形结构,通常是一棵完全二叉树(满二叉树的一种特例,满二叉树是每个节点要么有2个子节点要么就没有子节点,而完全二叉树是除了最后一层,其他层都是满的,即其他层每个节点的父节点都有两个孩子,并且完全二叉树的最后一层的节点要靠左排列)

类型:

类型 特性
最大堆(Max Heap) 每个节点的值都大于或等于其子节点的值,根节点是最大值
最小堆(Min Heap) 每个节点的值都小于或等于其子节点的值,根节点是最小值

最大堆:

复制代码
          [90]  <-- 最大值在根部
         /    \
      [50]    [80]
      /  \    /  \
   [30] [40] [70] [60]

最小堆:

复制代码
          [30]  <-- 最小值在根部
         /    \
      [40]    [60]
      /  \    /  \
   [50] [90] [70] [80]

2.堆的存储

堆在逻辑上是树形结构,但在计算机内存中实际使用数组存储,这种巧妙的映射是堆高效的关键

核心映射关系:

假设有一个最小堆,包含元素:[10, 20, 30, 40, 50, 60, 70]

逻辑视图(从索引0开始存储数据,也可以从1开始,但是找父子关系的时候规律要稍微变化):

复制代码
              索引: 0
               [10]
              /    \
    索引: 1  /      \  索引: 2
           [20]    [30]
           /  \    /  \
  索引: 3 /    \4 /5   \6
         [40] [50][60] [70]

物理视图(数组存储):

复制代码
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  0  │  1  │  2  │  3  │  4  │  5  │  6  │  ← 索引
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 10  │ 20  │ 30  │ 40  │ 50  │ 60  │ 70  │  ← 值
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

索引计算公式:

对于数组中任意索引i的节点:

复制代码
| 关系   |        公式     |      示例 (i=1)   |
| 父节点 | (i - 1) // 2    | (1-1)//2 = 0      | // 代表下取整
| 左孩子 | 2 * i + 1       | 2×1+1 = 3         |
| 右孩子 | 2 * i + 2       | 2×1+2 = 4         |

内存布局示意:

复制代码
内存地址(简化):  0x1000  0x1004  0x1008  0x100C  0x1010  0x1014  0x1018
                   ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐
                   │  10  │  20  │  30  │  40  │  50  │  60  │  70  │
                   └──────┴──────┴──────┴──────┴──────┴──────┴──────┘
                      ↑                                        ↑
                   堆顶                                     堆尾
                (最小值)                                 (最后元素)

连续存储:数组在内存中是连续的,这使得缓存友好,访问速度快

3.常见应用

  • 优先队列(Priority Queue)
  • 堆排序算法
  • 图算法中的最短路径(如Dijkstra算法)
  • 任务调度系统

2.内存管理中的堆

在程序内存管理中,堆是指动态内存分配区域:

特点:

特性 说明
分配方式 程序员手动分配和释放(如C/C++中的 malloc/freenew/delete
生命周期 由程序员控制,不会自动释放
访问速度 比栈慢,但更灵活
大小限制 通常比栈大得多

与栈的区别:

复制代码
┌─────────────┐
│    栈       │  ← 自动管理,存放局部变量
│   (Stack)   │
├─────────────┤
│    堆       │  ← 手动管理,存放动态分配的对象
│   (Heap)    │
└─────────────┘

3.两种不同概念的堆的总结

方面 数据结构堆 内存管理堆
本质 一种树形数据结构 内存分配区域
用途 优先队列、排序等 动态存储数据
管理 算法自动维护 程序员手动管理

二.数据结构的堆(Java实现)

堆的实现

java 复制代码
package algorithm.datastructure.heapdemo;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

/*public class Heap<T extends Comparable<T>>
* 这是Java泛型中的类型边界语法
* 从左到右:T是泛型类型参数, extends表示类型边界(上界), Comparable<T>表示T可以和自己比较
* 这行代码的含义是:T必须是实现了Comparable<T>接口的类型
* 问题1: 什么是Comparable接口
*       Comparable接口是Java用于对象比较的标准接口
*       在IDEA中鼠标移到下面的Comparable上,快捷键Ctrl+B(B代表Base)(或者Ctrl+鼠标左键单击)查看Comparable接口的声明
*       public interface Comparable<T> {
*           int compareTo(T other); // 这个方法返回int,表示比较结果
*       }
*       compareTo方法返回值是一个整数,通过正负零表示大小关系
*       返回负数,表示当前对象小于其他对象
*       返回0,表示当前对象等于其他对象
*       返回正数,表示当前对象大于其他对象
* 问题2: 什么是泛型边界
*       泛型边界是Java泛型中用于限制类型参数范围的机制,用于确保泛型类型的安全
*       没有边界,则T可以是任何类型,如:
*           class Box<T> {}
*       有边界,则T必须是Number的子类,如:
*           class Box<T extends Number>
        而extends就是实现泛型边界上界的关键字,还有泛型边界下届super(限制T必须是某类型的子类)、多重边界&(限制T必须同时满足多个条件)
* 问题3: 为什么要用extends而不用implements来实现Comparable接口
*        理解了问题2,就很容易理解问题3
*        在泛型中,无论是类还是接口,类型边界都有extends(用于上边界),这是Java的语法设计
*        泛型边界不能用implements
*        普通类:继承父类用extends、实现接口用implements
*        泛型类型边界:边界是类用extends、边界是接口也用extends(语法统一、语义清晰、基于历史的设计)
* 问题4: 接口可以继承吗
*        可以!接口之间用extends继承
* 问题5: 对问题3提出的问题的纠正
*        <T extends Comparable<T>>中:
*        T 表示Heap类的参数都是T类型,即Heap存储的元素都是T类型
*        而T extends Comparable<T>是泛型边界 表示T类型只能和同类型T进行比较*/
public class Heap<T extends Comparable<T>> {
     private List<T> heap;
    // List是Java集合框架中的核心接口,用于存储有序、可重复的元素集合
    // 这里用List而没有用具体的类,这是面向接口编程,可以灵活换成LinkedList类等其他实现,且不依赖与具体实现类
    private boolean maxHeap; // 堆类型标志,true表示最大堆,false表示最小堆

    public Heap(boolean maxHeap) {
        // 多态: List<T> heap = new ArrayList<>(); List是引用类型,ArrayList是heap的实际类型
        // 不能这样:List<T> heap = new List<>();
        // List是接口,而接口不能实例化,且接口没有构造函数,接口中的方法默认是抽象方法(接口的作用是定义实现该接口的类的规范)
        this.heap = new ArrayList<> (); // 这里是Java7及之后的写法,使用菱形操作符时可以省略泛型类型,编译器会根据左侧声明的类型自动判断右侧的泛型类型(这里判断为Integer)
        this.maxHeap = maxHeap;
    }

    private int parent(int i) { return (i - 1) / 2; }
    private int leftChild(int i) { return 2 * i + 1; }
    private int rightChild(int i) { return 2 * i + 2; }

    // 根据maxHeap标志选择比较方向
    private boolean compare(T a, T b) {
        return maxHeap ? a.compareTo(b) > 0 : a.compareTo(b) < 0;
        // 三元运算符
        // maxHeap为条件判断
        // a.compareTo(b) > 0为true时返回true,否则返回false

        /*三元运算符运用简化简单的if-else逻辑
        * 基本语法:条件表达式 ? 表达式1 : 表达式2
        * 说明: 条件表达式是布尔类型的条件,当条件为true时执行表达式1,条件为false时执行表达式2*/
    }

    private void swap(int i, int j) {
        T temp = heap.get(i);
        heap.set(i, heap.get(j));
        heap.set(j, temp);
    }

    // 向上调整堆,维护堆的性质
    // 当新元素插入到堆末尾时,可能破话堆的性质。这时候就需要将新元素"上浮"到正确位置
    // 从而确保堆序性(父节点与子节点的大小关系)不被破坏
    private void heapifyUp(int i) {
        // i是新插入元素的索引
        while (i > 0) { // 只要没到根节点(索引为0)
            int p = parent(i); // 获取父节点
            // 如果当前节点应该排在父节点前面,则需要交换
            if (compare(heap.get(i), heap.get(p))) {
                swap(i, p);
                i = p; // 更新索引,继续向上检查
            } else {
                break; // 堆序性以满足,停止调整
            }
        }
    }

    // 向下调整堆,维护堆的性质
    // 当堆顶元素被删除/替换后,可能破坏堆的性质,需要将新堆顶元素"下沉"到正确位置
    private void heapifyDown(int i) {
        int n = heap.size();
        while (true) { // 无限循环,直到break
            int target = i; // 记录当前"最优先"的节点
            int left = leftChild(i); // 左孩子索引
            int right = rightChild(i); // 右孩子索引
            // 如果左孩子存在且应该优先与当前节点
            if (left < n && compare(heap.get(left), heap.get(target))) {
                target = left; // 更新target为左孩子
            }
            // 如果右孩子存在其应该优先与当前节点
            if (right < n && compare(heap.get(right), heap.get(target))) {
                target = right; // 更新target为右孩子
            }
            // 如果target变了,则需要交换
            if (target != i) {
                swap(i, target); // 交换当前节点和更优先的孩子
                i = target; // 更新索引,继续向下检查
            } else {
                break; // 堆序性满足,停止调整
            }
        }
    }

    public void push(T val) {
        heap.add(val);
        heapifyUp(heap.size() - 1);
    }

    public void printAll() {
        System.out.println(heap);
    }

    // 弹出并返回堆顶元素
    // 删除堆顶元素之后,问题是如何保持堆的性质
    // 方案1:将后续元素全部前移,时间复杂度O(n),太慢了
    // 方案2:将最后一个元素放到堆顶,然后"下沉"到正确位置,时间复杂度O(log n)
    public T pop() {
        if (heap.isEmpty()) {
            throw new NoSuchElementException("堆为空");
        }
        T result =  heap.getFirst(); // 获取堆顶元素 等价于 T result = heap.get(0);
        heap.set(0, heap.get(heap.size() - 1)); // 用堆底元素替换堆顶元素
        heap.remove(heap.size() - 1); // 删除堆底元素 也可以用List的removeLast方法
        if (!heap.isEmpty()) {
            heapifyDown(0); // 如果堆不为空,向下调整
        }
        return result; // 返回堆顶元素
    }

    // 返回堆顶元素
    public T peek() {
        if (heap.isEmpty()) {
            throw new NoSuchElementException("堆为空");
        }
        return heap.get(0);
    }

    public int size() {
        return heap.size();
    }

    public boolean isEmpty() {
        return heap.isEmpty();
    }
}

assert断言

写一个测试类

java 复制代码
package algorithm.datastructure.heapdemo;

public class HeapTest {
    public static void main(String[] args) {
        testMinHeap();
    }

    static void testMinHeap() {
        // 最小堆
        Heap<Integer> minHeap = new Heap<>(false);
        assert minHeap.isEmpty() : "新堆应该为空";

    }
}

发现没有任何输出,这是因为Java是默认禁用assert断言功能的

要在IDE即集成开发环境(integrated development environment)中启用这个功能:

IDE 设置方法
IntelliJ IDEA Run → Edit Configurations → VM options → 添加 -ea
Eclipse Run → Run Configurations → Arguments → VM arguments → 添加 -ea
VS Code settings.json → 添加 "java.debug.settings.enableAssertions": true

其实还有一个小问题,就算启用了assert功能,运行后也什么都不会输出

这是因为当断言的条件为true时,程序会继续指向,不会抛出AssertionError

断言的语法为:

java 复制代码
assert 条件表达式 : 错误消息;
// 其中条件表达式的结果若为true,则断言通过,否则会抛出AssertionError错误
// 错误消息是可选的,它表示断言失败后会输出的消息
// 如:assert 条件表达式;
相关推荐
暴力袋鼠哥1 小时前
基于 SpringBoot + Vue3 的社区医院管理系统实战(含 AI 问诊 + 电子病历 PDF 导出
java·spring boot·intellij-idea·mybatis
马尔代夫哈哈哈1 小时前
Spring 事务处理
java·后端·spring
自然语1 小时前
人工智能之数字生命-观察的实现
数据结构·人工智能·学习·算法
苦藤新鸡1 小时前
63.排序数组中找元素的第一个元素和最后一个元素
算法·leetcode
苦藤新鸡1 小时前
59 分割回文串
算法
得一录1 小时前
LoRA(Low-Rank Adaptation)的原理和实现
python·算法·机器学习
Andy Dennis1 小时前
各种单例模式的实现方式
java·单例模式
逆境不可逃1 小时前
【从零入门23种设计模式02】创建型之单例模式(5种实现形式)
java·spring boot·后端·单例模式·设计模式·职场和发展
We་ct1 小时前
LeetCode 106. 从中序与后序遍历序列构造二叉树:题解+思路拆解
前端·数据结构·算法·leetcode·typescript