The first of three lectures introducing the design and implementation of recursive data structures. In these lectures we compare four different implementations of stacks and arithmetic expression trees. A fourth lecture on Abstraction and data structures comes later.
Introduce some of the ideas
Work through details of different approaches
Compare their strengths and weaknesses
Develop an appreciation of the design trade-offs involved
Prepare students for their later work on the XML document tree structure in the word processor
Revise Lectures on data structures in COMP1110 (the ones on recursion, collections, hashing, stacks and trees). Lectures 9, 17, 18 in 2012.
Yes, that means you have to do some work. Go to the COMP1110 lecture notes 2012 and read through Lectures including 9, 17, 18: linked lists, recursion, binary search trees, general trees, hash maps. You may need to look at several places in the COMP1110 lecture notes. You may need to refer to otehr textbook sources to fill in the details and find examples of code. Binary serach trees are a classic exmapl that you will find in many books and many places.
A recursive algorithm is one that calls itself — in other words, it contains a reference to itself inside itself.
Similarly, a recursive data structure is one which contains (smaller) structures of the same form within itself.
The commonest examples are lists, stacks, trees.
Because the structure is recursive, it is natural to process it using recursive algorithms.

There are two kinds of stacks:
empty stacks
nonempty stacks
Nonempty stacks have two parts:
the top
the rest
The ‘rest’ of a nonempty stack is itself a (shorter, and possibly empty) stack. That's why it's a recursive data structure.
public class Stack1 {
private Object value;
private Stack1 rest;
private boolean empty;
public boolean isEmpty() { return empty; }
/**
* Initialise to a new empty stack
*/
public Stack1() {
value = null;
rest = null;
empty = true;
}
/**
* Initialise to an identical copy of another stack. Note
* that this constructor is only visible from this class.
*/
private Stack1(Stack1 other) {
this.value = other.value;
this.rest = other.rest;
this.empty = other.empty;
}
/**
* Add x at the top
*/
public void push(Object x) {
this.rest = new Stack1(this);
this.value = x;
this.empty = false;
}
/**
* Pop off the top value
*/
public void pop() {
assert !isEmpty() // you need Java 1.4 for that
// run it by "java -enableassertion Stack1"
this.value = rest.value;
this.empty = rest.empty;
this.rest = rest.rest;
}
/**
* The top element
*/
public Object top() {
assert !isEmpty() // you need Java 1.4 for that
return value;
}
/**
* The number of elements
*/
public int depth() {
if (isEmpty()) {
return 0;
} else {
return 1 + rest.depth();
}
}
}


/**
* Test the main operations of the class
*/
public static void main(String[] args) {
Stack1 s = new Stack1();
s.push(new Integer(1));
s.push(new Integer(2));
int x = ((Integer) s.top()).intValue();
s.pop();
int y = ((Integer) s.top()).intValue();
s.pop();
s.push(new Integer(x + y));
System.out.println("Depth = " + s.depth());
System.out.println("Top = " + ((Integer) s.top()).intValue());
}
+Relatively short and clean.
-Manipulation of object references is subtle.
-Not all elements need the value and rest attributes. (Though it's not an issue here, as all but the last link in the chain require them.)
[Expert's note: some of you are thinking “Why not only store non-empty stacks, and use null to represent an empty stack?” A very good question. The very good answer: it enables a client to refer to a stack (by ‘dotting’ into it) uniformly without first checking if the value is null. For instance, in this implementation you can say s.depth() when s refers to an empty stack. Note that s.depth() causes an exception at run-time if s = null. Of course, s could still be null, but that's now a different problem.]
Java note: Notice the way the fields in class Stack1 are declared as private. This protects them from interference by clients, but still allows other Stack1 objects to see them. There is no need for an accessor method for rest, since no-one but another part of the same stack will ever need to see it.
Consider a simple language of arithmetic expressions that can include:
constants
addition
multiplication
negation
For example:
0
0 + 1
(0 + 1) × 2
2 + (-1)
4 + (3 × (-2))
(1 + 2) × (3 + 4)
Expressions are usually represented with a tree-like recursive data-structure.

public class Expression1 {
public static final int CONSTANT = 1;
public static final int ADDITION = 2;
public static final int MULTIPLICATION = 3;
public static final int NEGATION = 4;
/* better pre-Java 1.5 solution is to use
a special class with a private constructor
to code the enumerated type variables like above;
see for detail J. Bloch's "Effective Java", item 21.
Java 1.5 has enum types built-in (you can refactor
this code using those as an exercise. */
/** Which type is this one? */
private int type;
/** The value, if it's a constant */
private int constantValue = 0;
/** Left and right sub-expressions, if any */
private Expression1 left = null;
private Expression1 right = null;
/**
* Initialise as a constant
*/
public Expression1(int v) {
type = CONSTANT;
constantValue = v;
}
/**
* Intialise as a copy of another
*/
public Expression1(Expression1 other) {
type = other.type;
constantValue = other.constantValue;
left = other.left;
right = other.right;
}
/**
* Add another expression to this expression
*/
public void add(Expression1 e) {
left = new Expression1(this);
right = e;
type = ADDITION;
}
/**
* Multiply this expression by another
*/
public void multiply(Expression1 e) {
left = new Expression1(this);
right = e;
type = MULTIPLICATION;
}
/**
* Reverse the sign of this expression
*/
public void negate() {
left = new Expression1(this);
type = NEGATION;
}
/**
* The value of this expression
*/
public int value() {
int result = 0;
switch (type) {
case CONSTANT:
result = constantValue;
break;
case ADDITION:
result = left.value() + right.value();
break;
case MULTIPLICATION:
result = left.value() * right.value();
break;
case NEGATION:
result = -left.value();
break;
}
return result;
}
/**
* Pretty infix string representation
*/
public String toString() {
String result = "";
switch (type) {
case CONSTANT:
result = "" + constantValue;
break;
case ADDITION:
result = "(" + left.toString() + " + "
+ right.toString() + ")";
break;
case MULTIPLICATION:
result = "(" + left.toString() + " * "
+ right.toString() + ")";
break;
case NEGATION:
result = "-(" + left.toString() + ")";
break;
}
return result;
}
}

Build this expression in e, and evaluate:
/**
* Test the main functions of the class
*/
public static void main(String[] args) {
Expression1 a, b, e;
e = new Expression1(1);
a = new Expression1(2);
e.add(a);
a = new Expression1(3);
b = new Expression1(4);
a.add(b);
e.multiply(a);
System.out.println(e.toString() + " = " + e.value());
}
+ Conceptually simple
- Subtle manipulation of object references
- All types of expression have all attributes
- Wasteful of storage
- Counter-intuitive
- Explicit case by case programming of routines for different types.
- Difficult to expand (division, exponentiation, ...)
- Difficult to maintain
- Strange asymmetry: a.add (b)
public class Expression2 {
public static final int CONSTANT = 1;
public static final int ADDITION = 2;
public static final int MULTIPLICATION = 3;
public static final int NEGATION = 4;
/** Which type is this one? */
private int type;
/** The value, if it's a constant */
private int constantValue = 0;
/** Left and right sub-expressions, if any */
private Expression2 left = null;
private Expression2 right = null;
/** Intialise as a constant expression */
public Expression2(int v) {
type = CONSTANT;
constantValue = v;
}
/** Initialise as a sum or product */
public Expression2(Expression2 l, Expression2 r, int t) {
assert t == ADDITION || t == MULTIPLICATION
type = t;
left = l;
right = r;
}
/** Initialise as a negation */
public Expression2(Expression2 e, int t) {
assert t == NEGATION
type = NEGATION;
left = e;
}
...
}
The methods value() and toString() are the same as in the first version.

Build this expression in e, and evaluate:
/** Test the main features */
public static void main(String[] args) {
Expression2 a, b, c, d, e;
a = new Expression2(1);
b = new Expression2(2);
c = new Expression2(a, b, Expression2.ADDITION);
a = new Expression2(3);
b = new Expression2(4);
d = new Expression2(a, b, Expression2.ADDITION);
e = new Expression2(c, d, Expression2.MULTIPLICATION);
System.out.println(e.toString() + " = " + e.value());
}
+ Conceptually simple
+ No subtle manipulation of object references
+ Construction process is symmetric
- All types of expression have all attributes
- Wasteful of storage
- Counter-intuitive
- Explicit case by case programming of routines for different types.
- Difficult to expand (division, exponentiation, ...)
- Difficult to maintain
The new implementation is not much of an improvement. But it's the first conceptual step to a radical redesign...
Thanks to Dante Mavec (2005's student) for this suggestion. Suppose we add a couple of static methods to class Expression2 like this:
/** The sum of two expressions */
public static Expression2 sum(Expression2 l, Expression2 r) {
return new Expression2(l, r, ADDITION);
}
/** The product of two expressions */
public static Expression2 product(Expression2 l, Expression2 r) {
return new Expression2(l, r, MULTIPLICATION);
}
Then the code to build our example expression looks like this:
a = new Expression2(1); b = new Expression2(2); c = Expression2.sum(a, b); a = new Expression2(3); b = new Expression2(4); d = Expression2.sum(a, b); e = Expression2.product(c, d);
This makes creating the compound expressions more natural.
public class Stack2 {
private Object value;
private Stack2 rest;
private boolean empty;
public boolean isEmpty() { return empty; }
public Stack2() {
empty = true;
}
public Stack2(Object x, Stack2 s) {
empty = false;
value = x;
rest = s;
}
/** The stack that results from removing the top element */
public Stack2 pop() {
assert !isEmpty()
return rest;
}
/** The top element */
public Object top() {
asert !isEmpty()
return value;
}
...
/** Test the main features */
public static void main(String[] args) {
Stack2 s = new Stack2();
s = new Stack2(new Integer(1), s);
s = new Stack2(new Integer(2), s);
int x = ((Integer) s.top()).intValue();
s = s.pop();
int y = ((Integer) s.top()).intValue();
s = s.pop();
s = new Stack2(new Integer(x + y), s);
System.out.println("Depth = " + s.depth());
System.out.println("Top = " + ((Integer) s.top()).intValue());
}
}
The method depth() is the same as in Stack Version 1.
Notice particularly the redefinition of pop() as a function that returns a stack. This is the key to a much better way of doing things in Version 3.
Copyright © 2006, 2007, Jim Grundy and Ian Barnes & Richard Walker
& Chris Johnson, The Australian National University
$Revision: 1.4 $ $Date: 2009/03/03 06:13:46 $ $Author: cwj $
Feedback & Queries to
comp2100@cs.anu.edu.au