【Java】堆

注意:不要将本文的"堆"与 JVM 内存中的"堆内存"混淆。前者是组织数据的方式,后者是存放对象的内存区域。

堆的概念

什么是堆?

是一种特殊的 完全二叉树 ,并且满足以下 堆性质

  • 大顶堆 :每个节点的值都 大于或等于 其左右子节点的值(根节点最大)。
  • 小顶堆 :每个节点的值都 小于或等于 其左右子节点的值(根节点最小)。

为什么用数组存储堆?

由于堆是一棵完全二叉树,可以用 数组 来高效存储,不需要维护复杂的左右指针。

父子节点下标关系(假设数组索引从 0 开始):

  • 父节点索引 = (i - 1) / 2
  • 左子节点索引 = 2 * i + 1
  • 右子节点索引 = 2 * i + 2

手搓简化版堆

MyHeap的成员变量和构造函数

java 复制代码
private int[] heap;
private int size;
private int capacity;

public MyHeap(int capacity) {
    this.capacity = capacity;
    this.heap = new int[capacity];
    this.size = 0;
}

MyHeap的辅助函数

获取父节点和左右孩子的索引

java 复制代码
// 获取父节点和左右孩子
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; }

交换数组中两个位置的元素

java 复制代码
private void swap(int i, int j) {
    int temp = heap[i];
    heap[i] = heap[j];
    heap[j] = temp;
}

获取堆顶元素

java 复制代码
public int peek() {
    if (size == 0) throw new IllegalStateException("Heap is empty");
    return heap[0];
}

判空

java 复制代码
public boolean isEmpty() { return size == 0; }

求大小

java 复制代码
public int size() { return size; }

打印堆

java 复制代码
public void printHeap() {
    System.out.print("Heap: ");
    for (int i = 0; i < size; i++) {
        System.out.print(heap[i] + " ");
    }
    System.out.println();
}

MyHeap插入元素

java 复制代码
/**
 * 向大顶堆中插入新元素
 * 
 * 【算法思想】:上浮(Sift Up / Bubble Up)
 * 
 * 核心原理:
 * 1. 将新元素添加到堆的末尾(完全二叉树的最后一个位置)
 * 2. 由于新元素可能破坏堆的性质(父节点 ≥ 子节点),需要向上调整
 * 3. 比较新节点与其父节点,如果新节点更大,则交换位置
 * 4. 重复步骤3,直到新节点不大于父节点或到达根节点
 * 
 * 时间复杂度:O(log n),其中n为堆中元素个数
 *   - 最坏情况:从叶子节点上浮到根节点,需要比较树的高度次
 *   - 完全二叉树的高度为 ⌊log₂n⌋
 * 
 * 空间复杂度:O(1),原地调整
 * 
 * 举例说明:
 * 初始大顶堆:[50, 30, 40, 20, 25]
 * 插入新元素 60:
 *   1. 添加到末尾:[50, 30, 40, 20, 25, 60]
 *   2. 60的父节点是40,60>40,交换:[50, 30, 60, 20, 25, 40]
 *   3. 60的父节点是50,60>50,交换:[60, 30, 50, 20, 25, 40]
 *   4. 60到达根节点,完成插入
 * 
 * @param val 要插入的元素值
 * @throws IllegalStateException 如果堆已满
 */
public void insert(int val) {
    // 1. 边界检查:堆容量是否已满
    if (size == capacity) {
        throw new IllegalStateException("Heap is full");
    }
    
    // 2. 将新元素放在数组末尾(完全二叉树的最后一个位置)
    heap[size] = val;
    
    // 3. 上浮调整:从新插入的位置开始向上调整
    siftUp(size);
    
    // 4. 堆大小增加1
    size++;
}
java 复制代码
/**
 * 上浮操作:将指定位置的节点向上调整,维护大顶堆性质
 * 
 * 【算法思想】:自底向上调整(Bottom-Up Heapification)
 * 
 * 核心原理:
 * 1. 大顶堆要求每个父节点 ≥ 子节点
 * 2. 当子节点大于父节点时,违反堆性质,需要交换
 * 3. 交换后,子节点上移成为父节点,继续与新的父节点比较
 * 4. 直到满足堆性质或到达根节点
 * 
 * 循环不变式:
 *   - 每次循环开始时,childIndex位置的节点是待调整的节点
 *   - 交换后,以parentIndex为根的子树满足堆性质
 * 
 * 时间复杂度:O(log n)
 *   - 最多比较树的高度次:⌊log₂(n+1)⌋
 * 
 * 空间复杂度:O(1)
 * 
 * 示意图:
 *        50                    60
 *       /  \                  /  \
 *      30   60    →          30   50
 *     / \  /                / \  /
 *    20 25 40              20 25 40
 *    
 *    60上浮到根节点
 * 
 * @param childIndex 需要上浮的节点索引(通常是新插入的节点)
 */
private void siftUp(int childIndex) {
    // 1. 计算当前节点的父节点索引
    int parentIndex = parent(childIndex);
    
    // 2. 循环条件:未到达根节点 且 子节点 > 父节点(违反大顶堆性质)
    while (childIndex > 0 && heap[childIndex] > heap[parentIndex]) {
        // 3. 交换子节点和父节点的值
        swap(parentIndex, childIndex);
        
        // 4. 更新索引,继续向上检查
        childIndex = parentIndex;
        parentIndex = parent(childIndex);
    }
    
    // 5. 循环结束时,堆性质已恢复
}

MyHeap删除堆顶

java 复制代码
/**
 * 删除并返回堆顶元素(最大值)
 * 
 * 【算法思想】:替换 + 下沉(Replace + Sift Down)
 * 
 * 核心原理:
 * 1. 大顶堆的堆顶就是最大值,需要返回给调用者
 * 2. 直接删除堆顶会破坏完全二叉树结构
 * 3. 解决方法:将最后一个元素移到堆顶,然后向下调整
 * 4. 堆顶元素下沉到合适位置,重新满足堆性质
 * 
 * 操作步骤:
 *   - 保存堆顶元素(待返回值)
 *   - 将最后一个元素移到堆顶
 *   - 删除最后一个元素(size-1)
 *   - 从堆顶开始向下调整
 * 
 * 时间复杂度:O(log n)
 *   - 主要耗时在siftDown操作,最多下沉树的高度次
 * 
 * 空间复杂度:O(1)
 * 
 * 举例说明:
 * 大顶堆:[90, 80, 70, 60, 50, 40, 30]
 * 执行poll():
 *   1. 保存堆顶90
 *   2. 最后一个元素30移到堆顶:[30, 80, 70, 60, 50, 40]
 *   3. 从堆顶开始下沉:
 *      - 30与较大的子节点80比较,30<80,交换:[80, 30, 70, 60, 50, 40]
 *      - 30与较大的子节点60比较,30<60,交换:[80, 60, 70, 30, 50, 40]
 *      - 30与子节点40比较,30<40,交换:[80, 60, 70, 40, 50, 30]
 *   4. 返回90
 * 
 * @return 堆顶元素(最大值)
 * @throws IllegalStateException 如果堆为空
 */
public int poll() {
    // 1. 边界检查:堆是否为空
    if (size == 0) {
        throw new IllegalStateException("Heap is empty");
    }
    
    // 2. 保存堆顶元素(即将返回的值)
    int root = heap[0];
    
    // 3. 堆大小减1(逻辑删除最后一个元素)
    size--;
    
    // 4. 如果堆不为空,将最后一个元素移到堆顶
    if (size > 0) {
        heap[0] = heap[size];
        // 5. 从堆顶开始下沉调整
        siftDown(0);
    }
    
    // 6. 返回原堆顶元素
    return root;
}
java 复制代码
/**
 * 下沉操作:将指定位置的节点向下调整,维护大顶堆性质
 * 
 * 【算法思想】:自顶向下调整(Top-Down Heapification)
 * 
 * 核心原理:
 * 1. 大顶堆要求父节点 ≥ 两个子节点
 * 2. 当父节点小于某个子节点时,违反堆性质
 * 3. 选择较大的子节点与父节点交换(确保交换后父节点最大)
 * 4. 交换后,原父节点下沉到子节点位置,继续向下比较
 * 5. 直到满足堆性质或到达叶子节点
 * 
 * 为什么选择较大的子节点?
 *   - 如果与较小的子节点交换,交换后的父节点可能仍小于另一个子节点
 *   - 示例:父节点=20,左子=30,右子=40
 *     与左子交换后:[30, 20, 40] → 20 < 40,仍违反性质
 *     与右子交换后:[40, 30, 20] → 满足性质
 * 
 * 循环不变式:
 *   - parentIndex始终指向待调整的节点
 *   - 每次循环选择较大的子节点进行比较
 * 
 * 时间复杂度:O(log n)
 *   - 最多下沉树的高度次:⌊log₂n⌋
 * 
 * 空间复杂度:O(1)
 * 
 * 示意图:
 *        30                    80
 *       /  \                  /  \
 *      80  70    →           30  70
 *     / \  /                / \  /
 *    40 50 60              40 50 60
 *    
 *    30下沉,与较大的子节点80交换
 * 
 * @param parentIndex 需要下沉的节点索引(通常是堆顶或某个父节点)
 */
private void siftDown(int parentIndex) {
    // 1. 获取左子节点索引(作为循环起始条件)
    int left = leftChild(parentIndex);
    
    // 2. 循环条件:左子节点存在(说明有子节点)
    while (left < size) {
        // 3. 找出左右子节点中较大的一个
        int maxIndex = left;           // 假设左子节点最大
        int right = rightChild(parentIndex);
        
        // 4. 如果右子节点存在且大于左子节点,则更新最大索引
        if (right < size && heap[right] > heap[left]) {
            maxIndex = right;
        }
        
        // 5. 如果最大的子节点 > 父节点,违反堆性质,需要交换
        if (heap[maxIndex] > heap[parentIndex]) {
            // 6. 交换父节点和较大的子节点
            swap(maxIndex, parentIndex);
            
            // 7. 更新索引,继续向下调整
            parentIndex = maxIndex;
            left = leftChild(parentIndex);
        } else {
            // 8. 如果父节点已经是最大的,满足堆性质,结束循环
            break;
        }
    }
}

MyHeap批量建堆

java 复制代码
/**
 * 从无序数组批量构建大顶堆
 * 
 * 【算法思想】:Floyd建堆算法(自底向上堆化)
 * 
 * 核心原理:
 * 1. 方法一(低效):逐个插入,时间复杂度 O(n log n)
 * 2. 方法二(高效):从最后一个非叶子节点开始,自底向上执行siftDown
 *    - 叶子节点无需调整(它们没有子节点)
 *    - 从最后一个父节点开始,依次向前对每个父节点执行siftDown
 *    - 保证每个子树都满足堆性质,最终整个树满足堆性质
 * 
 * 为什么从最后一个非叶子节点开始?
 *   - 叶子节点已经满足堆性质(没有子节点)
 *   - 最后一个非叶子节点是第一个可能需要调整的节点
 *   - 计算公式:(size/2) - 1
 * 
 * 时间复杂度:O(n),数学推导证明
 *   - 看似有n/2次siftDown,但每层节点数和下沉高度不同
 *   - 第h层的节点下沉h次,总时间 = Σ(h * 2^(h))
 *   - 最终收敛到O(n)
 * 
 * 空间复杂度:O(1)
 * 
 * 对比分析:
 *   逐个插入:O(n log n)
 *   Floyd建堆:O(n)
 *   当n=100000时,Floyd算法快约10倍
 * 
 * 举例说明:
 * 无序数组:[20, 50, 30, 70, 40, 60, 10]
 * 
 * 步骤1:找到最后一个父节点(索引2,值30)
 *        20
 *       /  \
 *      50  30 ← 从这开始
 *     / \  / \
 *    70 40 60 10
 * 
 * 步骤2:调整索引2的节点(30与60交换)
 *        20
 *       /  \
 *      50  60
 *     / \  / \
 *    70 40 30 10
 * 
 * 步骤3:调整索引1的节点(50与70交换)
 *        20
 *       /  \
 *      70  60
 *     / \  / \
 *    50 40 30 10
 * 
 * 步骤4:调整索引0的节点(20与70交换,然后20与50交换)
 *        70
 *       /  \
 *      50  60
 *     / \  / \
 *    20 40 30 10
 * 
 * 最终得到大顶堆
 * 
 * @param arr 无序数组(会复制到堆内部数组中)
 * @throws IllegalArgumentException 如果数组大小超过堆容量
 */
public void buildHeap(int[] arr) {
    // 1. 边界检查:数组是否超过堆容量
    if (arr.length > capacity) {
        throw new IllegalArgumentException("Array size exceeds capacity");
    }
    
    // 2. 复制数组到堆内部存储
    System.arraycopy(arr, 0, heap, 0, arr.length);
    size = arr.length;
    
    // 3. 从最后一个非叶子节点开始,自底向上执行siftDown
    //    最后一个非叶子节点 = (size - 2) / 2
    //    因为最后一个节点的索引是size-1,其父节点就是最后一个非叶子节点
    for (int i = (size - 2) / 2; i >= 0; i--) {
        siftDown(i);
    }
    
    // 4. 循环结束时,整个数组已经满足大顶堆性质
}

完整测试

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

public class MyHeap {
    private int[] heap;
    private int size;
    private int capacity;

    public MyHeap(int capacity) {
        this.capacity = capacity;
        this.heap = new int[capacity];
        this.size = 0;
    }

    // 获取父节点和左右孩子
    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; }

    // 插入元素
    public void insert(int val) {
        if (size == capacity) {
            throw new IllegalStateException("Heap is full");
        }
        heap[size] = val;
        siftUp(size);
        size++;
    }

    private void siftUp(int childIndex) {
        int parentIndex = parent(childIndex);
        while (parentIndex >= 0 && childIndex > 0) {
            if (heap[childIndex] > heap[parentIndex]) {
                swap(parentIndex, childIndex);
                childIndex = parentIndex;
                parentIndex = parent(childIndex);
            } else {
                break;
            }
        }
    }

    // 删除堆顶
    public int poll() {
        if (size == 0) throw new IllegalStateException("Heap is empty");

        int root = heap[0];
        size--;  // 先减少size
        if (size > 0) {
            heap[0] = heap[size];
            siftDown(0);
        }
        return root;
    }

    // 批量建堆(从无序数组构建堆)
    public void buildHeap(int[] arr) {
        if (arr.length > capacity) {
            throw new IllegalArgumentException("Array size exceeds capacity");
        }
        System.arraycopy(arr, 0, heap, 0, arr.length);
        size = arr.length;
        // 从最后一个非叶子节点开始下沉
        for (int i = (size - 2) / 2; i >= 0; i--) {
            siftDown(i);
        }
    }

    // 下沉操作
    private void siftDown(int parentIndex) {
        int left = leftChild(parentIndex);
        int right = rightChild(parentIndex);

        while (left < size) {
            int max = heap[left];
            int index = left;

            if (right < size && heap[right] > heap[left]) {
                max = heap[right];
                index = right;
            }

            if (max > heap[parentIndex]) {
                swap(index, parentIndex);
                parentIndex = index;
                left = leftChild(parentIndex);
                right = rightChild(parentIndex);
            } else {
                break;
            }
        }
    }

    private void swap(int i, int j) {
        int temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }

    public int peek() {
        if (size == 0) throw new IllegalStateException("Heap is empty");
        return heap[0];
    }

    public boolean isEmpty() { return size == 0; }

    public int size() { return size; }

    public void printHeap() {
        System.out.print("Heap: ");
        for (int i = 0; i < size; i++) {
            System.out.print(heap[i] + " ");
        }
        System.out.println();
    }

    // 测试主函数
    public static void main(String[] args) {
        System.out.println("========== 大顶堆测试 ==========\n");

        // 测试1:基本插入和删除
        System.out.println("【测试1】基本插入和删除操作");
        MyHeap heap1 = new MyHeap(10);
        int[] testData = {50, 30, 80, 20, 90, 10, 70, 40, 60, 100};
        System.out.println("插入数据: " + Arrays.toString(testData));
        for (int num : testData) {
            heap1.insert(num);
        }
        heap1.printHeap();
        System.out.println("堆顶元素: " + heap1.peek());

        System.out.println("\n依次取出堆顶元素:");
        while (!heap1.isEmpty()) {
            System.out.print(heap1.poll() + " ");
        }
        System.out.println("\n");

        // 测试2:批量建堆
        System.out.println("【测试2】批量建堆测试");
        MyHeap heap2 = new MyHeap(15);
        int[] randomArray = {15, 5, 25, 10, 30, 20, 8, 35, 3, 28};
        System.out.println("原始数组: " + Arrays.toString(randomArray));
        heap2.buildHeap(randomArray);
        heap2.printHeap();
        System.out.println("堆顶元素: " + heap2.peek());

        System.out.println("\n降序输出(堆排序):");
        while (!heap2.isEmpty()) {
            System.out.print(heap2.poll() + " ");
        }
        System.out.println("\n");

        // 测试3:边界情况
        System.out.println("【测试3】边界情况测试");
        MyHeap heap3 = new MyHeap(5);
        System.out.println("堆是否为空: " + heap3.isEmpty());

        try {
            heap3.peek();
        } catch (IllegalStateException e) {
            System.out.println("正确捕获: " + e.getMessage());
        }

        try {
            heap3.poll();
        } catch (IllegalStateException e) {
            System.out.println("正确捕获: " + e.getMessage());
        }

        heap3.insert(100);
        heap3.insert(200);
        heap3.insert(300);
        heap3.insert(400);
        heap3.insert(500);
        heap3.printHeap();

        try {
            heap3.insert(600);
        } catch (IllegalStateException e) {
            System.out.println("正确捕获: " + e.getMessage());
        }
        System.out.println();

        // 测试4:随机数据验证堆性质
        System.out.println("【测试4】随机数据验证堆性质");
        MyHeap heap4 = new MyHeap(100);
        Random rand = new Random(42);
        int[] randomData = new int[20];
        for (int i = 0; i < 20; i++) {
            randomData[i] = rand.nextInt(100);
            heap4.insert(randomData[i]);
        }
        System.out.println("随机数据: " + Arrays.toString(randomData));
        heap4.printHeap();

        // 验证堆性质:每个父节点都大于等于子节点
        boolean isValid = true;
        for (int i = 0; i < heap4.size() / 2; i++) {
            int left = 2 * i + 1;
            int right = 2 * i + 2;
            if (left < heap4.size() && heap4.heap[i] < heap4.heap[left]) {
                isValid = false;
                System.out.printf("违反堆性质: 父节点[%d]=%d < 左孩子[%d]=%d\n",
                        i, heap4.heap[i], left, heap4.heap[left]);
            }
            if (right < heap4.size() && heap4.heap[i] < heap4.heap[right]) {
                isValid = false;
                System.out.printf("违反堆性质: 父节点[%d]=%d < 右孩子[%d]=%d\n",
                        i, heap4.heap[i], right, heap4.heap[right]);
            }
        }
        System.out.println("堆性质验证: " + (isValid ? "✓ 通过" : "✗ 失败"));
        System.out.println();

        // 测试5:堆排序(降序)
        System.out.println("【测试5】堆排序演示");
        MyHeap heap5 = new MyHeap(10);
        int[] sortData = {42, 17, 89, 33, 6, 71, 25, 94, 53, 28};
        System.out.println("原始数据: " + Arrays.toString(sortData));
        for (int num : sortData) {
            heap5.insert(num);
        }
        System.out.print("堆排序结果: ");
        while (!heap5.isEmpty()) {
            System.out.print(heap5.poll() + " ");
        }
        System.out.println("\n");

        // 测试6:相同元素处理
        System.out.println("【测试6】重复元素测试");
        MyHeap heap6 = new MyHeap(10);
        int[] duplicateData = {10, 20, 10, 30, 20, 40, 10};
        System.out.println("重复数据: " + Arrays.toString(duplicateData));
        for (int num : duplicateData) {
            heap6.insert(num);
        }
        heap6.printHeap();
        System.out.print("依次取出: ");
        while (!heap6.isEmpty()) {
            System.out.print(heap6.poll() + " ");
        }
        System.out.println();
    }
}

输出结果

复制代码
========== 大顶堆测试 ==========

【测试1】基本插入和删除操作
插入数据: [50, 30, 80, 20, 90, 10, 70, 40, 60, 100]
Heap: 100 90 70 60 80 10 50 20 40 30 
堆顶元素: 100

依次取出堆顶元素:
100 90 80 70 60 50 40 30 20 10 

【测试2】批量建堆测试
原始数组: [15, 5, 25, 10, 30, 20, 8, 35, 3, 28]
Heap: 35 30 25 10 28 20 8 5 3 15 
堆顶元素: 35

降序输出(堆排序):
35 30 28 25 20 15 10 8 5 3 

【测试3】边界情况测试
堆是否为空: true
正确捕获: Heap is empty
正确捕获: Heap is empty
Heap: 500 400 200 100 300 
正确捕获: Heap is full

【测试4】随机数据验证堆性质
随机数据: [30, 63, 48, 84, 70, 25, 5, 18, 19, 93, 82, 2, 76, 92, 76, 32, 56, 70, 43, 9]
Heap: 93 84 92 70 82 48 76 32 56 63 70 2 25 5 76 18 30 19 43 9 
堆性质验证: ✓ 通过

【测试5】堆排序演示
原始数据: [42, 17, 89, 33, 6, 71, 25, 94, 53, 28]
堆排序结果: 94 89 71 53 42 33 28 25 17 6 

【测试6】重复元素测试
重复数据: [10, 20, 10, 30, 20, 40, 10]
Heap: 40 20 30 10 20 10 10 
依次取出: 40 30 20 20 10 10 10 

拓展

PriorityQueue

补充

Top K 问题

补充Top K 问题

相关推荐
Lyyaoo.2 小时前
【Java基础面经】Java 反射机制
java·开发语言·python
YXWik62 小时前
Langchain4j(1)基础对话+连续对话+工具调用 + 流式响应+结构化 JSON 输出
java
m0_694845572 小时前
UVdesk部署教程:企业级帮助台系统实践
服务器·开发语言·后端·golang·github
泉飒2 小时前
C2001: 常量中有换行符-QT解决办法-逆向思路
开发语言·qt
96772 小时前
什么是 Thymeleaf?
java
Dream_sky分享2 小时前
找类中字段属性不同工具类
java
ghie90902 小时前
基于学习的模型预测控制(LBMPC)MATLAB实现指南
开发语言·学习·matlab
咚为2 小时前
Rust 经典面试题255道
开发语言·面试·rust
givemeacar2 小时前
SpringBoot3.3.0集成Knife4j4.5.0实战
java