Java实现大根堆与小根堆详解

定义

大根堆是一种特殊的完全二叉树,其中每个父节点的值都大于或等于其左右子节点的值。使用数组实现时,我们可以通过索引关系来表示节点间的父子关系:

  • 对于索引为 i 的节点,其父节点索引为 (i-1)/2
  • 左子节点索引为 2i+1
  • 右子节点索引为 2i+2

最大堆和最小堆的关键操作有2种:插入元素、获取堆顶元素

插入元素的思路

放到数组的末尾,执行上浮操作

获取堆顶元素的思路

移除堆顶元素后,把数组末尾的元素放在堆顶,执行下沉操作

最大堆java实现

java 复制代码
import java.util.Arrays;

public class MaxHeap {
    private int[] heap; // 存储堆元素的数组
    private int size;   // 当前堆中元素的数量
    private int capacity; // 堆的容量
    
    // 构造函数,初始化指定容量的堆
    public MaxHeap(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.heap = new int[capacity];
    }
    
    // 获取堆的大小
    public int size() {
        return size;
    }
    
    // 判断堆是否为空
    public boolean isEmpty() {
        return size == 0;
    }
    
    // 插入元素
    public void insert(int value) {
        if (size == capacity) {
            throw new IllegalStateException("堆已满,无法插入新元素");
        }
        
        // 将新元素添加到数组末尾
        heap[size] = value;
        size++;
        
        // 执行上浮操作,维持大根堆性质
        siftUp(size - 1);
    }
    
    // 弹出堆顶元素(最大值)
    public int pop() {
        if (isEmpty()) {
            throw new IllegalStateException("堆为空,无法弹出元素");
        }
        
        // 保存堆顶元素
        int maxValue = heap[0];
        
        // 将最后一个元素移到堆顶
        heap[0] = heap[size - 1];
        size--;
        
        // 执行下沉操作,维持大根堆性质
        siftDown(0);
        
        return maxValue;
    }
    
    // 上浮操作:当插入新元素时,将其向上调整到合适位置
    private void siftUp(int index) {
        // 只要不是根节点且当前节点大于父节点,就交换位置
        while (index > 0 && heap[index] > heap[parent(index)]) {
            swap(index, parent(index));
            index = parent(index);
        }
    }
    
    // 下沉操作:当移除堆顶元素后,将新的堆顶元素向下调整到合适位置
    private void siftDown(int index) {
        // 循环直到当前节点是叶子节点或满足大根堆性质
        while (true) {
            int left = leftChild(index);
            int right = rightChild(index);
            int largest = index; // 假设当前节点是最大的
            
            // 比较左子节点
            if (left < size && heap[left] > heap[largest]) {
                largest = left;
            }
            
            // 比较右子节点
            if (right < size && heap[right] > heap[largest]) {
                largest = right;
            }
            
            // 如果当前节点已是最大,则无需继续下沉
            if (largest == index) {
                break;
            }
            
            // 交换当前节点与最大子节点的位置
            swap(index, largest);
            index = largest;
        }
    }
    
    // 获取父节点索引
    private int parent(int index) {
        return (index - 1) / 2;
    }
    
    // 获取左子节点索引
    private int leftChild(int index) {
        return 2 * index + 1;
    }
    
    // 获取右子节点索引
    private int rightChild(int index) {
        return 2 * index + 2;
    }
    
    // 交换两个索引位置的元素
    private void swap(int i, int j) {
        int temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }
    
    // 打印堆中的元素
    public void printHeap() {
        System.out.println(Arrays.toString(Arrays.copyOf(heap, size)));
    }
    
    // 测试代码
    public static void main(String[] args) {
        MaxHeap heap = new MaxHeap(10);
        
        // 插入元素
        heap.insert(10);
        heap.insert(20);
        heap.insert(5);
        heap.insert(30);
        heap.insert(15);
        
        System.out.println("插入元素后的堆:");
        heap.printHeap(); // 应该输出 [30, 20, 5, 10, 15]
        
        // 弹出元素
        System.out.println("弹出的最大值: " + heap.pop()); // 30
        System.out.println("弹出后堆的状态:");
        heap.printHeap(); // 应该输出 [20, 15, 5, 10]
        
        heap.insert(25);
        System.out.println("插入25后的堆:");
        heap.printHeap(); // 应该输出 [25, 15, 5, 10, 20]
    }
}

最小堆java实现

java 复制代码
import java.util.Arrays;

public class MinHeap {
    private int[] heap;   // 存储堆元素的数组
    private int size;     // 当前堆中元素的数量
    private int capacity; // 堆的容量
    
    // 构造函数,初始化指定容量的堆
    public MinHeap(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.heap = new int[capacity];
    }
    
    // 获取堆的大小
    public int size() {
        return size;
    }
    
    // 判断堆是否为空
    public boolean isEmpty() {
        return size == 0;
    }
    
    // 插入元素
    public void insert(int value) {
        if (size == capacity) {
            throw new IllegalStateException("堆已满,无法插入新元素");
        }
        
        // 将新元素添加到数组末尾
        heap[size] = value;
        size++;
        
        // 执行上浮操作,维持小根堆性质
        siftUp(size - 1);
    }
    
    // 弹出堆顶元素(最小值)
    public int pop() {
        if (isEmpty()) {
            throw new IllegalStateException("堆为空,无法弹出元素");
        }
        
        // 保存堆顶元素
        int minValue = heap[0];
        
        // 将最后一个元素移到堆顶
        heap[0] = heap[size - 1];
        size--;
        
        // 执行下沉操作,维持小根堆性质
        siftDown(0);
        
        return minValue;
    }
    
    // 上浮操作:当插入新元素时,将其向上调整到合适位置
    private void siftUp(int index) {
        // 只要不是根节点且当前节点小于父节点,就交换位置
        while (index > 0 && heap[index] < heap[parent(index)]) {
            swap(index, parent(index));
            index = parent(index);
        }
    }
    
    // 下沉操作:当移除堆顶元素后,将新的堆顶元素向下调整到合适位置
    private void siftDown(int index) {
        // 循环直到当前节点是叶子节点或满足小根堆性质
        while (true) {
            int left = leftChild(index);
            int right = rightChild(index);
            int smallest = index; // 假设当前节点是最小的
            
            // 比较左子节点
            if (left < size && heap[left] < heap[smallest]) {
                smallest = left;
            }
            
            // 比较右子节点
            if (right < size && heap[right] < heap[smallest]) {
                smallest = right;
            }
            
            // 如果当前节点已是最小,则无需继续下沉
            if (smallest == index) {
                break;
            }
            
            // 交换当前节点与最小子节点的位置
            swap(index, smallest);
            index = smallest;
        }
    }
    
    // 获取父节点索引
    private int parent(int index) {
        return (index - 1) / 2;
    }
    
    // 获取左子节点索引
    private int leftChild(int index) {
        return 2 * index + 1;
    }
    
    // 获取右子节点索引
    private int rightChild(int index) {
        return 2 * index + 2;
    }
    
    // 交换两个索引位置的元素
    private void swap(int i, int j) {
        int temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }
    
    // 打印堆中的元素
    public void printHeap() {
        System.out.println(Arrays.toString(Arrays.copyOf(heap, size)));
    }
    
    // 测试代码
    public static void main(String[] args) {
        MinHeap heap = new MinHeap(10);
        
        // 插入元素
        heap.insert(10);
        heap.insert(20);
        heap.insert(5);
        heap.insert(30);
        heap.insert(15);
        
        System.out.println("插入元素后的堆:");
        heap.printHeap(); // 应该输出 [5, 10, 20, 30, 15]
        
        // 弹出元素
        System.out.println("弹出的最小值: " + heap.pop()); // 5
        System.out.println("弹出后堆的状态:");
        heap.printHeap(); // 应该输出 [10, 15, 20, 30]
        
        heap.insert(3);
        System.out.println("插入3后的堆:");
        heap.printHeap(); // 应该输出 [3, 10, 20, 30, 15]
    }
}

空间复杂度

  • 整体空间复杂度:O (n),其中 n 是堆的容量
    • 主要占用空间的是存储堆元素的数组heap,其大小为堆的容量
    • 其他变量(size、capacity 等)占用常数空间 O (1)

时间复杂度分析

  1. 插入操作(insert)

    • 时间复杂度:O (log n)
    • 分析:插入元素后可能需要执行上浮操作(siftUp),最多需要比较和交换的次数等于堆的高度。对于包含 n 个元素的完全二叉树,高度为 log₂n,因此时间复杂度为 O (log n)
  2. 弹出操作(pop)

    • 时间复杂度:O (log n)
    • 分析:弹出堆顶元素后需要执行下沉操作(siftDown),最多需要比较和交换的次数等于堆的高度,即 log₂n,因此时间复杂度为 O (log n)
  3. 构建堆操作(buildMinHeap)

    • 时间复杂度:O (n)
    • 分析:这个结果可能有点反直觉,虽然我们对每个非叶子节点执行了下沉操作(O (log n)),但实际上整体复杂度是线性的。
    • 具体推导:对于高度为 h 的节点,最多需要下沉 h 次。堆中高度为 h 的节点数量最多为 n/(2ʰ⁺¹),整体操作次数为 O (n)
  4. 辅助操作

    • parent ()、leftChild ()、rightChild ():O (1),仅进行简单的算术计算
    • swap ():O (1),仅进行常数次元素交换
    • size ()、isEmpty ():O (1),仅返回或判断变量值
相关推荐
free-elcmacom7 分钟前
机器学习高阶教程<6>推荐系统高阶修炼手册:混排、多任务与在线学习,解锁精准推荐新境界
人工智能·python·学习·算法·机器学习·机器人
断剑zou天涯7 分钟前
【算法笔记】AC自动机
java·笔记·算法
IT猿手10 分钟前
基于粒子群算法与动态窗口混合的无人机三维动态避障路径规划研究,MATLAB代码
算法·matlab·无人机·多目标优化算法·多目标算法
民乐团扒谱机11 分钟前
【微实验】仿AU音频编辑器开发实践:从零构建音频可视化工具
算法·c#·仿真·audio·fft·频谱
DanyHope14 分钟前
LeetCode 283. 移动零:双指针双解法(原地交换 + 覆盖补零)全解析
数据结构·算法·leetcode
山土成旧客16 分钟前
【Python学习打卡-Day24】从不可变元组到漫游文件系统:掌握数据结构与OS模块
数据结构·python·学习
bulingg29 分钟前
集成模型:gbdt,xgboost,lightgbm,catboost
人工智能·算法·机器学习
d111111111d31 分钟前
编码器测速详情解释:PID闭环控制
笔记·stm32·单片机·嵌入式硬件·学习·算法
麒qiqi36 分钟前
【Linux 进程间通信】信号通信与共享内存核心解析
java·linux·算法
肆悟先生1 小时前
3.15 引用类型
c++·算法