28 Checklists
In many aspects of life, both personal and professional, it is necessary to determine if a set of criteria have been satisfied/accomplished (e.g., goals have been reached, courses have been taken). One common way to do this is to use a checklist.
Motivation
Suppose you’re getting ready to go on vacation; you probably have a number of things that you want to remember to pack (e.g., shirts, socks, pants, and skirts). So, you decide to write a program to help you ensure that you don’t forget anything. However, unlike the situations considered in Chapter 10 on bit flags, the program will be provided with the checklist dynamically at run-time (i.e., it isn’t known when the program is written and compiled).
Review
To increase the flexibility of the program, you decide to represent the criteria that need to be satisfied/accomplished as a String[]
named checklist
, and you populate that array before you start working on the tasks. Then, as you accomplish a task, you enter it into another String[]
named accomplished
. Each time you accomplish a task you want to be able to determine whether or not you are done (e.g., whether you have completed all of the tasks in the checklist).
Of course, it’s easy to compare a single element of checklist
with a single element of accomplished
using the equals()
method in the String
class. However, in and of itself, that doesn’t solve the problem of determining whether or not you are done. Clearly, the equals()
method must be invoked iteratively.
Thinking About The Problem
You can determine whether any given element of checklist
has been accomplished by comparing it with each element in accomplished
. For example, you can determine whether element index
of checklist
has been accomplished as follows:
boolean done = false; for (int a = 0; a < accomplished.length; a++) { if (accomplished[a].equals(checklist[index])) { done = true; break; } }
At the end of this loop, done
will contain true
if and only if checklist[index]
has been accomplished.[1]
This loop can be used to determine whether a single criterion has been satisfied/accomplished. To determine if all of the criteria have been satisfied/accomplished this loop needs to be nested inside of another loop. Unfortunately, there are many ways to do this incorrectly.
For example, the following implementation returns false
at the first discrepancy between the two arrays, which may just be a result of a difference in how the two are ordered:
for all elements in accomplished { for all elements in checklist { if the accomplished element does not equal the checklist element { return false } } } return true
As another example, the following implementation returns true
as soon as it determines that one item on the checklist has been accomplished:
for all elements in accomplished { assign false to the accumulator named checked for all elements in checklist { if the accomplished element equals the checklist element { assign true to the accumulator named checked break } } if checked is true then return true } return false
In short, there are many incorrect ways to think about the problem. To get the right answer, you must think carefully about the way the loops are nested, the Boolean expression in the if
statement, the way in which break
statements are used, and where return
statements are located.
The Pattern
For this problem, there are two variants of the pattern. In the first variant, you only want the method to return true
when all of the items on the checklist have been accomplished. You can solve this variant with a single boolean
accumulator as follows:
private static boolean checkFor(String[] checklist, String[] accomplished) { boolean checked; for (int c = 0; c < checklist.length; c++) { checked = false; for (int a = 0; a < accomplished.length; a++) { if (checklist[c].equals(accomplished[a])) { checked = true; break; } } if (!checked) return false; // An item was not accomplished } return true; // All items were accomplished }
Note that this algorithm breaks out of the inner loop as soon as it determines that the checklist item of interest has been accomplished. It returns false
as soon as it determines that any checklist item was not satisfied/accomplished. Hence, if both loops terminate normally then all of the items on the checklist must have been accomplished and the method returns true
.
In the second variant of the pattern, you want the method to return true
when more than needed
elements of the checklist have been accomplished. For this variant, you can use an int
accumulator named count
that keeps track of the number of items on the checklist that have been accomplished, as follows:
private static boolean checkFor(String[] checklist, String[] accomplished, int needed) { int count; count = 0; for (int c = 0; c < checklist.length; c++) { for (int a = 0; a < accomplished.length; a++) { if (checklist[c].equals(accomplished[a])) { ++count; if (count >= needed) return true; else break; } } } return false; // Not enough items were accomplished }
Again, this algorithm can break out of the inner loop when it determines that the checklist item of interest has been accomplished. In addition, it can return early when count
reaches needed
. However, in this case, an early return means the checklist has been satisfied/accomplished. Hence, if both loops terminate normally then the method returns false
.
Note that both implementations could be improved by checking to ensure that accomplished.length
is at least as large is necessary to satisfy/accomplish the checklist (i.e., at least as large as checklist.length
in the first variant and at least as large as needed
in the second variant). This improvement was omitted for the sake of simplicity.
Examples
It is useful at this point to consider some examples involving both variants above. In all of these examples, checklist
contains the elements "Shirts"
, "Socks"
, "Pants"
, and "Skirts"
.
The Inflexible Variant
First, suppose that accomplished
contains the elements "Shirts"
, "Socks"
, "Pants"
, "Dresses"
, and "Shoes"
. In outer iteration 0, the method checks to see if "Shirts"
has been accomplished. In inner iteration 0, the method determines that it has been and breaks out of the inner loop. In outer iteration 1, the method checks to see if "Socks"
has been accomplished. In inner iteration 0, the method determines that it hasn’t been, but in inner iteration 1 it determines that it has been and breaks out of the inner loop. The iterations then continue as follows:
c |
checklist[c] |
a |
accomplished[a]
|
---|---|---|---|
0 | Shirts | 0 | Shirts |
1 | Socks | 0 | Shirts |
1 | Socks | ||
2 | Pants | 0 | Shirts |
1 | Socks | ||
2 | Pants | ||
3 | Skirts | 0 | Shirts |
1 | Socks | ||
2 | Pants | ||
3 | Dresses | ||
4 | Shoes |
Since checklist[3]
is not an element of accomplished
, the local variable checked
is never assigned the value true
, and the method returns false
.
Now, suppose that accomplished
contains the elements "Socks"
, "Shirts"
, "Skirts"
, and "Pants"
. In outer iteration 0, the method checks to see if "Shirts"
has been accomplished. In inner iteration 0, the method determines that it hasn’t been, but in inner iteration 1 it determines that it has and breaks out of the inner loop. In outer iteration 1, the method checks to see if "Socks"
has been accomplished. In inner iteration 0, the method sees that it has been and breaks out of the inner loop. The iterations then continue as follows:
c |
checklist[c] |
a |
accomplished[a]
|
---|---|---|---|
0 | Shirts | 0 | Socks |
1 | Shirts | ||
1 | Socks | 0 | Socks |
2 | Pants | 0 | Socks |
1 | Shirts | ||
2 | Pkirts | ||
3 | Pants | ||
3 | Skirts | 0 | Socks |
1 | Shirts | ||
2 | Skirts |
In this case, checked
is assigned the value true
in every outer iteration, and the method returns true
.
The Flexible Variant
Now consider the first example above but with the second variant of the method, when 2
is passed into the formal parameter named needed
(because, apparently, this person is fully-dressed when wearing any two items in the checklist). In outer iteration 0, the method checks to see if "Shirts"
has been accomplished. In inner iteration 0, the method determines that it has been, increases count
to 1
, and breaks out of the inner loop. In outer iteration 1, the method checks to see if "Socks"
has been accomplished. In inner iteration 0, the method determines that it hasn’t been, but in inner iteration 1 it determines that it has been, increases count
to 2
, determines that count
is greater than or equal to needed
, and returns true
.
Now, consider an example in which accomplished
contains the elements "Dresses"
and "Shirts"
. These iterations will proceed as follows:
c |
checklist[c] |
a |
accomplished[a]
|
---|---|---|---|
0 | Shirts | 0 | Dresses |
1 | Shirts | ||
1 | Socks | 0 | Dresses |
1 | Shirts | ||
2 | Pants | 0 | Dresses |
1 | Shirts | ||
3 | Skirts | 0 | Dresses |
1 | Shirts |
In inner iteration 1 of outer iteration 0 the method determines that "Shirts"
have been packed, and increases count
to 1. However, none of the inner iterations for outer iteration 1 correspond to "Socks"
, none of the inner iterations for outer iteration 2 correspond to "Pants"
, and none of the inner iterations for outer iteration 3 correspond to "Skirts"
. So, count is never increased to 2
, and the method returns false
.
Looking Ahead
Though some thought was given to the efficiency of the algorithms above, many issues were ignored, and none were considered formally. If you take a course on data structures and algorithms, you will consider these kinds of issues in detail. For example, it is interesting to ask whether it is better to sort checklist
and/or accomplished
, either partially or completely. It is also interesting to ask whether it is possible to create a more efficient algorithm when the checklist consists of sequential integers.
- Note that the body of the
if
statement contains abreak
statement. While not necessary (i.e., the fragment would be correct without it), there is no reason to continue iterating after it has been determined thatchecklist[index]
has been accomplished. Note also that the body of the loop does not contain anelse
clause that assignsfalse
todone
. Sincedone
is initialized tofalse
outside of the loop, there is no reason to assign a value todone
at each iteration. Finally, you should be able to convince yourself that, if thebreak
statement was omitted and theelse
clause was included, the fragment would not be correct (i.e., when the loop terminatesdone
would always contain the valueaccomplished[accomplished.length-1].equals(checklist[index])
). ↵