Module 6: Sets and lists


Supplemental material


Set operations: intersection, union, difference

What is a set?

List data structures as sets:

Here's the example:
// We need to import the class LinkedList from here:
import java.util.*;

public class SetExample {

    public static void main (String[] argv)
    {
	// Create an instance of the data structure.
	LinkedList<String> favoriteShows1 = new LinkedList<String>();

	// Put some elements in so that it becomes a set of strings.
	favoriteShows1.add ("Yes minister");
	favoriteShows1.add ("Seinfeld");
	favoriteShows1.add ("Cheers");
	favoriteShows1.add ("Frasier");
	favoriteShows1.add ("Simpsons");

	// Create a second instance and add some elements.
	LinkedList<String> favoriteShows2 = new LinkedList<String>();
	favoriteShows2.add ("Mad about you");
	favoriteShows2.add ("Seinfeld");
	favoriteShows2.add ("Frasier");
	favoriteShows2.add ("Cosby show");

	// Compute set intersection and the difference favoriteShows1-favoriteShows2 in separate methods:
	computeIntersection (favoriteShows1, favoriteShows2);

	computeDifference (favoriteShows1, favoriteShows2);
    }


    static void computeIntersection (LinkedList<String> listA, LinkedList<String> listB)
    {
	System.out.println ("Intersection:");
	for (int i=0; i < listA.size(); i++) {
	    String s = listA.get(i);
	    // To be in the intersection, s needs to be in both sets.
	    if ( listB.contains(s) ) {
		System.out.println ("  " + s);
	    }
	}
    }


    static void computeDifference (LinkedList<String> listA, LinkedList<String> listB)
    {
	System.out.println ("Difference: ");
	for (int i=0; i < listA.size(); i++) {
	    String s = listA.get(i);
	    // If s is not in B, it's in the difference.
	    if ( ! listB.contains(s) ) {
		System.out.println ("  " + s);
	    }
	}
    }

}
Note:

In-Class Exercise 1: Download this program and implement union.

A similar example with integers:

import java.util.*;

public class SetExample3 {

    public static void main (String[] argv)
    {
	// Create an instance of a linked list and add some data.
	LinkedList<Integer> oddGuys = new LinkedList<Integer>();
	oddGuys.add (1);
	oddGuys.add (3);
	oddGuys.add (5);
	oddGuys.add (7);
	oddGuys.add (9);

	// Another set.
	LinkedList<Integer> primes = new LinkedList<Integer>();
	primes.add (1);
	primes.add (2);
	primes.add (3);
	primes.add (5);
	primes.add (7);

	// Set intersection and difference.
	computeIntersection (oddGuys, primes);
	computeDifference (oddGuys, primes);
    }


    static void computeIntersection (LinkedList<Integer> listA, LinkedList<Integer> listB)
    {
	System.out.println ("Intersection:");
	for (int i=0; i<listA.size(); i++) {
	    Integer K = listA.get(i);
	    // To be in the intersection, K needs to be in both sets.
	    if ( listB.contains(K) ) {
		System.out.println ("  " + K);
	    }
	}
    }


    static void computeDifference (LinkedList<Integer> listA, LinkedList<Integer> listB)
    {
	System.out.println ("Difference: ");
	for (int i=0; i<listA.size(); i++) {
	    Integer K = listA.get(i);
	    // If K is not in B, it's in the difference.
	    if ( ! listB.contains(K) ) {
		System.out.println ("  " + K);
	    }
	}
    }

}

We'll modify the Integer example to actually compute the intersection and difference sets instead of printing them:

import java.util.*;

public class SetExample4 {

    public static void main (String[] argv)
    {
	// Create an instance of a linked list and add some data.
	LinkedList<Integer> oddGuys = new LinkedList<Integer>();
	oddGuys.add (1);
	oddGuys.add (3);
	oddGuys.add (5);
	oddGuys.add (7);
	oddGuys.add (9);

	// Another set.
	LinkedList<Integer> primes = new LinkedList<Integer>();
	primes.add (1);
	primes.add (2);
	primes.add (3);
	primes.add (5);
	primes.add (7);

	// Set intersection and difference.
	LinkedList<Integer> intersection = computeIntersection (oddGuys, primes);
	// Note use of toString() in LinkedList.
	System.out.println ("Intersection: " + intersection);

	LinkedList<Integer> difference = computeDifference (oddGuys, primes);
	System.out.println ("Difference: " + difference);
    }


    static LinkedList<Integer> computeIntersection (LinkedList<Integer> listA, LinkedList<Integer> listB)
    {
	LinkedList<Integer> listC = new LinkedList<Integer>();
	for (int i=0; i<listA.size(); i++) {
	    Integer K = listA.get(i);
	    // To be in the intersection, K needs to be in both sets.
	    if ( listB.contains(K) ) {
		listC.add (K);
	    }
	}
	return listC;
    }


    static LinkedList<Integer> computeDifference (LinkedList<Integer> listA, LinkedList<Integer> listB)
    {
	LinkedList<Integer> listC = new LinkedList<Integer>();
	for (int i=0; i<listA.size(); i++) {
	    Integer K = listA.get(i);
	    // If K is not in B, it's in the difference.
	    if ( ! listB.contains(K) ) {
		listC.add (K);
	    }
	}
	return listC;
    }

}
Note:

In-Class Exercise 2: Download this program and implement union so that it returns a list containing the union.

In-Class Exercise 3: Download SetExample6.java, DataTool.java, DataSet.java, and the text file datafortwo. Then, do the following:


Our own very list data structure

Recall the methods that we used in the LinkedList class:

Thus, if we were to create our own data structure to do this, it might look something like:

public class OurList {

    public void add (String s) 
    {
        // ...
    }

    public int size ()
    {
        // ...
    }

    public String get (int i)
    {
        // ...
    }

    // ... contains() method ...

}

In-Class Exercise 4: What is the signature of the contains method that should be in the template above?

We will implement such a data structure using arrays:

Here's the program:
public class OurListUsingArrays {

    // This is the array in which we'll store strings.
    String[] strings = new String [100];;

    // Initially, there are none.
    int numStrings = 0;


    public void add (String s)
    {
	if (numStrings < 100) {
	    strings[numStrings] = s;
	    numStrings ++;
	}
    }


    public int size ()
    {
	return numStrings;
    }


    public String get (int i)
    {
	return strings[i];
    }


    public boolean contains (String s)
    {
	// Note: we need to use numStrings instead of strings.length
	for (int i=0; i < numStrings; i++) {
	    if ( strings[i].equalsIgnoreCase(s) ) {
		return true;
	    }
	}
	return false;
    }

}

Note:

In-Class Exercise 5: Download OurListUsingArrays.java and modify SetExample8.java to compute unions.

About our implementation:


Our own linked list

A linked list solves the "unknown size" problem:

To see how a linked list works, we'll first examine this piece of code:

// We'll use instances of this object in main() below.

class ListItem {

    String data;    
    ListItem next;

}


public class StrangeExample {

    public static void main (String[] argv)
    {
	// Make one instance and put a string in the data field.
	ListItem first = new ListItem();
	first.data = "Yes minister";

	// Make a second one.
	ListItem second = new ListItem();
	second.data = "Seinfeld";

	// Now link them: make the first one point to the second.
	first.next = second;

	// Make a third and make the second point to the third.
	ListItem third = new ListItem();
	third.data = "Cheers";
	second.next = third;

	// Make a fourth etc.
	ListItem fourth = new ListItem();
	fourth.data = "Frasier";
	third.next = fourth;

	ListItem last = new ListItem();
	last.data = "Simpsons";
	fourth.next = last;

	// Now print. Note the extensive use of the dot-operator.
	System.out.println ("First: " + first.data);
	System.out.println ("Second: " + first.next.data);
	System.out.println ("Third: " + first.next.next.data);
	System.out.println ("Fourth: " + first.next.next.next.data);
	System.out.println ("Last: " + first.next.next.next.next.data);
	System.out.println ("Last (alt): " + last.data);
    }

}

In-Class Exercise 6: Draw the memory picture after each new ListItem has been created.

Instead of using the dot-operator in sequence, we'll use a different approach:

  • We will track the last item in the list using a pointer.

  • When a new item is to be added, we add that to the end
         ⇒ We'll use the "last" pointer.
Here's the program:
class ListItem {

    String data;    
    ListItem next;

}


public class StrangeExample2 {

    public static void main (String[] argv)
    {
	// Make one instance and put a string in the data field.
	ListItem first = new ListItem();
	first.data = "Yes minister";
        
        // This is also the last one, right now.
        ListItem last = first;

	// Make a second one.
	ListItem nextOne = new ListItem();
	nextOne.data = "Seinfeld";

	// Now link them: make the first one point to the second.
	last.next = nextOne;

        // Advance the "last" pointer.
        last = nextOne;

	// Make a third and make the second point to the third.
	nextOne = new ListItem();
	nextOne.data = "Cheers";
	last.next = nextOne;
        last = nextOne;


	// Make a fourth etc.
	nextOne = new ListItem();
	nextOne.data = "Frasier";
	last.next = nextOne;
        last = nextOne;

        // Last one.
	nextOne = new ListItem();
	nextOne.data = "Simpsons";
	last.next = nextOne;
        last = nextOne;


	// Now print by repeatedly advancing the pointer.
        ListItem listPointer = first;
	System.out.println ("First: " + listPointer.data);

        listPointer = listPointer.next;
	System.out.println ("Second: " + listPointer.data);

        listPointer = listPointer.next;
	System.out.println ("Third: " + listPointer.data);

        listPointer = listPointer.next;
	System.out.println ("Fourth: " + listPointer.data);

        listPointer = listPointer.next;
	System.out.println ("Last: " + listPointer.data);
    }

}
Note:
  • It's easier to first understand the printing:
    • Notice how the pointer variable listPointer starts by pointing to the same thing that first is pointing to?
    • Then observe that listPointer advances along to the second item, then the third etc.

  • Similarly, earlier in the program, the nextOne variable (a pointer, since it's an object variable) is made to point to a new object instance each time.

  • Each time a new item is added to the end, the last pointer is advanced along to point to the last one.

The repetition in printing suggests a loop:

class ListItem {

    String data;    
    ListItem next;

}


public class StrangeExample3 {

    public static void main (String[] argv)
    {
	// Make one instance and put a string in the data field.
	ListItem first = new ListItem();
	first.data = "Yes minister";
        ListItem last = first;

	// Make a second one.
	ListItem nextOne = new ListItem();
	nextOne.data = "Seinfeld";
	last.next = nextOne;
        last = nextOne;

	// Make a third and make the second point to the third.
	nextOne = new ListItem();
	nextOne.data = "Cheers";
	last.next = nextOne;
        last = nextOne;


	// Make a fourth etc.
	nextOne = new ListItem();
	nextOne.data = "Frasier";
	last.next = nextOne;
        last = nextOne;

        // Last one.
	nextOne = new ListItem();
	nextOne.data = "Simpsons";
	last.next = nextOne;
        last = nextOne;


	// Now print by repeatedly advancing the pointer - in a simple loop.
        ListItem listPointer = first;
        while (listPointer != null) {
            System.out.println (listPointer.data);
            listPointer = listPointer.next;
        }
    }

}
Note:
  • Once again, the variable listPointer now moves along the list, at each step pointing at the next item in the list.

  • Notice the loop termination condition:
            while (listPointer != null) {
                // ...
            }
        
    • When the last item is added, its .next field contains null (because it points to nothing).
    • Thus, we know we've reached the end when we're pointing to null.

In-Class Exercise 7: Download StrangeExample3.java and add the line

  System.out.println (listPointer);
  
inside the while-loop. Now draw the list with actual memory addresses and show how the variable listPointer advances along the list.

Can the code for adding new items also be compacted?
     ⇒ Yes, indeed:


class ListItem {

    String data;    
    ListItem next;

}


public class StrangeExample4 {

    static ListItem first = null;
    static ListItem last = null;

    public static void main (String[] argv)
    {
	// Make one instance and put a string in the data field.
	first = new ListItem();
	first.data = "Yes minister";
        last = first;

	// Add the rest.
        add ("Seinfeld");
        add ("Cheers");
        add ("Frasier");
        add ("Simpsons");

	// Now print by repeatedly advancing the pointer - in a simple loop.
        ListItem listPointer = first;
        while (listPointer != null) {
            System.out.println (listPointer.data);
            listPointer = listPointer.next;
        }
    }


    static void add (String s)
    {
        // Make a new instance (new list node) and add the data.
	ListItem nextOne = new ListItem();
	nextOne.data = s;

        // Make the current last one point to the new one.
	last.next = nextOne;

        // Adjust the last pointer.
        last = nextOne;
    }
    

}

We're now in position to create our own linked-list:

  • We will put all the code in a class called ListWithLinks (to resemble Java's LinkedList class).

  • We will name the methods add(), size(), get() and contains().
Here's the program:

class ListItem {

    String data;
    ListItem next;

}


public class ListWithLinks {

    // Instance variables.
    ListItem front = null;
    ListItem rear = null;

    // To keep track of the size.
    int numItems = 0;


    public void add (String s)
    {
	if (front == null) {
            // The special case of an empty list needs to be handled differently.
	    front = new ListItem ();
	    front.data = s;
	    rear = front;
	    rear.next = null;
	}
	else {
            // Just like before:
            ListItem nextOne = new ListItem ();
	    nextOne.data = s;
	    rear.next = nextOne;
	    rear = nextOne;
	}    

	numItems ++;
    }


    public int size ()
    {
	return numItems;
    }

    
    public String get (int i)
    {
        // Sanity check:
	if (i >= numItems) {
	    return null;
	}

        // Otherwise, count up to the i-th item.
	int count = 0;
	ListItem listPtr = front;

	while (count < i) {
	    listPtr = listPtr.next;
	    count ++;
	}

	return listPtr.data;
    }


    public boolean contains (String s)
    {
        // Sanity check.
	if (front == null) {
	    return false;
	}

        // Start from the front and walk down the list. If it's there,
        // we'll be able to return true from inside the loop.
	ListItem listPtr = front;
	while (listPtr != null) {
	    if ( listPtr.data.equals(s) ) {
		return true;
	    }
	    listPtr = listPtr.next;
	}
	return false;
    }

}

This class itself will be used in "main" (or elsewhere) as a data structure:

public class ListWithLinksExample {

    public static void main (String[] argv)
    {
	ListWithLinks favoriteShows1 = new ListWithLinks();
	favoriteShows1.add ("Yes minister");
	favoriteShows1.add ("Seinfeld");
	favoriteShows1.add ("Cheers");
	favoriteShows1.add ("Frasier");
	favoriteShows1.add ("Simpsons");

	ListWithLinks favoriteShows2 = new ListWithLinks();
	favoriteShows2.add ("Mad about you");
	favoriteShows2.add ("Seinfeld");
	favoriteShows2.add ("Frasier");
	favoriteShows2.add ("Cosby");

	computeIntersection (favoriteShows1, favoriteShows2);
    }


    static void computeIntersection (ListWithLinks listA, ListWithLinks listB)
    {
	System.out.println ("Intersection:");
	for (int i=0; i < listA.size(); i++) {    // Calls the size() method in ListWithLinks
	    String s = listA.get(i);              // Calls the get() method
	    if ( listB.contains(s) ) {            // The contains() method
		System.out.println ("  " + s);
	    }
	}
    }

}
Note:
  • Some nomenclature: each individual ListItem instance in the list is called a node of the list.

  • We've changed the names first and last to the more traditional names of front and rear.

In-Class Exercise 8: Download ListWithLinks2.java and ListWithLinksExample2.java and implement the printList() method in ListWithLinks2 to print out the list. The code in ListWithLinksExample2 calls this method.

Conceptual view of a linked-list:

  • A simple conceptual view:

  • With a little more detail (actual addresses):

  • Suppose this were a list of integers, containing the first four odd numbers:

    • Notice that the view is conceptual: the actual addresses do not necessarily correspond to visual order.
    • "front" and "rear" are variables that may be in other objects.

  • If we were to add the data "9" to this list:


Some enhancements

We will add a couple more methods ("features") to our list:

  • A toString() method.

  • A way to print the list with memory addresses of the nodes.

  • We'll change the name of the class to the more traditional OurLinkedList, but different from Java's LinkedList class.
Here's the program:

class ListItem {

    // ... 

}


public class OurLinkedList {

    // ... same as before ...

    public void add (String s)
    {
        // ...
    }


    public int size ()
    {
        // ...

    }

    
    public String get (int i)
    {
        // ...
    }


    public boolean contains (String s)
    {
        // ...
    }


    public String toString ()
    {
	if (front == null) {
	    return "empty";
	}

        // Put all the elements (data only) into the string.
	String s = "[";
	ListItem listPtr = front;
	while (listPtr != null) {
	    s += " \"" + listPtr.data + "\"";
	    listPtr = listPtr.next;
	}
	return s + "]";
    }


    public void printWithAddresses ()
    {
	if (front == null) {
	    return;
	}

	ListItem listPtr = front;
	while (listPtr != null) {
            //  listPtr's default toString() prints out the memory address.
	    System.out.println (" \"" + listPtr.data + "\"  at address " + listPtr);
	    listPtr = listPtr.next;
	}
    }

}
Note:
  • The toString() method must return a String.
         ⇒ We build the desired output into the String and return it.

  • For printing addressees, we have exploited the fact that when an object without a toString() method is printed, the address is printed.
    • ListItem doesn't have toString().
    • Thus, printing any instance of ListItem displays the address of that instance.

In-Class Exercise 9: Add a toString() method to ListItem above, and see what happens.


Doubly-linked lists

About the linked-list we've seen so far:

  • We call it a singly-linked list (to contrast it with the doubly-linked list we will see next).

  • Deletion is a little difficult in a singly-linked list, easier in a doubly-linked list.

  • A doubly-linked list allows traversal in any direction.

  • A singly-linked list allows traversal in only the "forward" (front to rear) direction.

  • One implication for the singly-linked list is this:
    • Consider this picture:

    • And this code:
             public class SinglyLinkedListProblem {
      
                 public static void main (String[] argv)
                 {
                     // Some list code:
                     ListItem front = new ListItem ();
                     front.next = new ListItem ();
                     rear = front.next;
                     
                     // Given rear, can one print the element before it?
                     printPrevNode (rear);
                 }
      
                 static void printPrevNode (ListItem listPtr)
                 {
                     // How do we go to whichever node comes before listPtr?
                 }
             }
             

In a doubly-linked list:

  • The conceptual view is:

    • Each node points both to the next one in the list and the previous one.
    • Except for the first node, whose "backpointer" points to null, and the last node, whose next pointer points to null.

  • Let us now add the additional pointer to the class that used for each node:
    class ListItem {
    
        String data;
        ListItem next;    // To point to next node in list.
        ListItem prev;    // To point to the previous node in the list.
    
    }
        

  • We'll have to set the prev pointer carefully when we add a new element.
Here's the program:

class ListItem {

    String data;
    ListItem next;    // To point to next node in list.
    ListItem prev;    // To point to the previous node in the list.

}


public class DoublyLinkedList {

    // Instance variables.
    ListItem front = null;
    ListItem rear = null;

    int numItems = 0;

    public void add (String s)
    {
	if (front == null) {
            // Similar to singly-linked list, except for setting rear.prev
	    front = new ListItem ();
	    front.data = s;
	    rear = front;
	    rear.next = null;
	    rear.prev = null;           // Must set this correctly.
	}
	else {
            // Make new ListItem and set its fields correctly.
            ListItem nextOne = new ListItem ();
	    nextOne.data = s;
	    nextOne.next = null;
	    nextOne.prev = rear;

            // Adjust the next pointer of the current last one, and adjust rear itself.
	    rear.next = nextOne;
	    rear = nextOne;
	}    

	numItems ++;
    }


    public int size ()
    {
    	// ...
    }

    
    public String get (int i)
    {
    	// ... same as in singly-linked list ...
    }


    public boolean contains (String s)
    {
    	// ... same as in singly-linked list ...
    }

    public String toString ()
    {
        // ... 
    }
}
Note:
  • The size(), get() and contains() methods are the same as in a singly-linked list because those don't need the prev pointer.

In-Class Exercise 10: Download DoublyLinkedList2.java and DoublyLinkedListExample2.java and implement a method to print the list in reverse (starting from the rear).

Next, let's examine what needs to change to build a doubly-linked list that can store int's.

In-Class Exercise 11: Download DoublyLinkedIntList.java and DoublyLinkedListExample3.java and implement a doubly-linked list to hold integers. You can copy over the code in DoublyLinkedList above into your DoublyLinkedIntList to begin with, and then change methods/data etc so that the list stores int's.


Deletion

There are many ways by which we might want to delete a particular element:

  • For example, we might want do it by identifying the element directly:
    	DoublyLinkedList favoriteShows = new DoublyLinkedList();
    	favoriteShows.add ("Crocodile Hunter");
    
            // ... add more stuff ...
    
            favoriteShows.delete ("Crocodile Hunter");
        

  • Alternatively, we might want to delete by order of occurence in list:
    	DoublyLinkedList favoriteShows = new DoublyLinkedList();
    	favoriteShows.add ("Crocodile Hunter");
    
            // ... add more stuff ...
    
            favoriteShows.delete (0);     // Delete 0-th element.
        

  • What needs to happen in a deletion?
    • We need to find the node in question.
    • For example, suppose we want to delete the third node.

    • The "gap" after removal needs to be "stitched together" by adjusting links.

Here's the program:

class ListItem {

   // ...

}


public class DoublyLinkedList4 {

    // ...

    public void add (String s)
    {
        // ...
    }


    public int size ()
    {
        // ...
    }

    
    public String get (int i)
    {
        // ...
    }


    public boolean contains (String s)
    {
        // ...
    }


    public String toString ()
    {
        // ...
    }



    // Find String s and delete it if it occurs in the list.

    public void delete (String s)
    {
	ListItem listPtr = front;
	while ( (listPtr != null) && (! listPtr.data.equals(s)) ) {
	    listPtr = listPtr.next;
	}

        // If it's not there, return.
	if (listPtr == null) {
	    return;
	}

	// Otherwise delete: four cases.
        if (front == rear) {
            // Case 1: only one element.
            front = rear = null;
        }
	else if (listPtr == front) {
            // Case 2: we're deleting from the front.
	    front = listPtr.next;
            front.prev = null;
	}
	else if (listPtr == rear) {
            // Case 3: delete the last element.
	    rear = listPtr.prev;
            rear.next = null;
	}
	else {
	    // Case 4: In the middle: stitch the prev and next nodes together.
	    listPtr.prev.next = listPtr.next;
	    listPtr.next.prev = listPtr.prev;
	}

	numItems --;
    }



    // Delete the element at a particular position in the list

    public void delete (int i)
    {
        // Check for bad input.
	if ( (i < 0) || (i >= numItems) ) {
	    return;
	}

        // Find the i-th element.
	int count = 0;
	ListItem listPtr = front;
	while ( (listPtr != null) && (count != i) ) {
	    listPtr = listPtr.next;
	    count ++;
	}

	// Otherwise delete: four cases.
        if (front == rear) {
            // Case 1: only one element.
            front = rear = null;
        }
	else if (listPtr == front) {
            // Case 2: we're deleting from the front.
	    front = listPtr.next;
            front.prev = null;
	}
	else if (listPtr == rear) {
            // Case 3: delete the last element.
	    rear = listPtr.prev;
            rear.next = null;
	}
	else {
	    // Case 4: In the middle: stitch the prev and next nodes together.
	    listPtr.prev.next = listPtr.next;
	    listPtr.next.prev = listPtr.prev;
	}

	numItems --;
    }

}

In-Class Exercise 12: Modify DoublyLinkedList4.java above to print out the addresses of the node-to-be-deleted, along with the nodes on either side. Then, draw "before" and "after" lists complete with addresses. Use DoublyLinkedListExample4.java as the class with main().

Deletion in a singly-linked list:

  • Deletion is a little more complicated in a singly linked list.

  • Example: suppose we want to delete the data "7" (4-th node) in this list of integers:

  • First, we walk down the list to find the node:

  • To remove the node, we have to make the previous node point to the next one:

  • How do we "reach" the previous node in order to change the pointer?
         ⇒ We need another pointer variable:

  • This pointer variable needs to "move" along with the listPtr variable when we search for the node to be deleted:

In-Class Exercise 13: Write on paper the code needed to search the list for both the item-to-be-deleted and the node prior to it. Then write on paper the code needed for deletion.

In-Class Exercise 14: Implement your code in OurLinkedList2.java and test it with OurLinkedListExample2.java.


© 2006-2020, Rahul Simha & James Taylor (revised 2020)