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),仅返回或判断变量值
相关推荐
荆棘鸟智能3 分钟前
AI算法持续学习与迭代怎么做?从数据闭环到灰度发布的MLOps工程实践
人工智能·算法·架构·边缘计算
Lyyaoo.8 分钟前
【二分查找】【中等】搜索二维矩阵/排序数组的第一位和最后位置/搜索旋转排序数组/旋转排序数组中的最小值
java·数据结构·算法
土司大王17 分钟前
LeetCode hot100——153.寻找旋转排序数组中的最小值:Java 二分模板与 O(log n) 分析
java·算法·leetcode
光电的一只菜鸡18 分钟前
isp中关于锐化对图像清晰度的影响
算法
土司大王39 分钟前
LeetCode hot100——4.寻找两个正序数组的中位数:Java 二分第K小 递归裁剪
java·算法·leetcode
薛定e的猫咪1 小时前
(ICML2024)QSM:基于 Q 函数梯度对齐分数场的扩散模型离策略强化学习
人工智能·深度学习·算法
hetao17338371 小时前
2026-09-13 hetao1733837 的刷题记录
c++·算法
hansang_IR1 小时前
【题解】[JSOI2016] 扭动的回文串
c++·算法
Lyyaoo.1 小时前
【二分查找】【困难】寻找两个有序数组的中位数
java·数据结构·算法