eifueo/docs/2a/ece250.md
2023-11-03 12:40:16 -04:00

3.5 KiB

ECE 250: DSA

Heaps

A heap is a binary tree stored in an array in which all levels but the lowest are filled. It is guaranteed that the parent of index \(i\) is greater than or equal to the element at index \(i\).

  • the parent of index \(i\) is stored at \(i/2\)
  • the left child of index \(i\) is stored at \(2i\)
  • the right child of index \(i\) is stored at \(2i+1\)

(Source: Wikimedia Commons)

The heapify command takes a node and makes it and its children a valid heap.

fn heapify(&mut A: Vec, i: usize) {
    if A[2*i] >= A[i] {
        A.swap(2*i, i);
        heapify(A, 2*i)
    } else if A[2*i + 1] >= A[i] {
        A.swap(2*i + 1, i);
        heapify(A, 2*i + 1)
    }
}

Repeatedly heapifying an array from middle to beginning converts it to a heap.

fn build_heap(A: Vec) {
    let n = A.len()
    for i in (n/2).floor()..0 { // this is technically not valid but it's much clearer
        heapify(A, i);
    }
}

Heapsort

Heapsort constructs a heap annd then does magic things that I really cannot be bothered to figure out right now.

fn heapsort(A: Vec) {
    build_heap(A);
    let n = A.len();
    for i in n..0 {
        A.swap(1, i);
        heapify(A, 1);  // NOTE: heapify takes into account the changed value of n
    }
}

Priority queues

A priority queue is a heap with the property that it can remove the highest value in \(O(\log n)\) time.

fn pop(A: Vec, &n: usize) {
    let biggest = A[0];
    
    A[0] = n;
    *n -= 1;
    heapify(A, 1);
    return biggest;
}
fn insert(A: Vec, &n: usize, key: i32) {
    *n += 1;
    
    let i = n;
    while i > 1 && A[parent(i)] < key {
        A[i] = A[parent(i)];
        i = parent(i);
    }
    A[i] = k;
}

Sorting algorithms

Quicksort

Quicksort operates by selecting a pivot point that ensures that everything to the left of the pivot is less than anything to the right of the pivot, which is what partitioning does.

fn partition(A: Vec, left_bound: usize, right_bound: usize) {
    let i = left_bound;
    let j = right_bound;
    
    while true {
        while A[j] <= A[right_bound] { j -= 1; }
        while A[i] >= A[left_bound] { i += 1; }
        
        if i < j { A.swap(i, j); }
        else { return j }  // new bound!
    }
}

Sorting calls partitioning with smaller and smaller bounds until the collection is sorted.

fn sort(a: Vec, left: usize, right: usize) {
    if left < right {
        let pivot = partition(A, left, right);
        sort(A, left, pivot);
        sort(A, pivot+1, right);
    }
}
  • In the best case, if partitioning is even, the time complexity is \(T(n)=T(n/2)+\Theta(n)=\Theta(n\log n)\).
  • In the worst case, if one side only has one element, which occurs if the list is sorted, the time complexity is \(\Theta(n^2)\).

Counting sort

If items are or are linked to a number from \(1..n\) (duplicates are allowed), counting sort counts the number of each number, then moves things to the correct position. Where \(k\) is the size of the counter array, the time complexity is \(O(n+k)\).

First, construct a count prefix sum array:

fn count(A: Vec, K: usize) {
    let counter = vec![0; K];
    
    for i in A {
        counter[i] += 1;
    }
    
    for (index, val) in counter.iter_mut().enumerate() {
        counter[index + 1] += val;   // ignore bounds for cleanliness please :)
    }
    return counter
}

Next, the prefix sum represents the correct position for each item.

fn sort(A: Vec) {
    let counter = count(A, 100);
    let sorted = vec![0; A.len()];
    
    for i in n..0 {
        sorted[counter[A[i]]] = A[i];
        counter[A[i]] -= 1;
    }
}