Module 6: Supplemental Material
Iterating through a set - some variations
Recall the integer example:
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);
// ...
}
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;
}
// ...
}
We'll now focus on the computeIntersection() and write
it in a few different ways. All of these are in
SetExample9.java.
First, instead of get() we'll use an iterator
object:
static LinkedList<Integer> computeIntersectionAlt1 (LinkedList<Integer> listA, LinkedList<Integer> listB)
{
LinkedList<Integer> listC = new LinkedList<Integer>();
Iterator<Integer> iter = listA.iterator();
while (iter.hasNext()) {
Integer K = iter.next();
if ( listB.contains(K) ) {
listC.add (K);
}
}
return listC;
}
Note:
- An Iterator is an object returned by the
iterator() method in LinkedList.
- This object has two methods:
- hasNext() - to see if there are more elements
left in the iteration.
- next() to fetch the next element.
Notice that the Iterator above is specialized to
Integer's. It is possible to use one without this
specialization:
Iterator iter = listA.iterator();
while (iter.hasNext()) {
Integer K = (Integer) iter.next();
if ( listB.contains(K) ) {
listC.add (K);
}
}
In this case, we need to cast the return value of
next() to our Integer variable K.
The iteration above was written with a while-loop. We can convert that
into a for-loop:
for (Iterator iter = listA.iterator(); iter.hasNext(); ) {
Integer K = (Integer) iter.next();
if ( listB.contains(K) ) {
listC.add (K);
}
}
Exercise 1:
A standard for-loop has three parts to the for-statement: an
initialization part, a test and an increment. The above example
doesn't have the third part. Why does it still work?
An even more compact way of iterating through Java's data structures
is to use this version of the for-loop:
for (Integer K: listA) {
if ( listB.contains(K) ) {
listC.add (K);
}
}
Next, observe that we need not "specialize" the list to
Integer's. For example:
static LinkedList<Integer> computeIntersectionAlt5 (LinkedList listA, LinkedList listB)
{
LinkedList<Integer> listC = new LinkedList<Integer> ();
Iterator iter = listA.iterator();
while (iter.hasNext()) {
Integer K = (Integer) iter.next();
if ( listB.contains(K) ) {
listC.add (K);
}
}
return listC;
}
Note:
- Even though specialized lists are passed in, we can treat them
as not specialized. In this case, a cast is required:
Integer K = (Integer) iter.next();
- We have left the return list type as specialized to Integer's.
We could also change the return type:
static LinkedList computeIntersectionAlt6 (LinkedList listA, LinkedList listB)
{
LinkedList listC = new LinkedList ();
Iterator iter = listA.iterator();
while (iter.hasNext()) {
Integer K = (Integer) iter.next();
if ( listB.contains(K) ) {
listC.add (K);
}
}
return listC;
}
Note:
- While this works, it generates a compiler warning for the line:
listC.add (K);
This is because the add() method is defined for so-called
generic types in Java.
- Generic (what we've been calling "specialized") types are
a big topic and somewhat complicated, which we will not cover in
this course and which is why our examples are not written that way.
More about linked lists
Recall the add() method in OurLinkedList:
(source file)
public void add (String s)
{
if (front == null) {
front = new ListItem ();
front.data = s;
rear = front;
rear.next = null;
}
else {
// We declare and use the variable nextOne.
ListItem nextOne = new ListItem ();
nextOne.data = s;
rear.next = nextOne;
rear = nextOne;
}
numItems ++;
}
It is possible to write the else part a little
more compactly, without an additional variable:
(source file)
public void add (String s)
{
if (front == null) {
// ... same ...
}
else {
// No variable declared. Simply make rear.next point to a new instance.
rear.next = new ListItem ();
rear = rear.next;
rear.data = s;
}
// ...
}
Note:
- Recall that rear.next is an object variable of type
ListItem and so it is possible to make it point to a new instance.
⇒
No separate variable is needed.
- Once rear.next points to the new node, we make
rear point to it:
rear = rear.next;
- Finally, that is where we assign the new data to be added.
Exercise 2:
Contrast the above code with the previous approach on paper.
Imagine an initially empty list into which three strings are added.
Show the complete memory picture (provide some imagined addresses)
at each step of the else clause above.
Next, let us return to deletion in a doubly-linked list:
- First, recall the two deletion methods:
(source file)
public class DoublyLinkedList4 {
// ... other methods etc ...
// 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 --;
}
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 --;
}
} // end-list-class
- First, noticing the common deletion code, we can create a
cleaner version as follows:
(source file)
public class DoublyLinkedList5 {
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;
}
remove (listPtr);
}
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 ++;
}
remove (listPtr);
}
void remove (ListItem listPtr)
{
// 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 --;
}
} // end-list-class
- It is common to return the deleted item to the caller, so
let's re-write the code so that it can used as:
// Delete the item in position 4.
String deletedString = list.delete(4);
- The re-written version:
(source file)
public String 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 null;
}
remove (listPtr);
return listPtr.data;
}
public String delete (int i)
{
// Check for bad input.
if ( (i < 0) || (i >= numItems) ) {
return null;
}
// Find the i-th element.
int count = 0;
ListItem listPtr = front;
while ( (listPtr != null) && (count != i) ) {
listPtr = listPtr.next;
count ++;
}
remove (listPtr);
return listPtr.data;
}
void remove (ListItem listPtr)
{
// ...
}
Copying a list:
- Consider the following code:
DoublyLinkedList listA = new DoublyLinkedList ();
listA.add ("Crocodile Hunter");
listA.add ("Evening at the Improv");
// Is this a copy?
DoublyLinkedList listB = listA;
listB.add ("Tonight Show");
// What gets printed?
System.out.println (listA);
- To properly copy a list, we need to perform a deep
copy: copy the nodes and the data items in the nodes.
Exercise 3:
Download and modify DoublyLinkedList7.java
to make copy() work correctly. Use
DoublyLinkedListExample7.java
for testing (Read this file first).
Array list revisited
Recall array lists:
- We implemented the methods add(), size() etc by using
an array.
- We found the fixed array size to be a limitation.
- We will now see that the array size can be changed
dynamically at run time by creating a new, larger array and copying
the existing data into it.
- We'll create such a list for integers, because that will
allow us to cover a related topic of interest a little later.
Here's the program:
(source file)
public class OurArrayList {
// This is the array in which we'll store the integers.
Integer[] data = new Integer [1];
// Initially, there are none.
int numItems = 0;
public void add (Integer K)
{
if (numItems >= data.length) {
// Need more space. Let's double it.
Integer[] data2 = new Integer [2 * data.length];
// Copy over data into new space.
for (int i=0; i < data.length; i++) {
data2[i] = data[i];
}
// Make the new array the current one.
data = data2;
}
data[numItems] = K;
numItems ++;
}
public int size ()
{
return numItems;
}
public Integer get (int i)
{
return data[i];
}
public boolean contains (Integer K)
{
// Note: we need to use numItems instead of strings.length
for (int i=0; i < numItems; i++) {
if ( data[i].equals(K) ) {
return true;
}
}
return false;
}
public String toString ()
{
if (numItems == 0) {
return "Empty";
}
String s = "List: ";
for (int i=0; i < numItems; i++) {
s += " " + data[i];
}
return s;
}
}
Note:
- The initial array size is very small, just 1.
- Here's an example of using the array-list:
(source file)
public static void main (String[] argv)
{
OurArrayList intList = new OurArrayList();
// Put 10 integers.
for (int k=0; k < 10; k++) {
Integer K = new Integer (k);
intList.add (k);
}
System.out.println (intList);
Integer M = new Integer (5);
if ( intList.contains(M) ) {
System.out.println ("List contains " + M);
}
}
- For large arrays, the copying over can be a significant
overhead. However, this is to be balanced against the fact the
number of increments is not likely to be large.
- Arrays can also waste space, with almost half of it unused
(just after an increment).
- However, arrays are very efficient for accessing the data.
Exercise 4:
Download both files OurArrayList.java
and OurArrayListExample.java
and modify OurArrayList so that the new array size is printed out
each time the array size is increased. Then, modify the code
in OurArrayListExample to add a 100 integers. How many
times is the array size increased? Can you tell, without running
the program (i.e., just by thinking), how many increments
will occur if 1000 integers are added? In general, for adding
n integers, how many times is the array size increased?
Let's use the code above to explore autoboxing, a Java
feature that allows automatic conversion from int's to Integer's.
First, we will re-write the array list as follows:
(source file)
public class OurArrayList2 {
// Note: an int array
int[] data = new int [1];
int numItems = 0;
public void add (Integer K)
{
if (numItems >= data.length) {
// Need more space. Let's double it.
int[] data2 = new int [2 * data.length];
// Copy over data into new space.
for (int i=0; i < data.length; i++) {
data2[i] = data[i];
}
// Make the new array the current one.
data = data2;
}
// Conversion from Integer to int for assignment:
data[numItems] = K;
numItems ++;
}
public int size ()
{
return numItems;
}
public Integer get (int i)
{
// Conversion from int to Integer:
return data[i];
}
public boolean contains (Integer K)
{
// Note: we need to use numItems instead of strings.length
for (int i=0; i < numItems; i++) {
// Conversion from Integer to int for direct comparison:
if ( K == data[i] ) {
return true;
}
}
return false;
}
public String toString ()
{
// ...
}
}
Note:
- Java converts from int's to Integer's and
vice-versa where appropriate.
- Thus, above the internal representation was changed to
int's but the code in main() didn't change.
- The input and output to this class is still of type Integer:
public void add (Integer K)
{
// ...
}
- Observe the direct comparison:
// K is an Integer, whereas data[i] is an int.
if ( K == data[i] ) {
}
- This feature is known as autoboxing:
- Autoboxing is provided for conversion from all base types
(e.g, double) to their corresponding classes (e.g, Double).
- It's called autoboxing because wrapper code is
inserted by the compiler to handle the conversions.
The following self-explanatory code provides more examples:
(source file)
public class AutoboxingExample {
public static void main (String[] argv)
{
// One of each.
Integer I = 5;
int j = 6;
// Mixed sum of Integer and int:
Integer K = I * j;
// Use of Integer's with comparison operators:
if (K == 30) {
System.out.println ("K=30");
}
// Here, I gets converted to an int. The return type of
// add() is an Integer, which gets converted to the int "sum".
int sum = add (I, j);
System.out.println ("Sum=" + sum);
}
static Integer add (int a, int b)
{
// Automatic conversion to Integer of result.
return (a + b);
}
}
© 2006-2020, Rahul Simha & James Taylor (revised 2020)