Let's think about writing a definition of function length(L) that returns the length of list L. For example, length([2, 4, 6]) = 3 and length([ ]) = 0. It suffices to say how to compute the length of an empty list and how to compute the length of a nonempty list. But instead of thinking about an algorithm right now, let's focus on facts.
(length.1) length([]) = 0 (length.2) length(L) = 1 + length(tail(L)) (when L is not empty)Here are two important observations.
These are equations just as much as the ones that you have studied in mathematics. For example, you learned that x(y + z) = xy + xz. That tells you that two expressions yield the same value for all values of the variables x, y and z. Similarly, equation length([ ]) = 0 tells you that two expressions, length([ ]) and 0, yield the same value.
As you can see, each equation can have a proviso indicating requirements for it to hold; the second equation only holds when L is not empty. The second equation tells you, for example, that length([2, 4, 6, 8]) = 1 + length([4, 6, 8]). Since 4 = 1 + 3, that is true.
So we have two facts (equations) about the length function. But what we really want is an algorithm to compute the length of a list. Is it reasonable to say that those two equations define an algorithm? Let's try to compute length([2, 4, 6]) using them. The only thing we do is replace expressions with equal expressions, using facts (length.1) and (length.2) and a little arithmetic.
length([2,4,6]) = 1 + length([4,6]) by (length.2), since tail([2,4,6]) = [4,6] = 1 + (1 + length([6])) by (length.2), since tail([4,6]) = [6] = 1 + (1 + (1 + length([]))) by (length.2), since tail([6]) = [] = 1 + (1 + (1 + 0)) by (length.1) = 3 by arithmetic
Equations (length.1) and (length.2) are easy to convert into C++. Since there are two equations, there are two cases.
int length(ConstList L) { if(isEmpty(L)) { return 0; // by (length.1) } else { return 1 + length(tail(L)) // by (length.2) } }It is also fine to use C++ notation directly.
int length(ConstList L) { if(L == NULL) { return 0; } else { return 1 + length(L->tail); } }Use whichever form you prefer.
Let's use equations to define function member(x, L), which is true if x belongs to list L. For example, member(3, [2, 6, 3, 5]) = true and member(4, [2, 6, 3, 5]) = false.
Here are some equations. Operator 'or' is the logical or operator, the same as | | in C++.
(member.1) member(x, []) = false (member.2) member(x, L) = head(L) == x or member(x, tail(L))The first equation should be clear. The empty list does not have any members. Let's try some examples of the second equation, (member.2). According to that equation,
member(6, 2:[4,6,8]) = 6 == 2 or member(6, [4,6,8]) = false or true = true(Remember the rule for hand-simulating recursive algorithms. Recursive calls are assumed to work correctly. You can do the same for checking equations.) |
member(2, 2:[4,6,8]) = 2 == 2 or member(2, [4,6,8]) = true or member(2, [4,6,8]) = trueNotice that we do not need to evaluate member(2, [4, 6, 8]) because (true or x) is true regardless of what x is. This is a search problem, and the algorithm does not need to look at the entire list when the it finds what it is looking for. |
member(5, 2:[4,6,8]) = 5 == 2 or member(5, [4,6,8]) = false or false = false |
Let's convert Equations (member.1) and (member.2) into a definition of member in C++.
bool member(const int x, ConstList L) { if(L == NULL) { return false; } else { return x == L->head || member(x, L->tail); } }
Notice that, for both length and member, we wrote one equation for an empty list and one for a nonempty list. That is typical. In some cases, you need (or prefer) more equations, as the next example illustrates.
Let's write equations that define function isPrefix(A, B), which is true if list A is a prefix of list B. For example, isPrefix([1,3], [1,3,5]) is true. Every list is a prefix of itself, so isPrefix([2, 4, 6], [2, 4, 6]) is true, and the empty list is a prefix of every list. Here are equations for isPrefix.
(isPrefix.1) isPrefix([], B) = true (isPrefix.2) isPrefix(A, []) = false (when A is not []) (isPrefix.3) isPrefix(A, B) = head(A) == head(B) and isPrefix(tail(A), tail(B)) (when A ≠ [] and B ≠ [])The first two equations should be evident. Equation (isPrefix.1) says that the empty list is a prefix of every list. We assume that the equations are tried in order, so (isPrefix.2) is only relevant when A is not []. Equation (isPrefix.2) says that a nonempty list is not a prefix of an empty list. The third equation should be more obvious through an example.
isPrefix([3,5], [3,5,7,9]) = 3 == 3 and isPrefix([5], [5,7,9]) by (isPrefix.3) = true and isPrefix([5], [5,7,9]) = isPrefix([5], [5,7,9]) = 5 == 5 and isPrefix([], [7,9]) by (isPrefix.3) = true and isPrefix([], [7,9]) = isPrefix([], [7,9]) = true by (isPrefix.1)
Here is a C++ definition of isPrefix.
bool isPrefix(ConstList A, ConstList B) { if(A == NULL) { return true; } else if(B == NULL) { return false; } else { return A->head == B->head && isPrefix(A->tail, B->tail) } }
Let's write equations that define function evens(L), which yields a list of all members of list L that are even, preserving their order. For example, evens([2, 5, 6, 7, 9, 10]) = [2, 6, 10]. First, an obvious equation.
(evens.1) evens([]) = []
Now suppose that L starts with an even number. Then that even
number will be the first value in the result list, and it will be followed
by all even numbers in tail(L).
(evens.2) evens(L) = head(L):evens(tail(L)) (when head(L) is even)Finally, if L does not begin with an even number, just ignore that number.
(evens.3) evens(L) = evens(tail(L)) (when head(L) is odd)Putting those three equations together defines an algorithm for evens(L).
List evens(ConstList L) { if(L == NULL) { return NULL; } else { int h = L->head; ConstList t = L->tail; if(h % 2 == 0) { return cons(h, evens(t)); } else { return evens(t); } } }
The following equation about isPrefix is false. Give a counterexample that shows it is wrong. Evaluate the two sides for your counterexample and show that they are not equal.
isPrefix(h:t, L) = isPrefix(t, L)Answer
Using equations (length.1) and (length.2'), show an evaluation of length([6,5,4,3]) by only replacing expressions by equal expressions. Answer
Using equations (isPrefix.1–isPrefix.3), show an evaluation of isPrefix([2,3,4], [2,4,3]) by only replacing expressions by equal expressions. Answer
Using equations (isPrefix.1–isPrefix.3), show an evaluation of isPrefix([2,3], [2]) by only replacing expressions by equal expressions. Answer
Write equations for function sum(L), which yields the sum of all members of list L. For example, sum([2,3,4]) = 2 + 3 + 4 = 9. The sum of an empty list is 0. Make sure that your equations contain enough information to determine the value of sum(L) for any list of integers L. Answer
Using your equations for sum(L) from the preceding question, do an evaluation of sum([3, 5, 7]) by only replacing expressions by equal expressions. Answer
Following your equations, write a C++ definition of sum(L). Answer
Write equations for function smallest(L), which yields the smallest member of nonempty list L. For example, smallest([2, 3, 4]) = 2 and smallest([6, 4, 8, 7]) = 4. Your equations should not try to define smallest([ ]), since the list is required to be nonempty. You can use function min(x, y) to compute the smaller of two integers x and y. Answer
Write a definition of smallest based on your equations. Answer
Write equations for function prefix(L, n), which yields the length n prefix of list L. For example, prefix([2, 4, 6, 8, 10], 3) = [2, 4, 6]. If n is larger than the length of L then prefix(L, n) should return L. For example, prefix([2, 4, 6, 8, 10], 50) = [2, 4, 6, 8, 10]. Answer
Convert your equations for prefix to a C++ definition of prefix(L, n). Answer