Heaps and Priority Queues
Introduction
In this lesson, we'll explore heaps and priority queues, their properties, and their usage in Rust. Heaps and priority queues are essential for managing and accessing data with priority.
Key Points
- Heaps: A specialized tree-based data structure that satisfies the heap property, commonly used to implement priority queues.
- Priority Queues: An abstract data type where each element has a priority, and elements are served based on their priority.
Heaps
A heap is a complete binary tree that satisfies the heap property. There are two types of heaps:
- Max-Heap: The value of each node is greater than or equal to the values of its children.
- Min-Heap: The value of each node is less than or equal to the values of its children.
Max-Heap Implementation
Here’s a basic implementation of a max-heap in Rust using a vector:
#[derive(Debug)] struct MaxHeap<T> { heap: Vec<T>, } impl<T: Ord> MaxHeap<T> { fn new() -> Self { MaxHeap { heap: Vec::new() } } fn insert(&mut self, value: T) { self.heap.push(value); self.heapify_up(self.heap.len() - 1); } fn extract_max(&mut self) -> Option<T> { if self.heap.is_empty() { return None; } let max = self.heap.swap_remove(0); self.heapify_down(0); Some(max) } fn heapify_up(&mut self, index: usize) { let mut child = index; while child > 0 { let parent = (child - 1) / 2; if self.heap[child] <= self.heap[parent] { break; } self.heap.swap(child, parent); child = parent; } } fn heapify_down(&mut self, index: usize) { let mut parent = index; let len = self.heap.len(); loop { let mut max = parent; let left = 2 * parent + 1; let right = 2 * parent + 2; if left < len && self.heap[left] > self.heap[max] { max = left; } if right < len && self.heap[right] > self.heap[max] { max = right; } if max == parent { break; } self.heap.swap(parent, max); parent = max; } } } fn main() { let mut max_heap = MaxHeap::new(); max_heap.insert(10); max_heap.insert(20); max_heap.insert(5); println!("Max Heap: {:?}", max_heap); while let Some(max) = max_heap.extract_max() { println!("Extracted Max: {}", max); } }
Code Explanation
insert
: Adds a value to the heap and maintains the heap property.extract_max
: Removes and returns the maximum value from the heap.heapify_up
: Reorders the heap after insertion to maintain the heap property.heapify_down
: Reorders the heap after extraction to maintain the heap property.
Priority Queues
A priority queue is an abstract data type where each element has a priority. Elements are dequeued based on their priority rather than their order of insertion.
Priority Queue Implementation with Heap
Here's an implementation of a priority queue using a max-heap in Rust:
#[derive(Debug)] struct PriorityQueue<T> { heap: MaxHeap<T>, } impl<T: Ord> PriorityQueue<T> { fn new() -> Self { PriorityQueue { heap: MaxHeap::new(), } } fn enqueue(&mut self, value: T) { self.heap.insert(value); } fn dequeue(&mut self) -> Option<T> { self.heap.extract_max() } fn peek(&self) -> Option<&T> { self.heap.heap.first() } } fn main() { let mut pq = PriorityQueue::new(); pq.enqueue(30); pq.enqueue(10); pq.enqueue(20); println!("Priority Queue: {:?}", pq); while let Some(max) = pq.dequeue() { println!("Dequeued: {}", max); } }
Code Explanation
enqueue
: Adds an element to the priority queue with the given priority.dequeue
: Removes and returns the element with the highest priority.peek
: Returns a reference to the element with the highest priority without removing it.
Real World Examples
Heaps
Heaps are often used in:
- Heap Sort: A sorting algorithm that uses a heap to sort elements efficiently.
- Task Scheduling: In operating systems, heaps can manage tasks based on their priority.
Priority Queues
Priority queues are used in scenarios where tasks or elements need to be processed based on priority:
- Dijkstra’s Algorithm: Used in shortest path algorithms where nodes are processed based on their distance.
- Job Scheduling: In operating systems, priority queues manage processes or tasks based on their priority.
Conclusion
Heaps and priority queues are powerful data structures for managing and accessing data with priority. By implementing these structures in Rust, you can handle various applications more efficiently and understand key algorithms better.