19. Sets and Maps
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
20. Hashing
21. Graphs

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

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 in which 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, which we delegate to the find() helper method. Our implementation is shown below.

ListSet.java

The size() and iterator() methods inherit the \(O(1)\) performance of their respective ArrayList counterparts. The runtimes of add(), contains(), and remove() are all bounded by the runtime of find(), 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

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. Try it in the editor below. The rest of the SortedListSet is provided for you, so you can press Run to check your work against the Set tests (which also check that your search takes only \(O(\log N)\) comparisons).

SortedListSet.java

SortedListSet.java

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

SortedListSet.java

SortedListSet.java

SortedListSet.java

SortedListSet.java

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, linearly-ordered 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.

TreeSet.java

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.

To see this in action, the client below adds the same elements (including some repeats) to a ListSet, a SortedListSet, and a TreeSet, using only the Set interface, and then iterates over each. Press Run and compare the iteration orders. Why do the sorted implementations print their elements in increasing order, while the ListSet does not?

SetImplDemo.java

A Set definition backed by a balanced BST 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

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

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 a runtime bounded by the \(N+M\) contains() calls, which will have a 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 they 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 be an earlier-added element of other since other has distinct elements. Overall, these modifications lead to the following definition.

ListSet.java

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 shortcut 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

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).

Java provides a functional interface to model a Predicate.

Predicate.java

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

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.

Take some time to try completing the definition of this method on your own in the editor below before looking at our implementation. The rest of the ListSet is provided for you, and pressing Run checks your work against a set of tests.

ListSet.java

ListSet.java

Overall, the runtime of the restrict() method is \(O(N)\) times the complexity of the test() 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 in the editor below. Here, restrict() is already provided.

ListSet.java

ListSet.java

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.

Putting these operations together, the following client builds two sets and combines them with union(), restrict(), and intersection(). Press Run to see the results.

SetDemo.java

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 it is ubiquitous in many software systems. For example, our course grade book uses Maps to associate each student’s 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

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

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

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

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.

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

In this implementation, we extract the common subroutine of locating the index of an entry with the given key into a private find() helper method. This handles one indirection between keys and entries. This method returns an index, which we can use in conjunction with the ArrayList’s get(), set(), and remove() methods to access and/or modify entries of the map.

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).

Here is a client that uses a Map to associate each scientist’s name (the key) with their birthday (the value). Notice that it is written entirely against the Map interface, so it works with any of our implementations. Press Run to see it work.

MapDemo.java

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

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. 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.
  • Many data structures 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?
(b)
Which of the following implementations would be the most suitable for modeling a set of Set<T>s?
(c)
Recall the improved implementation of ListSet.union() with runtime \(O(MN)\). With SortedListSet and TreeSet, we can take advantage of the sorted invariant in our optimal implementation of union(). Let \(N=\) this.size() and \(M=\) other.size(). What is the asymptotic runtime complexity of the optimal implementation for both SortedListSet and TreeSet?
$$O(M+N)$$
(d)
Given two sets s and t, you observe that s.intersection(t).size() == 0. Which of the following must be true?
(e)
Suppose we carry out the following operations on a map:
What is the value of map.get("y")?
(f)
Suppose you have a map m, key k, and value v. Which of the following can you guarantee to be false in m?
(g)
Why is the index map crucial for supporting efficient update()s?
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 with?
If not, is there a natural order that clients might 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.
(a)
Choose a representation for this set. Add the fields and document them with specifications.
(b)
Implement the constructor.
(c)
Implement contains() and size().
(d)
Implement add() and remove(). Keep in mind that these methods should not modify the backing representation of this.
(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(), begin by sorting this.list.
(b)
Drawing inspiration from the merge() method of merge sort, we merge the two lists in SortedListSet.union(). Instead of adding duplicate elements to the resulting list, we'll discard them. Your method should run in \(O(N+M)\) time.

Note: We've restricted the argument to be a SortedListSet so we can take advantage of its sorted order to do this fast merge.

SortedListSet.java

(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.
Recall that a Point has x() and y() accessors.
(c)
If every element of CS2110List<T> satisfies another Predicate<T> check.

Predicates.java

Exercise 19.6: Set Differences
Implement the following set operations.
(a)
Given two sets \(A\) and \(B\), their set difference, denoted \(A\setminus B\), consists of all the elements in \(A\) that are not in \(B\).

SetOps.java

(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\).

SetOps.java

Exercise 19.7: Filtering on Two Predicates
Given two Predicates p1 and p2 and a Set s, compute the subset of s consisting of all elements that satisfy both p1 and p2 in the three following ways.
(a)
Chain two calls to restrict().
s.restrict(p1).restrict(p2);
(b)
Write a compound predicate; that is, call restrict() one time on a lambda expression that combines p1 and p2.
s.restrict((t) -> p1.test(t) && p2.test(t));
(c)
Using restrict() and intersection().
s.restrict(p1).intersection(s.restrict(p2));
Exercise 19.8: SortedListMap
Similar to the Set ADT, a Map can be implemented with a sorted list. We have set up the fields for you (the keys must be Comparable so the entries can be sorted by key).
(a)
Complete the methods in the editor below, using the parts that follow as a guide.

SortedListMap.java

(b)
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.
Exercise 19.10: MaxHeapDynamicPriorityQueue
Let's flesh out the implementation of a DynamicPriorityQueue described in Section 19.3.3. 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 DynamicPriorityQueue<Character> pq after each line is executed.
(b)
Review the class invariant. Write an invariantSatisfied() 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.
(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.

Exercise 19.11: Sets as Relations
A Set<SamePair<T>> s (see Exercise 12.2 for a definition of SamePair) can be viewed as a binary relation on some Set<T>. An element in the set x means that x.first() relates to x.second(). Write methods to verify each of the following properties of a binary relation.
(a)
A binary relation is symmetric if, whenever \((a, b)\) is in \(s\), \((b, a)\) is also in \(s\). (In the editors below, SamePair is provided and also defines equals(), so contains() works on pairs.)

Relations.java

(b)
A binary relation is anti-symmetric if, whenever \((a, b)\) is in \(s\) and \((b, a)\) is in \(s\), then \(a=b\).

Relations.java

(c)
A binary relation is transitive if, whenever \((a, b)\) is in \(s\) and \((b, c)\) is in \(s\), then \((a, c)\) is also in \(s\).

Relations.java

Exercise 19.12: Maps as Functions
A map can be viewed as a function from keys to values. Given sets \(X, Y\), a well-defined function \(f: X \to Y\) assigns every element of the domain \(X\) to exactly one element in the codomain \(Y\).
(a)
Is our Map ADT guaranteed to be a well-defined function on the domain keySet()? If not, how many entries in our Map ADT must you inspect to confirm whether it is?

A function is injective if each element in the domain maps to a distinct value in the codomain; i.e., for all pairs of domain elements \(x_1, x_2 \in X\), if \(x_1 \neq x_2\), then \(f(x_1) \neq f(x_2)\).
(b)
Suppose that map represents a well-defined function. Implement isInjective().

Functions.java


A function is surjective if there is some element of the domain that maps to each element of the codomain; i.e., for all \(y \in Y\), there is some \(x\in X\) such that \(f(x)=y\).
(c)
Given a codomain, verify whether a Map is surjective.

Functions.java