1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
9. Interfaces and Polymorphism
10. Inheritance
11. Additional Java Features
12. Collections and Generics
13. Linked Data
14. Iterating over Data Structures
15. Stacks and Queues
16. Trees and their Iterators
17. Binary Search Trees
18. Heaps and Priority Queues
19. Sets and Maps
19. Sets and Maps

19. Sets and Maps

In today’s lecture, we’ll introduce two more ADTs, Set and Map. We’ll consider a few realizations of these ADTs backed by different data structures we have seen earlier in the course, and we’ll compare the performance trade-offs we get from each of these approaches. In the next lecture, we’ll introduce another amazing data structure, a hash table, that will provide even stronger performance guarantees under some assumptions. Later in the lecture, we’ll also consider some more specialized operations on Sets, and we’ll leverage Maps to develop an enhanced priority queue that allows the priorities of its elements to be updated.

The Set ADT

Sets are one of the central building blocks of mathematics that provide a way to collect different objects together into a single named entity. Sets are characterized by two main properties. First, their elements are unordered. There is no notion of a first, second, or last element in a set; in other words, sets are distinguished only by their contents (which elements they contain and don’t contain) and not by the particular order that their elements are enumerated. Second, the elements of a set are distinct. A set cannot contain two or more copies of the same element.

Definition: Set

A set is an unordered collection of distinct elements.

In designing a Set ADT, we must model these properties. As a collection, we will want a way to add() and remove() elements to our set and to check whether the set contains() a particular element. Since sets are unordered, our ADT should not provide a way to obtain an index of an element or expose notions of next, previous, first, or last elements. To ensure that the elements of our set are distinct, we will need to allow our add() method to fail if the client attempts to re-add an element that is already present in the set. We can do this by having add() return a boolean that indicates whether the addition was successful. Symmetrically, we’ll have our remove() method return a boolean that indicates whether the removal was successful (i.e., whether the element that the client asked to remove() was present in the set). This results in the following Set ADT.

Set.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Models an unordered collection of elements of type T without duplicates.
 */
public interface Set<T> extends Iterable<T> {
  /**
   * Attempts to add the given `elem` to this set. Returns true if `elem` was 
   * successfully added, and returns false if `elem` could not be added because 
   * it was already present in the set. Requires that `elem != null`.
   */
  boolean add(T elem);

  /**
   * Returns whether the given `elem` is present in this set. Requires that 
   * `elem != null`.
   */
  boolean contains(T elem);

  /**
   * Returns the number of elements present in this set.
   */
  int size();

  /**
   * Attempts to remove the given `elem` from this set. Returns true if `elem` 
   * was successfully removed, and returns false if `elem` could not be 
   * removed because it was not present in the set. 
   * Requires that `elem != null`.
   */
  boolean remove(T elem);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Models an unordered collection of elements of type T without duplicates.
 */
public interface Set<T> extends Iterable<T> {
  /**
   * Attempts to add the given `elem` to this set. Returns true if `elem` was 
   * successfully added, and returns false if `elem` could not be added because 
   * it was already present in the set. Requires that `elem != null`.
   */
  boolean add(T elem);

  /**
   * Returns whether the given `elem` is present in this set. Requires that 
   * `elem != null`.
   */
  boolean contains(T elem);

  /**
   * Returns the number of elements present in this set.
   */
  int size();

  /**
   * Attempts to remove the given `elem` from this set. Returns true if `elem` 
   * was successfully removed, and returns false if `elem` could not be 
   * removed because it was not present in the set. 
   * Requires that `elem != null`.
   */
  boolean remove(T elem);
}

Note that our Set interface extends the Iterable interface so that a client can iterate over the elements of a Set using an enhanced-for loop. Since Sets are unordered, the specification makes no guarantee about the order that the elements are returned by calls to the iterator’s next() method.

Now that we have introduced the basic Set interface, we can consider different data structures that can realize it.

ListSet

Similar to the priority queue from last lecture, we can implement the Set interface using a List (in this case, Java’s ArrayList) as the backing storage. Since our Set interface exposes less information than the List interface (it “forgets” about the notion of indices), a composition relationship is the natural choice. If we add() new elements at the end of the list, a particular element can be at an arbitrary position in the list. Therefore, checking for the presence of a particular element within the set (necessary for add(), contains() , and remove()) becomes a linear time operation, a linear search. Our implementation is shown below.

ListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

/**
 * An implementation of the Set interface leveraging composition with an ArrayList.
 */
public class ListSet<T> implements Set<T> {
  /**
   * The ArrayList containing the elements of this set.
   */
  private final ArrayList<T> list;

  /**
   * Constructs a ListSet with no elements.
   */
  public ListSet() {
    list = new ArrayList<>();
  }

  @Override
  public boolean add(T elem) {
    assert elem != null;
    if (contains(elem)) {
      return false;
    }
    list.add(elem);
    return true;
  }

  @Override
  public boolean contains(T elem) {
    assert elem != null;
    // linear search:
    for (T member : list) {
      if (member.equals(elem)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public int size() {
    return list.size();
  }

  @Override
  public boolean remove(T elem) {
    assert elem != null;
    if (!contains(elem)) {
      return false;
    }
    list.remove(elem);
    return true;
  }

  @Override
  public Iterator<T> iterator() {
    return list.iterator();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

/**
 * An implementation of the Set interface leveraging composition with an ArrayList.
 */
public class ListSet<T> implements Set<T> {
  /**
   * The ArrayList containing the elements of this set.
   */
  private final ArrayList<T> list;

  /**
   * Constructs a ListSet with no elements.
   */
  public ListSet() {
    list = new ArrayList<>();
  }

  @Override
  public boolean add(T elem) {
    assert elem != null;
    if (contains(elem)) {
      return false;
    }
    list.add(elem);
    return true;
  }

  @Override
  public boolean contains(T elem) {
    assert elem != null;
    // linear search:
    for (T member : list) {
      if (member.equals(elem)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public int size() {
    return list.size();
  }

  @Override
  public boolean remove(T elem) {
    assert elem != null;
    if (!contains(elem)) {
      return false;
    }
    list.remove(elem);
    return true;
  }

  @Override
  public Iterator<T> iterator() {
    return list.iterator();
  }
}

The size() and iterator() methods inherit the \(O(1)\) performance of their respective ArrayList counterparts. The runtime of add(), contains(), and remove(), are all bounded by the runtime of contains(), which is \(O(N)\) for the linear search. As we discussed above, the distinct-elements requirement of a Set requires us to frequently perform membership checks, so we’d prefer a data structure that can carry these out efficiently. Just as with the priority queue, a second thought is to impose a sorted invariant on the list entries.

SortedListSet

If our Set implementation composes with a sorted ArrayList, our membership queries can use a binary search instead of a linear search, improving their performance to \(O(\log N)\). We’ll extract this binary search into a private find() helper method that returns the index where this element is/would be located, as this extra information is required for our add() and remove() methods.

SortedListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * An implementation of the Set interface leveraging composition with an 
 * ArrayList that maintains a sorted invariant.
 */
public class SortedListSet<T extends Comparable<T>> implements Set<T> {
// Note: T must be Comparable so its elements can be sorted

  /**
   * The ArrayList containing the elements of this set in sorted order.
   */
  private final ArrayList<T> list;

  /**
   * Constructs a SortedListSet with no elements.
   */
  public SortedListSet() {
    list = new ArrayList<>();
  }

  /**
   * Returns the value `r` with `0 <= r <= list.size()` such that 
   * `list[..r) < v` and `list[r..) >= v`.
   */
  private int find(T target) { }

  // ... Set methods
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * An implementation of the Set interface leveraging composition with an 
 * ArrayList that maintains a sorted invariant.
 */
public class SortedListSet<T extends Comparable<T>> implements Set<T> {
// Note: T must be Comparable so its elements can be sorted

  /**
   * The ArrayList containing the elements of this set in sorted order.
   */
  private final ArrayList<T> list;

  /**
   * Constructs a SortedListSet with no elements.
   */
  public SortedListSet() {
    list = new ArrayList<>();
  }

  /**
   * Returns the value `r` with `0 <= r <= list.size()` such that 
   * `list[..r) < v` and `list[r..) >= v`.
   */
  private int find(T target) { }

  // ... Set methods
}

As a review of binary search, take some time to complete the definition of the find() method according to its specification. To reduce the space complexity, we’ve developed an iterative definition using a loop invariant. Since the list field can store elements of any Comparable reference type, your definition will need to use the compareTo() method.

find() definition

SortedListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Returns the value `r` with `0 <= r <= list.size()` such that `list[..r) < v` 
 * and `list[r..) >= v`.
 */
private int find(T target) {
  int l = 0; // left window boundary (inclusive)
  int r = list.size(); // right window boundary (exclusive)
  /* Loop invariant: `list[..l) < v`, `list[r..] >= v` */
  while (l < r) {
      int m = l + (r - l) / 2; // midpoint
      int c = target.compareTo(list.get(m)); // result of comparison
      if (c == 0) { // target at midpoint
          return m;
      } else if (c > 0) { // target bigger than midpoint
          l = m + 1;
      } else { // target smaller than midpoint
          r = m;
      }
  }
  return r;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Returns the value `r` with `0 <= r <= list.size()` such that `list[..r) < v` 
 * and `list[r..) >= v`.
 */
private int find(T target) {
  int l = 0; // left window boundary (inclusive)
  int r = list.size(); // right window boundary (exclusive)
  /* Loop invariant: `list[..l) < v`, `list[r..] >= v` */
  while (l < r) {
      int m = l + (r - l) / 2; // midpoint
      int c = target.compareTo(list.get(m)); // result of comparison
      if (c == 0) { // target at midpoint
          return m;
      } else if (c > 0) { // target bigger than midpoint
          l = m + 1;
      } else { // target smaller than midpoint
          r = m;
      }
  }
  return r;
}

Now, take some time to use this find() method to complete the definitions of the contains(), add(), and remove() methods.

contains() definition

SortedListSet.java

1
2
3
4
5
6
@Override
public boolean contains(T elem) {
  assert elem != null;
  int i = find(elem);
  return i < list.size() && list.get(i).equals(elem);
}
1
2
3
4
5
6
@Override
public boolean contains(T elem) {
  assert elem != null;
  int i = find(elem);
  return i < list.size() && list.get(i).equals(elem);
}

add() definition

SortedListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
  public boolean add(T elem) {
    assert elem != null;
    int i = find(elem);
    if (i < list.size() && list.get(i).equals(elem)) {
      return false;
    }
    list.add(i, elem);
    return true;
  }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
  public boolean add(T elem) {
    assert elem != null;
    int i = find(elem);
    if (i < list.size() && list.get(i).equals(elem)) {
      return false;
    }
    list.add(i, elem);
    return true;
  }

remove() definition

SortedListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public boolean remove(T elem) {
  assert elem != null;
  int i = find(elem);
  if (i == list.size() || !list.get(i).equals(elem)) {
    return false;
  }
  list.remove(i);
  return true;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public boolean remove(T elem) {
  assert elem != null;
  int i = find(elem);
  if (i == list.size() || !list.get(i).equals(elem)) {
    return false;
  }
  list.remove(i);
  return true;
}

Let’s consider the runtime complexities of these methods. Since the find() method is performing a binary search on a list of \(N\) elements, it has an \(O(\log N)\) runtime. Since contains() just calls find() along with three extra \(O(1)\) checks, its runtime is also \(O(\log N)\). While we may be tempted to declare that runtimes add() and remove() also have \(O(\log N)\) runtimes (because of their call to find()), we must be careful. These methods add or remove an element from its sorted position in the list, which may be at its start and require an \(O(N)\) shift of the remaining elements. Thus, the worst-case runtimes of both add() and remove() are \(O(N)\). To address this, we’ll need to move away from a dense, linear backing storage.

TreeSet

We know from previous lectures that binary search trees support add(), contains(), and remove() methods with an \(O(\textrm{height})\) worst-case time complexity, and that balanced binary trees achieve an \(O(\log N)\) complexity for these operations. We can leverage this to give a Set definition (a TreeSet) based on a composition relationship with a BST that achieves these same performance guarantees.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
 * An implementation of the Set interface leveraging composition with a BST.
 */
public class TreeSet<T extends Comparable<T>> implements Set<T> {
  // Note: T must be Comparable so it can be stored in the BST field

  /**
   * The BST containing the elements of this set.
   */
  private final BST<T> tree;

  /**
   * Constructs a TreeSet with no elements.
   */
  public TreeSet() {
    tree = new BST<>();
  }

  @Override
  public boolean add(T elem) {
    if (contains(elem)) {
      return false;
    }
    tree.add(elem);
    return true;
  }

  @Override
  public boolean contains(T elem) {
    return tree.contains(elem);
  }

  @Override
  public int size() {
    return tree.size();
  }

  @Override
  public boolean remove(T elem) {
    if (!contains(elem)) {
      return false;
    }
    tree.remove(elem);
    return true;
  }

  @Override
  public Iterator<T> iterator() {
    return tree.iterator();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
 * An implementation of the Set interface leveraging composition with a BST.
 */
public class TreeSet<T extends Comparable<T>> implements Set<T> {
  // Note: T must be Comparable so it can be stored in the BST field

  /**
   * The BST containing the elements of this set.
   */
  private final BST<T> tree;

  /**
   * Constructs a TreeSet with no elements.
   */
  public TreeSet() {
    tree = new BST<>();
  }

  @Override
  public boolean add(T elem) {
    if (contains(elem)) {
      return false;
    }
    tree.add(elem);
    return true;
  }

  @Override
  public boolean contains(T elem) {
    return tree.contains(elem);
  }

  @Override
  public int size() {
    return tree.size();
  }

  @Override
  public boolean remove(T elem) {
    if (!contains(elem)) {
      return false;
    }
    tree.remove(elem);
    return true;
  }

  @Override
  public Iterator<T> iterator() {
    return tree.iterator();
  }
}

Note that this code is almost identical to the ListSet but with list swapped out for tree. Both of these classes delegate most responsibility to their field data structures. This illustrates the benefits of abstractions and helps to underscore the importance of understanding the performance guarantees of different data structures. If our BST is balanced, and if our elements are Comparable, then simply switching out the data structure with which we compose (from List to BST) offers us an exponential performance improvement with minimal additional work.

A Set definition backed by a balanced tree offers the optimal worst-case performance guarantees. In the next lecture, we’ll introduce another data structure, the hash table, that will allow us to greatly improve the expected performance of the Set operations. For today, we’ll next consider how to augment our Set definitions to support some other useful set operations.

Additional Set Operations

Mathematical sets support some additional basic operations that we may wish to support on our Set data types. All of the operations that we’ll consider are operators, meaning they take in one or more sets as inputs and return a new set as their output. In our brief overview, we’ll only consider adding these methods to the ListSet class. We leave it to you to extend these ideas to our other Set implementations, which we walk through in the lecture exercises.

Union

The first operation we’ll consider is the set union.

Definition: Union

Given two sets \(S\) and \(T\), their union \(S \cup T\) consists of all elements that belong to either \(S\) or \(T\) (or both of these sets).

We can model the union operation with the following method of the ListSet class.

ListSet.java

1
2
3
4
/** 
 * Returns a new set containing the union of this set and the given `other` set.
 */
public Set<T> union(Set<T> other) { }
1
2
3
4
/** 
 * Returns a new set containing the union of this set and the given `other` set.
 */
public Set<T> union(Set<T> other) { }

Throughout, we’ll use \(N\) to denote set1.size() and \(M\) to denote set2.size(). For our complexity analysis, we’ll also assume that \(N \geq M\), since we can always switch the sets in constant time if this is not true. One straightforward way to define union() is to simply add() the elements from set1 and set2 to a new set.

ListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/** 
 * Returns a new set containing the union of this set and the given `other` set.
 */
public Set<T> union(Set<T> other) { 
  ListSet<T> union = new ListSet<>();
  for (T elem : list) {
      union.add(elem);
  }
  for (T elem : other) {
      union.add(elem);
  }
  return union;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/** 
 * Returns a new set containing the union of this set and the given `other` set.
 */
public Set<T> union(Set<T> other) { 
  ListSet<T> union = new ListSet<>();
  for (T elem : list) {
      union.add(elem);
  }
  for (T elem : other) {
      union.add(elem);
  }
  return union;
}

What will be the worst-case runtime of this method? This will occur when there are no common elements between this and other (in other words, this and other are disjoint), which will cause union to be as large as possible. In this case, the \(N+M\) add() calls will have runtime bounded by the \(N+M\) contains() calls, which will have total runtime,

\[ \sum_{i=1}^{N+M} i = O\big( (N+M)^2 \big). \]

This approach performs a lot of unnecessary work. Notice that during the first loop, we call add() for each element of list, which will do \(O(N)\) contains() checks to make sure none of these elements are in union. However, we know that the elements of list are distinct, so will definitely not already be in union during their add() call. We can skip these contains() checks and add the elements directly to union’s backing ArrayList. Similarly, when we are adding the elements of other, we only need to check whether they are in list, not in the larger union; we know that they cannot an earlier-added element of other since other has distinct elements. Overall, these modifications lead to the following definition.

ListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** 
 * Returns a new set containing the union of this set and the given `other` set.
 */
public Set<T> union(Set<T> other) { 
  ListSet<T> union = new ListSet<>();
  for (T elem : list) {
      union.list.add(elem);
  }
  for (T elem : other) {
    if (!contains(elem)) {
      union.list.add(elem);
    }
  }
  return union;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** 
 * Returns a new set containing the union of this set and the given `other` set.
 */
public Set<T> union(Set<T> other) { 
  ListSet<T> union = new ListSet<>();
  for (T elem : list) {
      union.list.add(elem);
  }
  for (T elem : other) {
    if (!contains(elem)) {
      union.list.add(elem);
    }
  }
  return union;
}
Remark:

This definition is another great example of encapsulation. The ListSet class exposes only one way for a client to add elements, the add() method. Since the client may attempt to add() any element (perhaps an element already in the set), this method must carry out a (costly) contains() check. In our union() method, we are more than just a client of the ListSet class; we are the class implementer. Therefore, we can access the (private) list field of another ListSet object within this method to perform the add without the check (with the added responsibility of guaranteeing that our short-cut still preserves the class invariant).

The list.add() calls in the first loop each run in \(O(1)\) amortized time for an \(O(N)\) amortized complexity. Then, in each of the \(O(M)\) iterations of the second loop, we perform an \(O(N)\) call to this.contains(). These contains() calls dominate the runtime and result in an overall amortized complexity of \(O(NM)\).

Remark:

Is this really a big improvement? It depends on the relative sizes of \(N\) and \(M\). If \(N\) and \(M\) are on the same order meaning \(N = O(M)\), then both \(O\big( (N+M)^2 \big)\) and \(O(NM)\) simplify to \(O(N^2)\), and the improvement is only in the constants. On the other hand, if \(N\) is significantly larger than \(M\), there can be a notable performance difference. For example, if \(N = M^2\), then \(O\big( (N+M)^2 \big)\) simplifies to \(O(M^4)\) but \(O(NM)\) simplifies to \(O(M^3)\).

We can leverage sorting or an order invariant on the set elements (as in both the SortedListSet and TreeSet), to further improve the performance of union().

Predicates and Restriction

When we restrict a set, we filter out only those elements that satisfy a certain desired property, called a predicate. We end up with a subset, another set in which every element belongs to the original set, but where some elements may be absent.

Definition: Predicate, Restriction, Subset

A predicate is a function that maps elements of a particular type to a boolean value. We say that the elements that are mapped to true satisfy the predicate, while the elements that are mapped to false do not satisfy it.

When we restrict a set on a predicate, we obtain the subset formed from the elements of the set that satisfy the predicate (and excluding all elements that do not satisfy the predicate).

We can model a predicate with a simple functional interface.

Predicate.java

1
2
3
4
5
6
7
@FunctionalInterface
public interface Predicate<T> {
  /**
   * Returns whether the given `elem` satisfies this predicate.
   */
  boolean satisfiedBy(T elem);
}
1
2
3
4
5
6
7
@FunctionalInterface
public interface Predicate<T> {
  /**
   * Returns whether the given `elem` satisfies this predicate.
   */
  boolean satisfiedBy(T elem);
}

We can instantiate this interface with a lambda expression. For example, we can create an isEven predicate over the Integer type by writing:

1
Predicate<Integer> isEven = i -> i % 2 == 0;
1
Predicate<Integer> isEven = i -> i % 2 == 0;

Now, we can add a restrict() method to our ListSet class that takes in a Predicate and builds a new set out of only those elements that satisfy this predicate. We can similarly optimize this method by operating directly on the list field of our new set.

ListSet.java

1
2
3
4
5
/**
 * Returns a new set containing only the elements of this set that satisfy the 
 * given predicate.
 */
public ListSet<T> restrict(Predicate<T> pred) { }
1
2
3
4
5
/**
 * Returns a new set containing only the elements of this set that satisfy the 
 * given predicate.
 */
public ListSet<T> restrict(Predicate<T> pred) { }

Take some time to try completing the definition of this method on your own before looking at our implementation.

restrict() definition

ListSet.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * Returns a new set containing only the elements of this set that satisfy the 
 * given predicate.
 */
public ListSet<T> restrict(Predicate<T> pred) {
  ListSet<T> subset = new ListSet<>();
  for (T elem : list) {
    if (pred.satisfiedBy(elem)) {
      subset.list.add(elem);
    }
  }
  return subset;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * Returns a new set containing only the elements of this set that satisfy the 
 * given predicate.
 */
public ListSet<T> restrict(Predicate<T> pred) {
  ListSet<T> subset = new ListSet<>();
  for (T elem : list) {
    if (pred.satisfiedBy(elem)) {
      subset.list.add(elem);
    }
  }
  return subset;
}

Overall, the runtime of the restrict() method is \(O(N)\) times the complexity of the satisfiedBy() method.

Intersection

Finally, we’ll consider the intersection operation.

Definition: Intersection

Given two sets \(S\) and \(T\), their intersection \(S \cap T\) consists of all elements that belong to both \(S\) and \(T\).

Using the tools that we have already developed (in particular a call to restrict() with a particular Predicate), we can develop a one-line definition of an intersection() method. Take some time to come up with this definition.

intersection() definition

ListSet.java

1
2
3
4
5
6
/**
 * Returns a new set containing the intersection of this set and `other`.
 */
public ListSet<T> intersection(ListSet<T> other) {
  return restrict(other::contains);
}
1
2
3
4
5
6
/**
 * Returns a new set containing the intersection of this set and `other`.
 */
public ListSet<T> intersection(ListSet<T> other) {
  return restrict(other::contains);
}
When we intersect this set with the other set, this is equivalent to restricting this set to only its elements that are contained in other. In the above definition, we use the method reference other::contains, which is a shorthand for the lambda expression elem -> other.contains(elem).

This intersection() definition performs a restriction based on a predicate with an \(O(M)\) time complexity. Thus, it requires \(O(NM)\) time.

Maps

Now, we’ll consider another ADT called a Map (or a Dictionary) that is closely related to a Set. Similar to a set, a Map consists of an unordered collection of distinct items called its keys. However, a Map contains additional information as well. Each key is associated with a value.

Definition: Map, Key, Value, Entry

A Map is an unordered collection of distinct keys of one type that are each associated with one value of another (potentially different) type. We call one associated (key, value) pair an entry in the map.

Typically, the keys will be smaller, simpler objects whose primary purpose is to enable quick access to their entry in the map. The values will often be larger or more complicated objects that model richer information about the entry. For example, in a physical dictionary, an entry consists of a word (its key) and an arbitrarily long description of various aspects of that word (its one or more definitions, parts of speech, example usages, etymology, etc.) that make up its value. When we want to look something up in the dictionary, the alphabetical organization of its keys allows us to quickly locate our desired entry. Once we have located the entry, we interact mainly with its value.

In a dictionary, the keys are used to arrange and look up entries, and the values store the other relevant information once we find the entry.

This (key, value) abstraction provides a convenient way to organize large, complicated data entries so is ubiquitous in many software systems. For example, our course grade book uses Maps to associate each student netID with an inner Map that associates each assignment with a grade. Because of their close connection to spreadsheets and other tabular data, we can visualize Maps using two-column tables, in which the first column contains the keys and the second column contains the values. For example, a nutritional app may create a Map that associates different foods with their calorie count, and we can visualize this map as follows:

Key (String) Value (Integer)
Avocado (2 tbsp) 40
Broccoli (1 cup) 31
Chicken (8 oz) 543
Pineapple (1/2 cup) 40
Swiss Cheese (1/4 cup) 110
â‹® â‹®
Remark:

When first encountering Maps, a common source of confusion is determining which type serves as the key and which type serves as the value. Filling in the following sentences can be helpful:

"In my map, every ( Key ) is paired with exactly one ( Value )." Remember, the keys in a map are distinct, but the values do not need to be (look at the calorie counts of pineapple and avocado in the above table). Here, every food has one calorie count, but not every calorie count belongs to exactly one food, so the foods must be the keys.

"In my map, I want to use a given ( Key ) to look up its ( Value )." Remember that keys are used for searching and values hold additional information. We'll see that the client passes keys into a Map and retrieves values. In this case, the user will enter foods to look up their calorie counts, so the foods must be the keys.

Some cases will be less clear than this example, and still others might require maps in both directions. Whenever you are working with a map but lacking the functionality that you need, it can be helpful to step back and think through these questions to check whether a map is useful in that scenario.

The Map ADT

Our Map interface is the first example that we have seen of using multiple generic types. It will have both a generic key type, K, and a generic value type V.

Map.java

1
2
3
4
/**
 * An unordered collection that associates keys of type K to values of type V.
 */
public interface Map<K, V> { }
1
2
3
4
/**
 * An unordered collection that associates keys of type K to values of type V.
 */
public interface Map<K, V> { }

What operations should our Map support? First, a client should be able to add entries to the map by specifying their key and value. Similarly, a client may wish to update an entry with a given key to have a new value. We will capture both of these behaviors through a single method, put().

Map.java

1
2
3
4
5
6
/**
 * Updates the map to associate the given `value` with the given `key`. This 
 * adds a new entry to the map if `key` was not present in the map and modifies 
 * an existing entry if `key` was present. Requires that `key != null`.
 */
void put(K key, V value);
1
2
3
4
5
6
/**
 * Updates the map to associate the given `value` with the given `key`. This 
 * adds a new entry to the map if `key` was not present in the map and modifies 
 * an existing entry if `key` was present. Requires that `key != null`.
 */
void put(K key, V value);

For most of the other operations, the client will pass only the key to the Map. They’ll need a containsKey() method to check whether a particular key has an associated entry. Once they know that a key is present, they can use this to access its associated value (either without modification using get() or with modification using remove()).

Map.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * Returns whether this map contains an entry with the given `key`. Requires 
 * that `key != null`.
 */
boolean containsKey(K key);

/**
 * Returns the value associated with the given `key`. Requires that 
 * `key != null` and `containsKey(key) == true`.
 */
V get(K key);

/**
 * Returns the value associated with the given `key` and removes this 
 * (key, value) pair from the map. Requires that `key != null` and 
 * `containsKey(key) == true`.
 */
V remove(K key);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * Returns whether this map contains an entry with the given `key`. Requires 
 * that `key != null`.
 */
boolean containsKey(K key);

/**
 * Returns the value associated with the given `key`. Requires that 
 * `key != null` and `containsKey(key) == true`.
 */
V get(K key);

/**
 * Returns the value associated with the given `key` and removes this 
 * (key, value) pair from the map. Requires that `key != null` and 
 * `containsKey(key) == true`.
 */
V remove(K key);

Finally, we’ll want a method to return the size() of the map, the number of (key, value) pairs it stores, as well as a method to efficiently iterate over the entries in a map by obtaining a set of its keys.

Map.java

1
2
3
4
5
6
7
8
9
/**
 * Returns the number of (key, value) pairs stored in this map.
 */
int size();
    
/**
 * Returns the set of keys contained in this map.
 */
Set<K> keySet();
1
2
3
4
5
6
7
8
9
/**
 * Returns the number of (key, value) pairs stored in this map.
 */
int size();
    
/**
 * Returns the set of keys contained in this map.
 */
Set<K> keySet();

Implementing a Map

Central to every Map implementation is an Entry class to represent (key, value) pairs, which we model as a nested record class.

1
2
/** Represents a (key, value) pair in this map */
private record Entry<K, V> (K key, V value) { }
1
2
/** Represents a (key, value) pair in this map */
private record Entry<K, V> (K key, V value) { }

Once we have this Entry class, a Map is essentially a Set of Entrys, so it can be defined using any of the data structures that we considered earlier (a list, a sorted list, a BST, or a hash table that we will discuss in the next lecture). Unfortunately, though, we cannot easily leverage a composition or inheritance relationship to reuse the Set operations in our Map definition. This is because the distinction between Entrys (the type stored in the Map) and their Keys (the type used to traverse the data structure) causes a new level of indirection that our previous code is unequipped to handle. As a result, our Map implementations will follow the same logic as our Set implementations, just with a few small changes to accommodate keys, values, and entries.

We give a complete definition of a ListMap class backed by an unordered list of Entrys below, and leave the other definitions as exercises.

ListMap.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
 * An implementation of the Map interface leveraging composition with an ArrayList.
 */
public class ListMap<K, V> implements Map<K, V> {
  /** Represents a (key, value) pair in this map */
  private record Entry<K, V> (K key, V value) { }

  /** The ArrayList containing the entries of this map. */
  private final ArrayList<Entry<K, V>> list;

  /** Constructs a ListMap with no elements. */
  public ListMap() {
    list = new ArrayList<>();
  }

  /**
   * Returns the Entry with the given `key` in this map, or null if there is 
   * no such entry. Requires that `key != null`.
   */
  private Entry<K, V> find(K key) {
    assert key != null;
    // linear search:
    for (Entry<K, V> entry : list) {
      if (entry.key.equals(key)) {
        return entry;
      }
    }
    return null;
  }

  @Override
  public void put(K key, V value) {
    Entry<K, V> entry = find(key);
    if (entry != null) { // update entry
      entry.value = value;
    } else { // create entry
      list.add(new Entry<>(key, value));
    }
  }

  @Override
  public boolean containsKey(K key) {
    return find(key) != null;
  }

  @Override
  public V get(K key) {
    Entry<K, V> entry = find(key);
    assert entry != null;
    return entry.value;
  }

  @Override
  public int size() {
    return list.size();
  }

  @Override
  public V remove(K key) {
    Entry<K, V> entry = find(key);
    assert entry != null;
    list.remove(entry);
    return entry.value;
  }

  @Override
  public Set<K> keySet() {
    Set<K> keys = new ListSet<>();
    for (Entry<K, V> entry : list) {
      keys.add(entry.key);
    }
    return keys;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
 * An implementation of the Map interface leveraging composition with an ArrayList.
 */
public class ListMap<K, V> implements Map<K, V> {
  /** Represents a (key, value) pair in this map */
  private record Entry<K, V> (K key, V value) { }

  /** The ArrayList containing the entries of this map. */
  private final ArrayList<Entry<K, V>> list;

  /** Constructs a ListMap with no elements. */
  public ListMap() {
    list = new ArrayList<>();
  }

  /**
   * Returns the Entry with the given `key` in this map, or null if there is 
   * no such entry. Requires that `key != null`.
   */
  private Entry<K, V> find(K key) {
    assert key != null;
    // linear search:
    for (Entry<K, V> entry : list) {
      if (entry.key.equals(key)) {
        return entry;
      }
    }
    return null;
  }

  @Override
  public void put(K key, V value) {
    Entry<K, V> entry = find(key);
    if (entry != null) { // update entry
      entry.value = value;
    } else { // create entry
      list.add(new Entry<>(key, value));
    }
  }

  @Override
  public boolean containsKey(K key) {
    return find(key) != null;
  }

  @Override
  public V get(K key) {
    Entry<K, V> entry = find(key);
    assert entry != null;
    return entry.value;
  }

  @Override
  public int size() {
    return list.size();
  }

  @Override
  public V remove(K key) {
    Entry<K, V> entry = find(key);
    assert entry != null;
    list.remove(entry);
    return entry.value;
  }

  @Override
  public Set<K> keySet() {
    Set<K> keys = new ListSet<>();
    for (Entry<K, V> entry : list) {
      keys.add(entry.key);
    }
    return keys;
  }
}

In this implementation, we extract the common subroutine of locating the map entry with the given key into a private find() helper method. This handles one indirection between keys and entries. This method returns a reference to an Entry object, the type stored in the list field, and we can use the reference to modify (in put()), access a value of (in get()), or remove (in remove()) an entry.

A SortedListMap definition follows a very similar design (see Exercise 19.8). A TreeMap definition is more complicated, since we will need to re-implement the BST to incorporate the Entry-Key indirection into its private find() method (see Exercise 19.9).

Dynamic Priority Queues

As a nice application of a Map, we’ll end today’s lecture by enhancing the heap-backed priority queue implementation that we wrote in the previous lecture. In particular, we will make the priority queue dynamic by allowing the client to update the priority of an element while it is queued. This, for example, can model an emergency room patient who experiences new symptoms in the waiting room and must be moved ahead of other patients.

To enable priority updates, we must add a restriction that the elements in the priority queue are distinct (so that we can unambiguously update their priorities). We’ll need to update the spec of our add() method to require that the entry is not already present in the queue. We’ll also add an update() method that allows the client to modify the priority of an element present in the queue.

DynamicPriorityQueue.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * A queue of distinct elements of type T that are removed in priority order. Supports updating the
 * priority of an element while it is in the queue.
 */
public interface DynamicPriorityQueue<T> extends PriorityQueue<T> {
  /**
   * Adds the given `elem` to the queue associated with the given `priority`. 
   * Requires that `elem` is not already present in this priority queue. 
   */
  @Override
  void add(T elem, double priority);

  /**
   * Updates the priority of the given `elem` to the given `newPriority`. 
   * Requires that `elem` is present in this priority queue. 
   */
  void update(T elem, double newPriority);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * A queue of distinct elements of type T that are removed in priority order. Supports updating the
 * priority of an element while it is in the queue.
 */
public interface DynamicPriorityQueue<T> extends PriorityQueue<T> {
  /**
   * Adds the given `elem` to the queue associated with the given `priority`. 
   * Requires that `elem` is not already present in this priority queue. 
   */
  @Override
  void add(T elem, double priority);

  /**
   * Updates the priority of the given `elem` to the given `newPriority`. 
   * Requires that `elem` is present in this priority queue. 
   */
  void update(T elem, double newPriority);
}
Remark:

Some sources prefer to define a single method (similar to put()) that is used both to add new elements to the priority queue and adjust the priorities of existing elements. We chose to use two separate methods so that our DynamicPriorityQueue is a more natural subtype of a PriorityQueue.

Now, we must consider how to implement these new methods. The heap order invariant doesn’t provide an efficient way to search for a particular element. If our target element is not the maximum element, then it can be in either of the heap’s subtrees. Thus, any search may need to visit all entries of the heap. To shortcut this process, we can use a Map to associate the entries with their indices in the heap. In particular, we’ll use Java’s TreeMap, which supports worst-case \(O(\log N)\) put(), get(), containsKey(), and remove() operations. We end up with the following state representation.

MaxHeapDynamicPriorityQueue.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * A dynamic priority queue implementation using composition with a max heap.
 */
public class MaxHeapDynamicPriorityQueue<T> implements DynamicPriorityQueue<T> {

    /**
     * Represents an association of a `priority` to an `elem`. `Entry`s are compared using their
     * priorities.
     */
    private record Entry<T>(T elem, double priority) implements
            Comparable<Entry<T>> {

        @Override
        public int compareTo(Entry<T> other) {
            return (int) Math.signum(priority - other.priority);
        }
    }

    /**
     * The backing heap of this priority queue, represented as an ArrayList. 
     * Entries are ordered according to a level-order traversal of a nearly 
     * complete binary tree and must satisfy the max heap order invariant.
     */
    private final ArrayList<Entry<T>> heap;

    /**
     * Associates the entries of priority queue with their indices in the heap, 
     * such that `heap.get(index.get(k.elem)).equals(k)` for each element k in 
     * this priority queue. Only maps elements that are currently in the queue 
     * (so that `index.size() == heap.size()`).
     */
    private final TreeMap<T, Integer> index;

    // ... methods
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * A dynamic priority queue implementation using composition with a max heap.
 */
public class MaxHeapDynamicPriorityQueue<T> implements DynamicPriorityQueue<T> {

    /**
     * Represents an association of a `priority` to an `elem`. `Entry`s are compared using their
     * priorities.
     */
    private record Entry<T>(T elem, double priority) implements
            Comparable<Entry<T>> {

        @Override
        public int compareTo(Entry<T> other) {
            return (int) Math.signum(priority - other.priority);
        }
    }

    /**
     * The backing heap of this priority queue, represented as an ArrayList. 
     * Entries are ordered according to a level-order traversal of a nearly 
     * complete binary tree and must satisfy the max heap order invariant.
     */
    private final ArrayList<Entry<T>> heap;

    /**
     * Associates the entries of priority queue with their indices in the heap, 
     * such that `heap.get(index.get(k.elem)).equals(k)` for each element k in 
     * this priority queue. Only maps elements that are currently in the queue 
     * (so that `index.size() == heap.size()`).
     */
    private final TreeMap<T, Integer> index;

    // ... methods
}

Note that we cannot leverage our Heap class from the previous lecture, since we will need to rewrite many of the Heap methods to update the index and preserve the class invariant. We leave it as an exercise to complete the definition of this dynamic priority queue and analyze the complexity of its methods (see Exercise 19.10). A dynamic priority queue will play a central role in our implementation of Dijkstra’s shortest path algorithm in a few lectures.

Main Takeaways:

  • A Set is an unordered collection of distinct elements. In its most basic form, the Set interface supports add(), contains() and remove() operations.
  • There are many data structures that can implement a Set. List backed implementations have poor performance due to inefficient searching (when unsorted) or memory shifting (when sorted). Balanced BSTs enable a Set implementation with \(O(\log N)\) operations.
  • Predicates filter the elements of a set, can be instantiated using lambda expressions, and form the basis for many set operations.
  • A Map consists of a collection of (key, value) entries. The keys are unique and are used to navigate the collection. The values store additional information related to the keys.
  • The two primary map operations are put() (to add or modify map entries) and get() (to access the value associated with a key).
  • An index map associating elements with their heap indices can be used to enable updating priorities in a dynamic priority queue.

Exercises

Exercise 19.1: Check Your Understanding
(a)
You add the same sequence of Integers into both a Set<Integer> set and a CS2110List<Integer> list in the same order. Which of the following must be true?
Check Answer
(b)

Suppose we carry out the following operations on a map:

1
2
3
4
5
6
7
Map<String, Integer> map = new ListMap<>();
map.put("x", 1);
map.put("y", 2);
map.put("x", 5);
map.put("z", 3);
map.remove("y");
map.put("y", 7);
1
2
3
4
5
6
7
Map<String, Integer> map = new ListMap<>();
map.put("x", 1);
map.put("y", 2);
map.put("x", 5);
map.put("z", 3);
map.remove("y");
map.put("y", 7);
What is the value of map.get("y")?
Check Answer
(c)
Suppose you have a map m, key k, and value v. Which of the following can you guarantee to be false in m?
Check Answer
Exercise 19.2: Iterating Over Sets
For each of our three Set implementations (ListSet, SortedListSet, and TreeSet):
(a)
Is there a specified order that its Iterator must comply to? If not, is there a natural order that clients would want to have?
(b)
Implement iterator().
Exercise 19.3: Immutable Sets
An immutable set is a set whose contents cannot be changed after it is created. Any updates that are made to this set result in a new ImmutableSet being returned.
1
2
/** An immutable set. */
public class ImmutableSet<T> { ... }
1
2
/** An immutable set. */
public class ImmutableSet<T> { ... }
(a)
Choose a representation for this set. Add the field(s), and document them with specifications.
(b)

Implement the constructor.

1
2
/** Returns a new `ImmutableSet` comprising the distinct elements of `collection`. */
public ImmutableSet(Iterable<T> collection) { ... }
1
2
/** Returns a new `ImmutableSet` comprising the distinct elements of `collection`. */
public ImmutableSet(Iterable<T> collection) { ... }
(c)
Implement contains() and size().
(d)

Implement add() and remove(). Keep in mind that these methods should not modify the backing representation of this.

1
2
3
4
5
6
7
8
/** Returns a new set containing the elements of `this` and `elem`. */
public ImmutableSet<T> add(T elem) { ... }

/** 
 * Returns a new set containing the elements of `this` without `elem`. 
 * Requires `contains(elem) == true`.
 */
public ImmutableSet<T> remove(T elem) { ... }
1
2
3
4
5
6
7
8
/** Returns a new set containing the elements of `this` and `elem`. */
public ImmutableSet<T> add(T elem) { ... }

/** 
 * Returns a new set containing the elements of `this` without `elem`. 
 * Requires `contains(elem) == true`.
 */
public ImmutableSet<T> remove(T elem) { ... }
(e)
What are the runtime complexities of each of these methods?
Exercise 19.4: Improving union()s
For each of the following, consider a revision to the union() method. Implement the changes and update the rest of the method to leverage them. State the new runtime complexity.
(a)
In ListSet.union(), we begin by sorting this.list.
(b)
Drawing inspiration from the merge() method of merge sort, we merge the two lists together in SortedListSet.union(). Instead of adding duplicate elements to the resulting list, we’ll discard them.
(c)
In TreeSet.union(), first clone other using a pre-order iterator. Then use simultaneous in-order traversals of both the clone and this BST to add additional elements to the clone so it contains the union of both original BSTs.
Exercise 19.5: Writing Predicates
For each of the following, use a lambda expression to initialize a Predicate.
(a)
If a String is a palindrome.
(b)
If a Point (refer to Lecture 11) is in quadrants 2 or 4 and not on any axes.
(c)
If every element of CS2110List<T> satisfies a different predicate Predicate<T> check.
Exercise 19.6: Set Differences
Given two sets \(A\) and \(B\), their set difference, denoted \(A\setminus B\), consists of all the elements in \(A\) which are not in \(B\). Implement the following methods using restrict.
(a)
1
2
/** Computes a \ b. */
static <T> Set<T> setDifference(Set<T> a, Set<T> b) { ... }
1
2
/** Computes a \ b. */
static <T> Set<T> setDifference(Set<T> a, Set<T> b) { ... }
(b)

The symmetric difference of two sets \(A\) and \(B\), denoted \(A \triangle B\) consists of all the elements in \(A \cup B\) that are not in \(A \cap B\).

1
2
/** Computes the symmetric difference of a and b. */
static <T> Set<T> symmetricDifference(Set<T> a, Set<T> b) { ... }
1
2
/** Computes the symmetric difference of a and b. */
static <T> Set<T> symmetricDifference(Set<T> a, Set<T> b) { ... }
Exercise 19.7: Filtering on Two Predicates
Given two predicates p1 and p2, apply these on a set s using the following ways.
(a)
Chain two calls to restrict().
(b)
Write a compound predicate; that is, call restrict() one time on a lambda expression that combines p1 and p2.
(c)
Using restrict() and intersection().
Exercise 19.8: SortedListMap
Similar to the Set ADT, a Map can be implemented with a sorted list.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/** A map implemented with a sorted list. */
public class SortedListMap<K, V> implements Map<K, V> {
  private static class Entry<K, V> { ... }

  /** The list containing the key-value pairs sorted in increasing order by key. */
  private ArrayList<Entry<K, V>> list;

  /** The number of keys in this map. */
  private int size;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/** A map implemented with a sorted list. */
public class SortedListMap<K, V> implements Map<K, V> {
  private static class Entry<K, V> { ... }

  /** The list containing the key-value pairs sorted in increasing order by key. */
  private ArrayList<Entry<K, V>> list;

  /** The number of keys in this map. */
  private int size;
}
(a)

Write a helper method findKey() that returns the index of a given key or -1 if not in the list. This should run in \(O(\log N)\) time.

1
2
/** Returns the index of `key` or -1 if not in `list`. */
private int findKey(K key) { ... }
1
2
/** Returns the index of `key` or -1 if not in `list`. */
private int findKey(K key) { ... }
(b)
Use findKey() to implement containsKey() and get().
(c)
Implement put() and remove(). Keep in mind the invariant of list.
(d)
Implement keySet().
(e)
State the runtime complexities of each method in terms of \(N = \) size.
Exercise 19.9: TreeMap
A map can also be implemented with a BST. Add any fields, constructors, and methods to implement the class. State the runtime complexity of each method.
1
2
3
4
5
6
7
8
9
/** A map implemented with a binary search tree. */
public class TreeMap<K, V> implements Map<K, V> {
  private static class Entry<K, V> { ... }

  /**
   * The BST containing the key-value pairs of this set, comparing on the key.
   */
  private BST<Entry<K, V>> tree;
}
1
2
3
4
5
6
7
8
9
/** A map implemented with a binary search tree. */
public class TreeMap<K, V> implements Map<K, V> {
  private static class Entry<K, V> { ... }

  /**
   * The BST containing the key-value pairs of this set, comparing on the key.
   */
  private BST<Entry<K, V>> tree;
}
Exercise 19.10: MaxHeapDynamicPriorityQueue
Let's flesh out the implementation of MaxHeapDynamicPriorityQueue. Recall that this data structure supports changing the priority of elements within a max heap. It maintains a max heap and a map associating each heap element with its index in the list.
(a)

Trace the state of the map and heap of a MaxHeapDynamicPriorityQueue<Character> pq after each line is executed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pq.add('A', 5);
pq.add('B', 2);
pq.add('C', 8);
pq.add('D', 1);
pq.add('E', 9);
pq.peek();
pq.remove();
pq.insert('F', 7);
pq.update('A', 10);
pq.remove();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pq.add('A', 5);
pq.add('B', 2);
pq.add('C', 8);
pq.add('D', 1);
pq.add('E', 9);
pq.peek();
pq.remove();
pq.insert('F', 7);
pq.update('A', 10);
pq.remove();
(b)
Review the class invariant. Write an assertInv() method.
(c)

In our implementation of a max heap, we had a helper method swap() to swap two indices in the backing list. Now that we have another field that must be consistent with the list, the map must also be updated on a swap.

1
2
3
4
5
/**
 * Swaps elements at indices `i` and `j` in the backing list.
 * Requires `0 <= i, j < heap.size()`.
 */
private void swap(int i, int j) { ... }
1
2
3
4
5
/**
 * Swaps elements at indices `i` and `j` in the backing list.
 * Requires `0 <= i, j < heap.size()`.
 */
private void swap(int i, int j) { ... }
(d)
Update add() and remove() to re-satisfy the class invariant of index.
(e)
Implement update().
(f)
State the runtime complexities of each method in this class.