CSE-250 Fall 2022 - Section B - Heaps, Sets, Bags, and Ordered Trees

Heaps, Sets, Bags, and Ordered Trees

CSE-250 Fall 2022 - Section B

Oct 26, 2022

Textbook: Ch. 18, 16

Heaps (contd...)

Analysis

Enqueue
Append to ArrayBuffer: $O(n)$, Amortized $O(1)$
fixUp: $O(\log(n))$ fixes, each $O(1)$ = $O(\log(n))$
Total: Amortized $O(\log(n))$, Worst-case $O(n)$
Dequeue
Remove End of ArrayBuffer: $O(1)$
fixDown: $O(\log(n))$ fixes, each $O(1)$ = $O(\log(n))$
Total: Worst-case $O(\log(n))$

Heap Sort

  • Insert items into priority queue with enqueue
  • Reconstruct sequence (in reverse order) with dequeue

Heap Sort

Heap Sort

    Enqueue Element $i$
    $O(\log(i))$
    Dequeue Element $i$
    $O(\log(n-i))$

$$\left(\sum_{i=1}^n O(\log(i))\right) + \left(\sum_{i=1}^n O(\log(n-i))\right)$$

$$< O(n\log(n))$$

Updating Heap Elements

After updating current, fixUp or fixDown.

If new current > parent, current moves up.
If new current < parent, current moves down.

How do we know where the value appears in the heap?

Heapify

Input: Array

Output: Array reorderd to be a heap

Heapify

Level $\log(n)$
fixDown all $\frac{n}{2}$ nodes at this level (max $0$ swaps each)
Level $\log(n)-1$
fixDown all $\frac{n}{4}$ nodes at this level (max $1$ swaps each)
Level $\log(n)-2$
fixDown all $\frac{n}{8}$ nodes at this level (max $2$ swaps each)

Heapify

$$O\left(\sum_{i = 1}^{\log(n)} \frac{n}{2^{i}} \cdot (i+1) \right)$$

$$O\left(n \sum_{i = 1}^{\log(n)} \frac{i}{2^{i}} + \frac{1}{2^i}\right)$$

$$O\left(n \sum_{i = 1}^{\log(n)} \frac{i}{2^{i}}\right)$$

$$O\left(n \sum_{i = 1}^{\infty} \frac{i}{2^{i}}\right)$$

$\sum_{i = 1}^{\infty} \frac{i}{2^{i}}$ is known to converge to a constant.

$$O\left(n\right)$$

Heapify

We can go from an unsorted array to a heap in $O(n)$

(but heap sort still requires $n \log(n)$ for dequeueing)

Sets and Bags

Set

An unordered collection of unique elements.

  • Order doesn't matter
  • At most one copy of each item (key)

Set

mutable.Set[T]
add(element: T): Unit
Store one copy of element if not already present.
apply(element: T): Boolean
Return true if the element is present.
remove(element: T): Boolean
Remove the element if present or return false if not.

Bag

An unordered collection of non-unique elements.

  • Order doesn't matter
  • Multiple copies of a key allowed.

Bag

mutable.Bag[T]
add(element: T): Unit
Register the presence of a new (copy of) the element.
apply(element: T): Int
Return the number of copies of the element present.
remove(element: T): Boolean
Remove one copy the element if present or return false if not.

Collection ADTs

Property Seq Set Bag
Explicit Order ✓
Enforced Uniqueness ✓
Iterable ✓ ✓ ✓

(Rooted) Trees

Tree Terminology

Rooted, Directed Tree
Root is the source node
Parent of node X
A node with an out-edge to X (Max 1)
Child of node X
A node with an in-edge from X
Depth of a node X
Number of edges in the path from X to the root.
Height of a node
Number of edges in the path from X to the deepest leaf.

Tree Terminology

Level of a node.
Depth +1
Size of a tree ($n$)
The number of nodes in the tree
Height/Depth of a tree ($d$)
The height of the root / depth of the deepest leaf.

Tree Terminology

Binary Tree
Every vertex has at most 2 children
Complete Binary Tree
All leaf vertices at the deepest 2 levels.
Full Binary Tree
All leaf vertices at the deepest level.
(Every vertex has exactly 0 or 2 children)
$$d = \log(n)$$

Scala Aside

Tree Nodes (via Option)


    class TreeNode[T](
      var _value: T, 
      var _left: Option[TreeNode[T]]
      var _right: Option[TreeNode[T]]
    )

    class Tree[T] {
      var root: Option[TreeNode[T]] = None // empty tree
    }
  

Tree Nodes (via Traits)


    trait Tree[+T]

    case class TreeNode[T](
      value: T, 
      left: Tree[T],
      right: Tree[T]
    ) extends Tree[T]

    case object EmptyTree extends Tree[Nothing]
  

Case Classes/Objects

Feature 1: Inline constructors (no new)
TreeNode(10, EmptyTree, EmptyTree)
Feature 2: Match deconstructors
foo match { case TreeNode(v, l, r) => ... }

Case Classes/Objects


  def printTree[T](root: ImmutableTree[T], indent: Int) =
  {
    root match {
      case TreeNode(v, left, right) => 
        print((“ “ * indent) + v)
        printTree(left, indent + 2)
        printTree(right, indent + 2)

      case EmptyTree => 
        /* Do Nothing */
    }
  }
  

Computing Tree Height

The height of a tree is the height of the root.

$$h(root) = \begin{cases} 0 & \textbf{if the tree is empty}\\ 1 + max(h(\texttt{root.left}), h(\texttt{root.right})) & \textbf{otherwise} \end{cases}$$

Computing Tree Height

$$h(root) = \begin{cases} 0 & \textbf{if the tree is empty}\\ 1 + max(h(\texttt{root.left}), h(\texttt{root.right})) & \textbf{otherwise} \end{cases}$$

    def height[T](root: Tree[T]): Int =
    {
      root match {
        case EmptyTree => 
          0

        case TreeNode(v, left, right) =>
          1 + Math.max( height(left), height(right) )
      }      
    }
  

Binary Search Tree

A Binary Tree over where each node stores a unique key, and a value's keys are ordered.

Enforce Constraints
  • No duplicate keys
  • For every node $X_L$ in the left sub-tree of a node $X_1$: $X_L\texttt{.key} < X_1\texttt{.key}$
  • For every node $X_R$ in the left sub-tree of a node $X_1$: $X_R\texttt{.key} > X_1\texttt{.key}$

$X_1$ partitions its children.

Find

Goal: Find an item with key $k$ in a BST rooted at root

  • Is root empty? (if so, not present)
  • Does root.value have key $k$? (If so, done!)
  • Is $k$ lesser than root.value's key? (Try left subtree)
  • Is $k$ greater than root.value's key? (Try right subtree)

Find


def find[V: Ordering](root: BST[V], target: V): Option[V] =
  root match {
    case TreeNode(v, left, right) => 
           if(Ordering[V].lt( target, v )){ return find(left, target) }
      else if(Ordering[V].lt( v, target )){ return find(right, target) }
      else                                { return Some(v) }

    case EmptyTree => 
      return None
  }
  

What's the complexity? (how many times do we call 'find'?) $O(d)$

Insert

Goal: Insert an item with key $k$ in a BST rooted at root

  • Is root empty? (if so, insert here)
  • Does root.value have key $k$? (If so, already present!)
  • Is root.value's key greater than $k$? (Left subtree)
  • Is root.value's key lesser than $k$? (Right subtree)

Insert

  def insert[V: Ordering](root: BST[V], value: V): BST[V] =
    node match {
      case TreeNode(v, left, right) => 
        if(Ordering[V].lt( target, v ) ){
          return TreeNode(v, insert(left, target), right)
        } else if(Ordering[V].lt( v, target ) ){
          return TreeNode(v, left, insert(right, target))
        } else {
          return node // already present
        }

      case EmptyTree => 
        return TreeNode(value, EmptyTree, EmptyTree)
    }

What's the complexity? $O(d)$

Remove

Goal: Remove the item with key $k$ from a BST rooted at root

  • Find the item
  • Replace the node with the right subtree.
  • Insert the left subtree under the right.

What's the complexity? $O(d)$

BST Mutations

Operation Runtime
find $O(d)$
insert $O(d)$
remove $O(d)$

What's that in terms of $n$? $O(n)$

Does it need to be that bad?

Bags

How do we implement bags?

Idea 1: Just allow multiple copies ($X_L \leq X_1$)

Idea 2: One copy, but store a count

BSTs with Distinct Key/Value Pairs


    trait Tree[+K, +V]

    case class TreeNode[K, V](
      key: K, 
      value: V, 
      left: Tree[K, V],
      right: Tree[K, V]
    ) extends Tree[K, V]

    case object EmptyTree extends Tree[Nothing, Nothing]
  

Next time...

Balancing Trees