| CS 261 | University of Puget Sound | Spring, 2020 |
|
Computer Science II
|
||
|
Abstract Data Types and their Implementations,
Some Basic Algorithms,
Object-oriented Problem Solving, and Efficiency |
||
Warning:
This course is under development.
Although the basic structure of this course is largely established,
nothing on this Web site should be considered official or even possibly
correct.
DO NOT MAKE PLANS BASED ON THE CONTENTS OF THIS SITE UNTIL JANUARY, 2020.
The previous reading on Simple lists introduced the concept of a linked list and considered several basic operations:
Use of a constructor to add a new node at the front of the list.
Given a list node, retrieving the data within the node with
the getData method.
Given a list node, retrieving a reference to the next list node with the
operation getNext method.
Traverse a list, going from one node to the next until null or a
designated condition is identified.
This reading extends the range of list operations and includes additional examples.
Additional examples of list traversals.
Insertion of a node into a list, either before or after the first node.
Deletion of a designated node from a list.
In each case, iterative algorithms are considered. (We'll examine another approach, called recursion, later in the course.)
All methods in this reading are gathered together in the complete class NameList.java, based upon the node class ListStringNode.java.
A list traversal is the process of moving from node-to-node in a
linked list to perform some type of processing. For example, the
reading on
simple lists discussed
printing
the data on a list using a loop that moved a list pointer
nodeRef systematically from one node to the next, printing a data
field at each node.
The printing example used a while loop to examine when a list pointer was pointing to a node. Another example of this approach, called an iterative algorithm involves examining the strings stored in list nodes to determine which comes first in alphabetical order.
An algorithm involving a loop (e.g., for or while) to
move from one case to the next is said to be iterative.
An algorithm that proceeds by identifying one or more simple cases and reducing complex cases to simpler ones — using the function to call itself — is said to be recursive. The simple case or cases are said to be base case(s), and the reduction from complex situations to simpler ones is said to be the general or recursive case.
This lab focuses upon iterative algorithms for list processing. Work later in the semester will utilize recursion for some list processing.
Consider the following problem:
When searching a 1-dimensional array for a minimm, a
for or while loop typically allows a program
to examine each of the n values in an array. For linked
lists, we may not know directly the number n of nodes on a
list. Instead, we use a pointer variable nodeRef to start
at the head of the list, and processing can proceed node-by-node
until nodeRef becomes null. Except for the
mechanism to move from one data value to the next, the resulting
code parallels the earlier code for an array of int values.
In what follows, we assume a ListNode has been
created with getData and getNext methods.
In ordering string data, a common approach compares two strings character by character.
If the first characters of two strings are different, the ordering of the strings depends upon which first character comes first.
If the first characters of two strings are the same, the second characters in each string are examined. If the second characters are different, the ordering of the strings depends upon which second character comes first.
More generally, in comparing two strings, successive characters of the strings are compared until a difference is found. The ordering of the two strings is determined by which of those different characters comes first.
This approach to ordering strings is commonly used in dictionaries, so this sequencing of strings is often called dictionary order.
/* post-condition: The string coming first in dictionary order is returned
note: processing proceeds iteratively
*/
public void findMinimum( ) {
if (first == null) {
System.out.println ("list is empty: no string comes first in dictionary order");
}
A null list has no strings, so no string comes first in dictionary order. That case is handled separately.
else {
ListStringNode nodeRef = this.first;
ListStringNode minRef = this.first;
while (nodeRef != null) {
The nodeRef pointer will go node-by-node from the start to the end
of the list. Comparison of the current minimum with data at the
nodeRef node allows consideration of each data string on the list.
The minRef pointer identifies the node with the string coming
first in dictionary order.
Processing continues until nodeRef has moved through all nodes on
the list.
if (minRef.compareTo (nodeRef) > 0) {
minRef = nodeRef;
}
nodeRef = nodeRef.getNext ( );
}
The compareTo method compares two strings
in dictionary order. If the first string comes after the second in
dictionary order, compareTo returns a positive value.
The minRef variable is updated if a new minimum is found,.
After examining this node, nodeRef moves to the next node.
System.out.println ("The minimum string on the list (dictionary order) is: "
+ minRef.getData ( ));
}
}
After searching all nodes, the minimum is printed.
The insertion of an item into a list may be done either iteratively or recursively; the overall result is illustrated in the figure below, where "Visit Library" is inserted into a new third box on the list.
Much of the discussion of insertion into a list and deletion from a list within this reading is edited from "Computer Science 2: Principles of Software Engineering, Data Types, and Algorithms" by Henry M. Walker, Scott, Foresman and Company, 1989, and is used with permission of the copyright holder.
In the new list, we created a new box, placed the "Visit Library" in the data field, made the pointer of the new box indicate that the "Read email" box comes next, and changed the pointer of the "Attend class" box to this new list item. In this insertion process, we can build the "Visit Library" box at any convenient place; then we add this box to the list by changing pointers appropriately.
This example illustrates the main steps involved in the iterative algorithm for inserting an item into a list. The following figure shows these steps in some detail.
Within this outline of steps for iterative insertion, two complications arise:
To insert an item at the beginning of a list, we must update the first pointer rather than the pointer in the preceding list item.
We need some criteria regarding where to place the new item. For example, we might ask a user to indicate the item that will precede the new item. In the example, we inserted "Visit Library" before "Read email".
Often, in inserting a new item into a list, it is convenient to identify
the node that should come after the new one. In that circumstance, when
searching for the location to insert the new list, we will need to keep
track of the previous node as well — Step 5 in the insertion process
requires that we update the next field of the previous node, so we
need to know where that node is located. (If we find the node after the
new node, we can follow next fields to go to toward the end of the
list, but we have no mechanism to back up.)
The following iterative procedure addName inserts an
item before a designed node.
/* pre-condition: none
post-condition: a designated newName is inserted into the list
at a designated place */
void addName(String newName) {
ListStringNode newNode = new ListStringNode (newName, null);
ListStringNode nodeRef;
Where ever the new node will be inserted, we will need a new node.
Thus, the start of this procedure creates a node and copies the
newName string into its data field. We do not yet know where
this node will be inserted into the list, so we use null
for now as the reference to the next node.
When inserting a new node, we will need to search the list to find
where it goes. nodeRef designates the node we are currently
searching.
if (this.first == null) {
/* insert name's node at start of list */
this.first = newNode;
}
When the list is null, the new node must be inserted at the start of the list
— pointed to by the first. (The next field of the new
node has already been assigned null with the
constructor, so nothing will come after this new node.)
else {
String oldName ;
System.out.println ("Enter old name which new name should preceed, ");
System.out.println ("or enter ? if new name should be placed last");
Scanner fileIn = new Scanner (System.in);
oldName = fileIn.next ( );
if (oldName.compareTo (first.getData ( )) == 0) {
/* insert name's node at start of list */
newNode.setNext (this.first);
this.first = newNode;
}
If the original list is not null, the user will need to specify where the new node should be inserted. In this case, the user is asked to enter the string in the node that will come directly after the new node, when it is inserted.
Once the successor of the new node is known, insertion will be at the head of the list if the new node is to precede the old first node. Insertion at the head here is the same as when inserting into an empty list (above).
else {
/* insert name's node after start of the list */
ListStringNode prevRef;
/* start at beginning of list */
nodeRef = first.getNext ( ); /* the current node to search */
prevRef = this.first; /* the node behind nodeRef */
while ((nodeRef != null)
&& (oldName.compareTo (nodeRef.getData ( )) != 0)) {
prevRef = nodeRef;
nodeRef = prevRef.getNext ( );
}
newNode.setNext (prevRef.getNext ( ));
prevRef.setNext (newNode);
}
}
System.out.println (newNode + " inserted into the list");
}
If the new node should not be first, then a search of the remaining nodes is needed.
nodeRef variable) will need to examine each
node.
prevRef identifies the preceding
node, in anticipation of Step 5 in the above figure for insertion.
Once the successor node is found, the newNode is inserted.
next field of the new node points to its successor node.
next field of the previous node points to the new node.
The overall strategy for deletion is illustrated in the following figure, in which the third item is deleted from the list.
In the new list, the box, "Read email", is deleted from the list. In the second list, the "Attend class" box no longer specifies that the "Read email" box comes next; rather the pointer for the "Attend class" box indicates that the "Eat lunch" box comes next. With a small change in a pointer, the "Read email" box is no longer on the list, since we cannot reach that box by starting at the beginning of the list and moving from one item to the next. Even if the box is physically present somewhere, it is lost for all future work because there is no way to locate it. Beyond changing pointers, we also may decide to throw away the old item that we deleted so that we can use that space again.
This example illustrates the main steps involved in the iterative algorithm for deleting an item from a linked list. The following figure shows these steps more carefully.
As the figure suggests, the ideas behind this deletion are fairly simple. However, the conceptual approach is complicated by two details:
The deletion of the first item in a list requires a special case, since we must designate a new first item.
When we find which item to delete, we still must keep track of the previous
item. For example, in the example above, when we delete the "Read email"
box, we also must be able to locate the "Attend Class" box (so we can
change its next pointer).
This second point emphasizes that in moving through linked lists, it is easy to go from any particular list item to later ones by following the arrows. We cannot follow the arrays backward, however, so it is difficult to back up toward the front. Each list item contains information about the next item, but there is no information about previous ones. Thus, to delete a list item, we must explicitly keep track of previous items as we search. With the preceding item, we can move ahead easily to the item we will actually delete.
These comments motivate the following deleteName
function.
/* post-condition: a node with the given deletionString is deleted
from the list */
void deleteName(String deletionString) {
if (this.first == null) {
System.out.println("List is empty - no deletions are possible");
}
An empty list contains few items, so little search is needed to find an item to delete (there are none).
else {
if (deletionString.compareTo (first.getData ( )) == 0) {
/* remove first item on list */
this.first = this.first.getNext ( );
System.out.println (deletionString + " removed as first item on list");
}
As noted in the above discussion, deletion of a node at the beginning of a list requires a special case.
/* item to remove is not at beginning of list */
/* start at beginning of list */
ListStringNode nodeRef = this.first.getNext ( ); /* the current node to search */
ListStringNode prevRef = this.first; /* the node behind nodeRef */
while ((nodeRef!=null)
&& (deletionString.compareTo(nodeRef.getData ( )) != 0)) {
prevRef = nodeRef;
nodeRef = prevRef.getNext ( );
}
Since the item to be deleted is not the first on the list, we need to traverse the list to find the desired item.
The deletion process requires that we find both the node to delete and the
previous node in the list. The pointer variables, nodeRef and
prevRef, handle that bookkeeping. In this loop, prevRef
is always one node behind nodeRef.
Traversing the list continues until we either find the desired node or reach the end of the list.
if (nodeRef != null) {
/* remove item from list */
prevRef.setNext (nodeRef.getNext ( ));
System.out.println (deletionString + " deleted from list");
} else {
System.out.println (deletionString +"not found on list");
}
}
} /* end processing of deletionString */
If the node with the desired data field is found, we delete it in a manner
analogous to deleting the first item. That is, we update the previous
pointer (in this case,the next field for prevRef).
else {
printf ("%s not found on list\n\n", dataString);
}
} /* end processing of dataString */
}
}
If we reach the end of the list without finding the intended string to delete, the program can report failure.
Reminder: As noted several times in the above discussion, program NameList.java compiles all functions in this reading into a full program.
|
created 26 November 2016 by Henry Walker revised to move recursive discussion to separate document 20-27 January 2020 by Henry Walker |
|
| For more information, please contact Henry M. Walker at walker@cs.grinnell.edu. |