题解 | #【模板】堆#
【模板】堆
https://www.nowcoder.com/practice/13f61c8c92404f5ea5d6fa4c692869fb
大根堆的概念
大根堆是一种特殊的完全二叉树结构,其中每个节点的值都大于或等于其子节点的值。在数组表示的堆中,对于任意节点i(从0开始计数),其左子节点的位置为2i+1,右子节点的位置为2i+2,而其父节点的位置为(i-1)/2。
大根堆的操作
- Push(插入):向堆中添加一个新的元素。
- Top(获取最大值):返回堆顶的元素,也就是当前堆中的最大值。
- Pop(删除最大值):移除堆顶的元素,并返回该值。
如何插入元素(Push)
当向堆中插入一个新元素时,我们首先将该元素添加到数组的末尾,然后执行上浮操作(Sift Up)。上浮操作是从叶子节点向上比较并交换节点与其父节点,直到满足堆的性质为止。
如何获取最大值(Top)
由于大根堆总是把最大的元素放在数组的第一个位置(即索引为0的位置),所以我们只需要返回数组的第一个元素即可。
如何删除最大值(Pop)
删除堆顶元素时,我们先记录下堆顶元素的值,然后将数组的最后一个元素移到堆顶,接着从堆顶开始执行下沉操作(Sift Down)。下沉操作是从根节点向下比较并交换节点与其较大的子节点,直到满足堆的性质为止。
算法重点:
- 数组表示的完全二叉树:使用数组来表示堆,其中每个元素的位置都有确定的关系:对于任意节点i(从0开始计数),其左子节点的位置为2i+1,右子节点的位置为2i+2,而其父节点的位置为(i-1)/2。
- 上浮操作(Sift Up):在插入新元素时,先将新元素放置在数组的末尾,然后通过上浮操作将其移动到合适的位置,确保堆的性质(父节点大于或等于其子节点)得到维持。
- 下沉操作(Sift Down):当删除堆顶元素后,将数组最后一个元素放置在堆顶,并通过下沉操作将其移动到合适的位置,同样是为了维持堆的性质。
代码亮点:
- 动态扩容机制:ensureCapacity() 方法确保当堆的大小达到数组容量时,能够自动扩展数组的大小。这样可以避免频繁的数组复制操作,提高性能。
- 简洁的上浮和下沉逻辑:siftUp() 和 siftDown() 方法的实现非常简洁明了,易于理解和维护。通过循环和条件判断,能够高效地调整堆的结构。
- 异常处理:在 top() 和 pop() 方法中加入了异常处理,当堆为空时抛出异常。这样可以在运行时检查堆的状态,防止程序因非法操作而崩溃。
import java.util.ArrayList; import java.util.Arrays; import java.util.Scanner; public class Main { // 定义堆数组和当前堆的大小 private int[] heap; private int size; // 构造函数初始化堆数组大小为0 public Main(int capacity) { heap = new int[capacity]; size = 0; } // 向堆中插入元素 public void push(int value) { ensureCapacity(); // 确保有足够空间容纳新元素 heap[size] = value; // 将新元素放入数组末尾 siftUp(size++); // 调整堆,使新元素上浮到合适位置 } // 返回堆顶元素(最大值) public int top() { if (size == 0) { throw new IllegalStateException("Heap is empty."); // 堆为空时抛出异常 } return heap[0]; // 堆顶元素即数组第一个元素 } // 移除并返回堆顶元素(最大值) public int pop() { if (size == 0) { throw new IllegalStateException("Heap is empty."); // 堆为空时抛出异常 } int root = heap[0]; // 记录堆顶元素 heap[0] = heap[--size]; // 将数组最后一个元素放到堆顶 siftDown(0); // 调整堆,使新堆顶元素下沉到合适位置 return root; // 返回原先的堆顶元素 } // 确保数组有足够的空间存放更多元素 private void ensureCapacity() { if (size == heap.length) { // 当前大小等于数组长度时 int newCapacity = heap.length * 2; // 新容量为原容量的两倍 heap = Arrays.copyOf(heap, newCapacity); // 创建新数组并复制旧数组内容 } } // 上浮操作,确保新插入的元素能正确排序 private void siftUp(int index) { while (index > 0 && heap[(index - 1) / 2] < heap[index]) { // 当前节点比父节点大时 swap((index - 1) / 2, index); // 交换当前节点和父节点 index = (index - 1) / 2; // 更新当前节点为父节点 } } // 下沉操作,确保替换堆顶元素后能正确排序 private void siftDown(int index) { int half = size >>> 1; // 计算一半的大小 while (index < half) { // 当前节点还有子节点时 int child = (index << 1) + 1; // 左子节点的索引 int swapIndex = child + (heap[child] < heap[child + 1] && child + 1 < size ? 1 : 0); // 找到较大子节点的索引 if (heap[index] >= heap[swapIndex]) { // 当前节点大于等于较大子节点时,不需要交换 break; } swap(index, swapIndex); // 交换当前节点和较大子节点 index = swapIndex; // 更新当前节点为较大子节点 } } // 交换两个指定索引处的元素 private void swap(int i, int j) { int temp = heap[i]; heap[i] = heap[j]; heap[j] = temp; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); // 初始化堆 Main heap = new Main(100000); // 读取输入的操作数量 int n = sc.nextInt(); // 创建ArrayList存储所有输入的操作 ArrayList<String> operations = new ArrayList<>(); while (sc.hasNextLine() && operations.size() <= n) { String operation = sc.nextLine(); operations.add(operation); } // 遍历所有操作并执行相应的方法 for (String op : operations) { if (op.startsWith("push")) { String[] parts = op.split(" "); int value = Integer.parseInt(parts[1]); heap.push(value); } else if ("top".equals(op)) { try { System.out.println(heap.top()); } catch (IllegalStateException e) { System.out.println("empty"); } } else if ("pop".equals(op)) { try { System.out.println(heap.pop()); } catch (IllegalStateException e) { System.out.println("empty"); } } } } }