使用 Rust 实现 Heap

使用 Rust 的 Vector 作为底层数据结构来实现一个二叉堆(binary heap)。

Heap 的特点

二叉堆是一种特殊的完全二叉树,它有两个关键的特性:

  1. 完全二叉树性质:二叉堆总是一个完全树。也就是说,除了最后一层,所有层都是完全填充的,而最后一层的所有节点都尽可能地集中在左边。这意味着我们可以使用一个数组来表示二叉堆,而不需要使用指针或链接。例如,父节点i的左子节点和右子节点的索引分别为 2i+1 和 2i+2,这个性质对于上浮和下沉操作非常重要。
  2. 堆性质:所有的节点都大于或等于(最大堆)或小于或等于(最小堆)它们的子节点。这个性质是通过上浮(当插入一个新的元素时)或下沉(当移除堆顶元素时)操作来维持的。

我们利用了完全二叉树可以用一个数组来表示的性质,而且每个节点(除了根节点)的父节点可以通过(i-1)/2找到,左右子节点可以通过2*i+12*i+2找到。这使得堆的操作变得简单高效。

二叉堆特性的详细解释:

  • push:当一个新的元素被插入堆时,我们首先把它添加到数组的末尾(保持完全二叉树的性质),然后执行bubble_up操作来恢复堆性质。bubble_up操作是通过比较新插入的节点和它的父节点,如果新节点小于它的父节点(在最小堆中),那么就交换它们。然后递归地继续这个操作,直到新节点大于它的父节点,或者它已经到达了根节点。
  • pop:当我们想要移除堆顶元素时,如果我们直接移除堆顶元素,那么会打破完全二叉树的性质。为了维护这个性质,我们将数组的最后一个元素移动到堆顶,然后执行bubble_down操作来恢复堆性质。bubble_down操作是通过比较当前节点和它的两个子节点,如果当前节点大于它的任一子节点(在最小堆中),那么就把当前节点和它的最小子节点交换。然后递归地继续这个操作,直到当前节点小于它的所有子节点,或者它已经到达了树的底部。

因此,我们看到,这些操作都是为了维持二叉堆的两个关键特性,即完全二叉树性质和堆性质。

代码实现

这里有一个基本的最小堆(min heap)的实现:

rust
1// MinHeap struct,它有一个 Vec<i32> 类型的字段,它是堆的内部存储
2pub struct MinHeap {
3    data: Vec<i32>,
4}
5
6#[allow(dead_code)]
7impl MinHeap {
8    // 新建一个空的 MinHeap
9    pub fn new() -> Self {
10        MinHeap { data: vec![] }
11    }
12
13    // 在堆中插入一个元素,然后进行上浮操作保持堆的性质。
14    // 我们将新元素添加到数组的末尾以维持完全二叉树的特性,然后进行上浮操作以维持堆的特性。
15    pub fn push(&mut self, item: i32) {
16        self.data.push(item);
17        self.bubble_up(self.data.len() - 1);
18    }
19
20    // 从堆中弹出最小元素。如果堆为空,返回 None。
21    // 否则,我们将堆的最后一个元素交换到堆顶(这保持了完全二叉树的特性),然后进行下沉操作以维持堆的特性。
22    pub fn pop(&mut self) -> Option<i32> {
23        if self.data.is_empty() {
24            return None;
25        }
26
27        let min = self.data[0];
28        let last = self.data.pop().unwrap();
29        if !self.data.is_empty() {
30            self.data[0] = last;
31            self.bubble_down(0);
32        }
33        Some(min)
34    }
35
36    // 上浮操作。从当前节点开始,如果当前节点的值小于其父节点的值,则交换这两个节点。
37    // 然后递归地对父节点进行上浮操作,直到当前节点的值不再小于其父节点,或者已经到达根节点。
38    // 这个操作用于维护插入新元素后的堆的性质。
39    fn bubble_up(&mut self, idx: usize) {
40        if idx > 0 {
41            let parent = (idx - 1) / 2;
42            if self.data[parent] > self.data[idx] {
43                self.data.swap(parent, idx);
44                self.bubble_up(parent);
45            }
46        }
47    }
48
49    // 下沉操作。从当前节点开始,如果当前节点的值大于其任一子节点的值,则交换这两个节点。
50    // 然后递归地对新的子节点进行下沉操作,直到当前节点的值不再大于其子节点,或者已经到达叶子节点。
51    // 这个操作用于维护移除最小元素后的堆的性质。
52    fn bubble_down(&mut self, idx: usize) {
53        let left = 2 * idx + 1;
54        let right = 2 * idx + 2;
55        let mut min_idx = idx;
56
57        // 找到当前节点,左子节点和右子节点中最小的那个
58        if left < self.data.len() && self.data[left] < self.data[min_idx] {
59            min_idx = left;
60        }
61        if right < self.data.len() && self.data[right] < self.data[min_idx] {
62            min_idx = right;
63        }
64
65        // 如果当前节点不是最小的,那么它就需要下沉
66        if min_idx != idx {
67            self.data.swap(min_idx, idx);
68            self.bubble_down(min_idx);
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_heap(){
79        let mut heap = MinHeap::new();
80        heap.push(1);
81        heap.push(4);
82        heap.push(5);
83        heap.push(2);
84        heap.push(3);
85        assert_eq!(heap.pop(), Some(1));
86        assert_eq!(heap.pop(), Some(2));
87        assert_eq!(heap.pop(), Some(3));
88        assert_eq!(heap.pop(), Some(4));
89        assert_eq!(heap.pop(), Some(5));
90        assert_eq!(heap.pop(), None);
91    }
92}

这里,MinHeap结构体含有一个Vec<i32>类型的data字段,它作为堆的内部存储。

push方法将元素添加到堆中,并执行bubble_up操作以维持堆的特性。

pop方法移除并返回堆中的最小元素。如果堆是空的,它会返回None。否则,它会交换堆中的最后一个元素和堆顶元素,并执行bubble_down操作以维持堆的特性。

bubble_upbubble_down方法是用来在添加和移除元素时维持堆的特性。

为什么 bubble_up 中 parent 的索引为 (idx - 1) / 2

在二叉堆中,堆可以被看作一个完全二叉树,并且通常使用数组来实现。对于数组中的任何一个元素,其父节点和子节点的位置都有明确的关系:

  • 对于索引为idx的节点,其父节点的索引为(idx - 1) / 2,这是因为在完全二叉树中,左子节点的索引为2*idx + 1,右子节点的索引为2*idx + 2,所以对于任何一个子节点,其父节点的索引可以通过(idx - 1) / 2(对于左子节点)或(idx - 2) / 2(对于右子节点)来获取,由于整数除法会自动向下取整,所以这两个表达式都可以简化为(idx - 1) / 2
  • 对于索引为idx的节点,其左子节点的索引为2*idx + 1,右子节点的索引为2*idx + 2

因此,idx - 1在此处的含义是为了找到当前节点的父节点,然后和父节点的值进行比较,如果当前节点的值更小,那么就将它们进行交换,然后递归地进行"上浮"操作,直到满足堆的性质。