使用 Rust 实现 Heap
Published by powerfulyang on Jun 1, 2023
使用 Rust 的 Vector 作为底层数据结构来实现一个二叉堆(binary heap)。
Heap 的特点
二叉堆是一种特殊的完全二叉树,它有两个关键的特性:
- 完全二叉树性质:二叉堆总是一个完全树。也就是说,除了最后一层,所有层都是完全填充的,而最后一层的所有节点都尽可能地集中在左边。这意味着我们可以使用一个数组来表示二叉堆,而不需要使用指针或链接。例如,父节点i的左子节点和右子节点的索引分别为 2i+1 和 2i+2,这个性质对于上浮和下沉操作非常重要。
- 堆性质:所有的节点都大于或等于(最大堆)或小于或等于(最小堆)它们的子节点。这个性质是通过上浮(当插入一个新的元素时)或下沉(当移除堆顶元素时)操作来维持的。
我们利用了完全二叉树可以用一个数组来表示的性质,而且每个节点(除了根节点)的父节点可以通过(i-1)/2
找到,左右子节点可以通过2*i+1
和2*i+2
找到。这使得堆的操作变得简单高效。
二叉堆特性的详细解释:
push
:当一个新的元素被插入堆时,我们首先把它添加到数组的末尾(保持完全二叉树的性质),然后执行bubble_up
操作来恢复堆性质。bubble_up
操作是通过比较新插入的节点和它的父节点,如果新节点小于它的父节点(在最小堆中),那么就交换它们。然后递归地继续这个操作,直到新节点大于它的父节点,或者它已经到达了根节点。pop
:当我们想要移除堆顶元素时,如果我们直接移除堆顶元素,那么会打破完全二叉树的性质。为了维护这个性质,我们将数组的最后一个元素移动到堆顶,然后执行bubble_down
操作来恢复堆性质。bubble_down
操作是通过比较当前节点和它的两个子节点,如果当前节点大于它的任一子节点(在最小堆中),那么就把当前节点和它的最小子节点交换。然后递归地继续这个操作,直到当前节点小于它的所有子节点,或者它已经到达了树的底部。
因此,我们看到,这些操作都是为了维持二叉堆的两个关键特性,即完全二叉树性质和堆性质。
代码实现
这里有一个基本的最小堆(min heap)的实现:
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_up
和bubble_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
在此处的含义是为了找到当前节点的父节点,然后和父节点的值进行比较,如果当前节点的值更小,那么就将它们进行交换,然后递归地进行"上浮"操作,直到满足堆的性质。