🚧 We are still building the lighthouse. Hang tight!

projectlighthouse logoprojectlighthouse

Sign in with your Google or GitHub account to get started

Push and Pop Operations

Implementing insertion and extraction in heaps. Learn sift-up and sift-down operations for maintaining the heap property.

The two fundamental heap operations are push (insert) and pop (extract). Both maintain the heap property by "sifting" elements up or down the tree. Understanding these operations is key to implementing priority queues.

Sift Up (Bubble Up)

When inserting, we add the element at the end (to maintain completeness) and then "sift up" to restore the heap property.

Insert 85 into max-heap [90, 70, 80, 40, 50, 60]:

Step 1: Add at end
       90
      /  \
    70    80
   /  \   / \
  40  50 60  85   <- Added here

Step 2: Compare 85 with parent 80
        85 > 80, swap!
       90
      /  \
    70    85
   /  \   / \
  40  50 60  80

Step 3: Compare 85 with parent 90
        85 < 90, done!

Result: [90, 70, 85, 40, 50, 60, 80]

Implementation

struct MaxHeap {
    heap: Vec<i32>,
}

impl MaxHeap {
    fn new() -> Self {
        MaxHeap { heap: Vec::new() }
    }

    fn push(&mut self, val: i32) {
        self.heap.push(val);
        self.sift_up(self.heap.len() - 1);
    }

    fn sift_up(&mut self, mut i: usize) {
        while i > 0 {
            let parent = (i - 1) / 2;

            if self.heap[i] <= self.heap[parent] {
                break;
            }

            // Swap with parent
            self.heap.swap(i, parent);
            i = parent;
        }
    }
}

For a min-heap, change <= to >=:

struct MinHeap {
    heap: Vec<i32>,
}

impl MinHeap {
    fn new() -> Self {
        MinHeap { heap: Vec::new() }
    }

    fn push(&mut self, val: i32) {
        self.heap.push(val);
        self.sift_up(self.heap.len() - 1);
    }

    fn sift_up(&mut self, mut i: usize) {
        while i > 0 {
            let parent = (i - 1) / 2;

            if self.heap[i] >= self.heap[parent] {
                break;
            }

            self.heap.swap(i, parent);
            i = parent;
        }
    }
}

Sift Down (Bubble Down)

When extracting the root, we:

  1. Replace root with the last element
  2. Remove the last position
  3. Sift down from the root to restore heap property
Extract from max-heap [90, 70, 85, 40, 50, 60, 80]:

Step 1: Save root (90), replace with last (80)
       80
      /  \
    70    85
   /  \   /
  40  50 60

Step 2: Compare 80 with children (70, 85)
        Largest child is 85, 80 < 85, swap!
       85
      /  \
    70    80
   /  \   /
  40  50 60

Step 3: Compare 80 with children (60)
        80 > 60, done!

Result: [85, 70, 80, 40, 50, 60], return 90

Implementation

impl MaxHeap {
    // ... previous methods ...

    fn pop(&mut self) -> Option<i32> {
        if self.heap.is_empty() {
            return None;
        }

        // Save the root value
        let result = self.heap[0];

        // Move last element to root
        if let Some(last) = self.heap.pop() {
            if !self.heap.is_empty() {
                self.heap[0] = last;
                self.sift_down(0);
            }
        }

        Some(result)
    }

    fn sift_down(&mut self, mut i: usize) {
        let n = self.heap.len();

        loop {
            let mut largest = i;
            let left = 2 * i + 1;
            let right = 2 * i + 2;

            if left < n && self.heap[left] > self.heap[largest] {
                largest = left;
            }

            if right < n && self.heap[right] > self.heap[largest] {
                largest = right;
            }

            if largest == i {
                break;
            }

            self.heap.swap(i, largest);
            i = largest;
        }
    }
}

Why Swap with Last Element?

Removing from the middle of an array is O(n). By moving the last element to the root, we maintain the complete tree property and only need O(log n) sift operations to restore the heap property.

Complete Min-Heap Implementation

struct MinHeap {
    heap: Vec<i32>,
}

impl MinHeap {
    fn new() -> Self {
        MinHeap { heap: Vec::new() }
    }

    fn push(&mut self, val: i32) {
        self.heap.push(val);
        self.sift_up(self.heap.len() - 1);
    }

    fn pop(&mut self) -> Option<i32> {
        if self.heap.is_empty() {
            return None;
        }

        let result = self.heap[0];
        if let Some(last) = self.heap.pop() {
            if !self.heap.is_empty() {
                self.heap[0] = last;
                self.sift_down(0);
            }
        }

        Some(result)
    }

    fn peek(&self) -> Option<i32> {
        self.heap.get(0).copied()
    }

    fn sift_up(&mut self, mut i: usize) {
        while i > 0 {
            let parent = (i - 1) / 2;
            if self.heap[i] >= self.heap[parent] {
                break;
            }
            self.heap.swap(i, parent);
            i = parent;
        }
    }

    fn sift_down(&mut self, mut i: usize) {
        let n = self.heap.len();

        loop {
            let mut smallest = i;
            let left = 2 * i + 1;
            let right = 2 * i + 2;

            if left < n && self.heap[left] < self.heap[smallest] {
                smallest = left;
            }

            if right < n && self.heap[right] < self.heap[smallest] {
                smallest = right;
            }

            if smallest == i {
                break;
            }

            self.heap.swap(i, smallest);
            i = smallest;
        }
    }

    fn len(&self) -> usize {
        self.heap.len()
    }

    fn is_empty(&self) -> bool {
        self.heap.is_empty()
    }
}

// Usage
fn main() {
    let mut heap = MinHeap::new();
    for val in [5, 3, 8, 1, 2, 7] {
        heap.push(val);
    }

    while !heap.is_empty() {
        println!("{}", heap.pop().unwrap());  // 1, 2, 3, 5, 7, 8
    }
}

Time Complexity

| Operation | Time | Description | |-----------|------|-------------| | push | O(log n) | Sift up at most height levels | | pop | O(log n) | Sift down at most height levels | | peek | O(1) | Access root directly |

The height of a heap with n elements is ⌊log₂(n)⌋, so both sift operations are O(log n).

Visualizing Sift Operations

Sift Up (after insert 95 into [90, 70, 80]):

     90                 90                 95
    /  \     insert    /  \     sift     /  \
   70  80    --->    70   80    up     90   80
                        \              /
                        95           70

Sift Down (after pop from [90, 70, 80, 40]):

     90                 40                 80
    /  \     replace   /  \     sift    /  \
   70  80    root    70   80    down   70   40
  /                  /                  /
 40                (empty)            (empty)

Sift Down Direction

When sifting down in a max-heap, always swap with the LARGER child. In a min-heap, swap with the SMALLER child. This maintains the heap property efficiently.

Using Rust's BinaryHeap

Rust provides a built-in max-heap in the standard library:

use std::collections::BinaryHeap;
use std::cmp::Reverse;

// Create a max-heap
let mut max_heap = BinaryHeap::new();
max_heap.push(5);
max_heap.push(3);
max_heap.push(8);
max_heap.push(1);

println!("{}", max_heap.peek().unwrap());  // 8 (peek at max)
println!("{}", max_heap.pop().unwrap());   // 8
println!("{}", max_heap.pop().unwrap());   // 5

// For min-heap, use Reverse wrapper
let mut min_heap = BinaryHeap::new();
min_heap.push(Reverse(5));
min_heap.push(Reverse(3));
min_heap.push(Reverse(8));
min_heap.push(Reverse(1));

println!("{}", min_heap.pop().unwrap().0);  // 1 (smallest)
println!("{}", min_heap.pop().unwrap().0);  // 3

Heap with Custom Objects

Using tuples or custom comparisons:

use std::collections::BinaryHeap;
use std::cmp::Reverse;

// Priority queue with (priority, task)
// Use Reverse for min-heap behavior
let mut pq = BinaryHeap::new();
pq.push(Reverse((2, "task B")));
pq.push(Reverse((1, "task A")));
pq.push(Reverse((3, "task C")));

while let Some(Reverse((priority, task))) = pq.pop() {
    println!("{} (priority {})", task, priority);
}

// Output:
// task A (priority 1)
// task B (priority 2)
// task C (priority 3)

Applications

Top K Elements

use std::collections::BinaryHeap;
use std::cmp::Reverse;

fn find_k_largest(nums: &[i32], k: usize) -> Vec<i32> {
    // Find k largest elements using min-heap of size k
    let mut heap = BinaryHeap::new();

    for &num in nums {
        heap.push(Reverse(num));
        if heap.len() > k {
            heap.pop();
        }
    }

    let mut result: Vec<i32> = heap.into_iter().map(|Reverse(x)| x).collect();
    result.sort_by(|a, b| b.cmp(a));  // Sort descending
    result
}

fn main() {
    let nums = vec![3, 1, 4, 1, 5, 9, 2, 6];
    println!("{:?}", find_k_largest(&nums, 3));  // [9, 6, 5]
}

Merge K Sorted Lists

use std::collections::BinaryHeap;
use std::cmp::Reverse;

fn merge_k_sorted(lists: Vec<Vec<i32>>) -> Vec<i32> {
    // Merge k sorted lists using min-heap
    let mut result = Vec::new();
    let mut heap = BinaryHeap::new();

    // Initialize heap with first element of each list
    for (i, list) in lists.iter().enumerate() {
        if !list.is_empty() {
            heap.push(Reverse((list[0], i, 0)));
        }
    }

    while let Some(Reverse((val, list_idx, element_idx))) = heap.pop() {
        result.push(val);

        // Push next element from same list
        if element_idx + 1 < lists[list_idx].len() {
            let next_val = lists[list_idx][element_idx + 1];
            heap.push(Reverse((next_val, list_idx, element_idx + 1)));
        }
    }

    result
}

Key Takeaways

  1. Push: Add at end, sift up to restore heap property
  2. Pop: Replace root with last, sift down to restore heap property
  3. Both operations are O(log n)
  4. Sift up compares with parent; sift down compares with children
  5. For max-heap: swap when child > parent; for min-heap: swap when child < parent
  6. Rust's BinaryHeap provides efficient max-heap operations (use Reverse for min-heap)

Understanding sift up and sift down is fundamental for heaps. These operations form the basis of heap sort, priority queues, and many graph algorithms.

Notes & Highlights

© 2025 projectlighthouse. All rights reserved.