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),仅返回或判断变量值
相关推荐
go54631584651 小时前
基于深度学习的食管癌右喉返神经旁淋巴结预测系统研究
图像处理·人工智能·深度学习·神经网络·算法
aramae1 小时前
大话数据结构之<队列>
c语言·开发语言·数据结构·算法
大锦终1 小时前
【算法】前缀和经典例题
算法·leetcode
想变成树袋熊2 小时前
【自用】NLP算法面经(6)
人工智能·算法·自然语言处理
Coovally AI模型快速验证3 小时前
数据集分享 | 智慧农业实战数据集精选
人工智能·算法·目标检测·机器学习·计算机视觉·目标跟踪·无人机
墨尘游子3 小时前
目标导向的强化学习:问题定义与 HER 算法详解—强化学习(19)
人工智能·python·算法
恣艺3 小时前
LeetCode 854:相似度为 K 的字符串
android·算法·leetcode
予早3 小时前
《代码随想录》刷题记录
算法
满分观察网友z4 小时前
别总想着排序!我在数据看板中悟出的O(N)求第三大数神技(414. 第三大的数)
算法