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