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),仅返回或判断变量值
相关推荐
Qhumaing26 分钟前
C++学习:【PTA】数据结构 7-1 实验7-1(最小生成树-Prim算法)
c++·学习·算法
踩坑记录2 小时前
leetcode hot100 3.无重复字符的最长子串 medium 滑动窗口(双指针)
python·leetcode
Z1Jxxx2 小时前
01序列01序列
开发语言·c++·算法
汽车仪器仪表相关领域3 小时前
全自动化精准检测,赋能高效年检——NHD-6108全自动远、近光检测仪项目实战分享
大数据·人工智能·功能测试·算法·安全·自动化·压力测试
Doro再努力4 小时前
【数据结构08】队列实现及练习
数据结构·算法
清铎5 小时前
leetcode_day12_滑动窗口_《绝境求生》
python·算法·leetcode·动态规划
linweidong5 小时前
嵌入式电机:如何在低速和高负载状态下保持FOC(Field-Oriented Control)算法的电流控制稳定?
stm32·单片机·算法
踩坑记录6 小时前
leetcode hot100 42 接雨水 hard 双指针
leetcode
net3m336 小时前
单片机屏幕多级菜单系统之当前屏幕号+屏幕菜单当前深度 机制
c语言·c++·算法