COMP2100/2500 Software Construction
Lecture 4: Recursive Data Types 1 of 4

Summary

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.

Aims

Preparation

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.


What is a recursive data structure?


Stack: The classic recursive data structure

A Stack

There are two kinds of stacks:

Nonempty stacks have two parts:

The ‘rest’ of a nonempty stack is itself a (shorter, and possibly empty) stack. That's why it's a recursive data structure.


Stack Implementation (Version 1)

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();
	}
    }
}

How push(2) works

pushing something on a
stack


How pop() works

popping something off a
stack


Using Stack (Version 1)

    /**
     * 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());
    }

Assessment of Stack (Version 1)

+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.


Expression: Another recursive data structure

Consider a simple language of arithmetic expressions that can include:

For example:

Expressions are usually represented with a tree-like recursive data-structure.

Expression tree diagram


Expression Implementation (Version 1)

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;
    }
}

Using Expression (Version 1)

Expression tree diagram

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());
    }

Assessment of Expression (Version 1)

+ 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)


Expression Implementation (Version 2)

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.


Using Expression (Version 2)

Expression tree diagram

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());
    }

Assessment of EXPRESSION (Version 2)

+ 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...


Adding static methods

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.


Stack gets the same treatment

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