Chapter 13: Data Structures

In the 1920s the Polish logician Jan Łukasiewicz was looking for a way to write logical formulae without brackets. His solution was to place every operator in front of the things it acted on, so that the order of the symbols by itself decided the order of the work. It was an elegant idea in logic, and for the next thirty years it stayed in logic.

Then the machines arrived, and with them a very ordinary problem. A computer handed the expression A + B * C has to work out which operation comes first, keep track of brackets, and hold on to half-finished results while it goes looking for the rest. In the late 1950s the Australian philosopher Charles Hamblin proposed reversing Łukasiewicz’s notation — operators written after their operands — and observed that a machine could evaluate such an expression using a very simple kind of memory: one that could be reached at a single end only. In Germany at about the same time, Friedrich Bauer and Klaus Samelson were building expression evaluation on exactly such a store.

Nobody had invented a new kind of memory. They had only restricted how an ordinary block of memory could be used, and the restriction turned out to be the whole idea. Fig. 13.0-1 shows two such restrictions side by side.

Diagram

Fig. 13.0-1 – two panels side by side. Left panel: a vertical container closed at the bottom and open at the top, holding four stacked cells; a curved arrow entering the open top labelled “in” and a curved arrow leaving the same open top labelled “out”; the closed lower end labelled “closed end”; caption under the panel “one end only”. Right panel: a horizontal channel open at both ends, holding four cells in a row; an arrow entering from the right end labelled “in”, an arrow leaving from the left end labelled “out”; caption under the panel “in at one end, out at the other”

NoteCuriosity Corner

Q1. How can a way of writing expressions that uses no brackets at all never become ambiguous? Q2. A queue can have empty cells lying unused and still refuse to accept a new element. How is that possible, and how is it cured? Q3. If the items of a list are scattered at unrelated places in the memory, how does a program still reach the fifth one? Q4. Why can some lists be walked from the beginning to the end but never from the end back to the beginning? Q5. Three students read the same tree and write down three different orders of the same nodes. Which of them has made a mistake?

This chapter answers all five.

13.1 Data Structures and Abstract Data Types

13.1.1 Why a Program Needs a Data Structure

You already know how to keep many values of the same type together. An array does it: fifty marks in int m[], reached by an index. The array is fast, it is simple, and for a great many problems it is exactly right.

But an array offers you everything at once. Any cell can be read, any cell can be written, at any moment. That freedom is useful when you want it and dangerous when you do not.

Think of a school library counter at the start of a period. Students arrive, join the line, and are served in the order they arrived. Now suppose the counter clerk had an “array” of students and could serve any of them at will. Nothing in the arrangement would stop the last arrival being served first. The fairness of the queue does not come from the students; it comes from the rule about who may be served.

A data structure is that combination: a way of holding data in memory together with a fixed rule about how the data may be put in and taken out.

NoteDefinition

A data structure is an arrangement of data in memory together with the set of operations permitted on it, where the permitted operations, and no others, decide how data may be inserted, accessed and removed.

Storage, and the discipline placed on it

The word “structure” is doing real work in that definition. Two structures can hold identical values in identical memory and still behave completely differently, because they permit different operations.

This chapter deals with structures whose elements sit one after another in a single sequence — a first element, a second, and so on to a last. Such a structure is called a linear data structure. The stack, the queue, the circular queue, the dequeue and the single linked list are all linear. The tree, which arrives at the end of the chapter, is not.

Q. If a stack and a queue can both be built inside an ordinary array, what actually makes them different?

A. Only the permitted operations. The array underneath is the same; the stack refuses to release anything except the most recent item, while the queue refuses to release anything except the oldest. The difference lives in the methods, not in the memory.

13.1.2 The Abstract Data Type

Ask what a queue is and there are two quite different answers.

The first answer describes behaviour: a queue holds items; you may add an item at the rear; you may remove the item at the front; you may ask whether it is empty. Nothing there says how the items are stored.

The second answer describes machinery: the items sit in an int array of capacity 20, two integers front and rear mark the ends, and removing an item means reading q[front] and moving front forward one place.

The first answer is the abstract data type (ADT). The second is one implementation of it.

NoteDefinition

An abstract data type is a description of a data structure given entirely in terms of the operations it permits and what each operation does, with no statement of how the data is stored or how the operations are carried out.

Why bother separating them? Because everything that uses the queue depends on the first answer only. A program that adds jobs to a print list and removes them in turn needs to know that removal takes the oldest job. It does not need to know whether the jobs sit in an array or somewhere else entirely. If you later change the storage, that program should not have to change at all.

The syllabus states this plainly: data structures are to be defined as abstract data types with a well-defined interface. Keeping the two apart also keeps a common confusion at bay.

Table 13.1 – Abstract data type vs data structure

Feature Abstract data type Data structure
What it states The operations permitted and what each one does How the data is actually stored and how each operation is carried out
Level A specification, written for the user of the structure An implementation, written for the machine
Storage mentioned No Yes — arrays, fields, links, capacities
How many can exist One ADT Many different structures can implement the same ADT
Expressed in Java as An interface A class that implements the interface
Changing it Forces every program that uses the structure to change Can be changed silently, provided the operations still behave as promised

13.1.3 Defining a Data Structure Through a Java Interface

Java gives you a construct that holds a specification and nothing else. An interface lists method headings with no bodies; any class that claims to implement the interface must supply a body for every one of them.

Be careful with the word. In ordinary technical English, “interface” loosely means the set of operations something offers — a class has an interface in that sense whether or not the keyword appears anywhere. In Java, interface is a specific language construct. This section uses the language construct, and it is the construct the syllabus asks for.

Here is a small ADT written that way. It describes a collection of marks that can be added to, counted and averaged.

interface Marks
{
    void record(int m);
    boolean isEmpty();
    int count();
    double average();
}

Read what this does and does not say. It says a mark can be recorded, that the collection can report emptiness, that it can report how many marks it holds, and that it can report their average. It says nothing about arrays, capacities or loops. Anyone reading only this file knows exactly how to use a Marks, and nothing at all about how one is built.

Methods declared in an interface are automatically public, so a class implementing the interface must declare each of them public too. Leaving the word out is the commonest slip here, and the message the compiler gives is not obvious the first time you meet it.

dropping public from an implementing method What students write:

class MarkTally implements Marks
{
    int count()
    {
        return n;
    }
}

What happens: the class will not compile. The compiler reports that the method is being given weaker access than the interface promised, because an interface method is public and this one is not. The fix: write public int count(). Every method that implements an interface method must be declared public.

13.1.4 Multiple Implementations of the Same Interface

The point of writing the specification separately is that more than one class can satisfy it. Fig. 13.1-1 shows the shape of the arrangement: one interface above, two unrelated classes below, each promising to provide everything the interface lists.

Diagram

Fig. 13.1-1 – three boxes. Top box labelled “interface Marks”, listing inside it the four method headings record(int), isEmpty(), count(), average(). Below it, two separate boxes side by side, the left labelled “class MarkList” with the note “stores every mark in an array” and the right labelled “class MarkTally” with the note “stores only a count and a running total”. A dashed arrow rises from each lower box to the top box, each arrow labelled “implements”

The two classes below are not variations of one another. MarkList keeps every mark it is given. MarkTally keeps no marks at all — it adds each one into a running total and forgets it. Both can answer every question the interface asks them to answer, and a program written against Marks cannot tell which it is holding.

two implementations of the same marks ADT Problem: Define an interface for a collection of marks offering record, isEmpty, count and average. Implement it twice — once storing every mark in an array, once storing only a count and a running total. Record the marks 72, 65, 88 and 54 in both, and print emptiness before recording and the count and average afterwards. An empty collection reports an average of 0.0. The marks are fixed inside the program.

interface Marks
{
    void record(int m);
    boolean isEmpty();
    int count();
    double average();
}

class MarkList implements Marks
{
    private int data[];
    private int n;

    MarkList(int capacity)
    {
        data = new int[capacity];
        n = 0;
    }

    public void record(int m)
    {
        if (n == data.length)
        {
            System.out.println("MarkList is full");
        }
        else
        {
            data[n] = m;
            n = n + 1;
        }
    }

    public boolean isEmpty()
    {
        return n == 0;
    }

    public int count()
    {
        return n;
    }

    public double average()
    {
        if (n == 0)
        {
            return 0.0;
        }
        int total = 0;
        for (int i = 0; i < n; i++)
        {
            total = total + data[i];
        }
        return (double) total / n;
    }
}

class MarkTally implements Marks
{
    private int n;
    private int total;

    MarkTally()
    {
        n = 0;
        total = 0;
    }

    public void record(int m)
    {
        total = total + m;
        n = n + 1;
    }

    public boolean isEmpty()
    {
        return n == 0;
    }

    public int count()
    {
        return n;
    }

    public double average()
    {
        if (n == 0)
        {
            return 0.0;
        }
        return (double) total / n;
    }
}

class MarksDemo
{
    public static void main(String args[])
    {
        Marks a = new MarkList(5);
        Marks b = new MarkTally();
        int paper[] = {72, 65, 88, 54};
        System.out.println("Empty at the start: " + a.isEmpty() + " " + b.isEmpty());
        for (int i = 0; i < paper.length; i++)
        {
            a.record(paper[i]);
            b.record(paper[i]);
        }
        System.out.println("Count: " + a.count() + " " + b.count());
        System.out.println("Average: " + a.average() + " " + b.average());
    }
}

Sample output:

Empty at the start: true true
Count: 4 4
Average: 69.75 69.75

Variable description:

Variable Type Purpose
data int[] Array in MarkList holding every mark recorded so far
n int Number of marks recorded — a field of MarkList and, separately, of MarkTally
total int Sum of the marks; a local variable in MarkList.average() and a field of MarkTally
m int Formal argument of record(), the mark being recorded
capacity int Formal argument of the MarkList constructor, the size of the array to create
i int Loop counter, in MarkList.average() and in main()
a Marks Reference in main() to the array-based collection
b Marks Reference in main() to the running-total collection
paper int[] The four marks to be recorded

Notes: The two references in main() are declared of type Marks, not of type MarkList and MarkTally. That is the whole design. main() calls only methods the interface promises, so the choice of implementation could be changed on one line without touching anything below it. Note also (double) total / n — the cast is applied to total before the division, so the arithmetic is done in double; without it, total / n would be integer division and the fractional part would be lost.

The two implementations are not equally good

They agree on every answer here, but they are not interchangeable in every situation, and it is worth being honest about the difference.

MarkTally uses two integers no matter how many marks arrive, and computes its average in a single division. MarkList uses one array cell per mark and adds up the whole array every time an average is asked for — one addition per mark stored. On memory and on speed, the tally wins.

But MarkList still holds the marks. Ask it for the highest mark, or how many students scored above the average, and it can answer; a new method would do it. MarkTally cannot, at any price, because the individual marks are gone. It also has no capacity limit, while MarkList refuses marks once its array is full.

That is the ordinary shape of a design choice in this subject. A structure that stores less is cheaper and answers fewer questions. Choosing between them is not a matter of one being correct — it is a matter of knowing which questions you will be asked.

Key in the program above and run it. Then change the first line of main() to Marks a = new MarkList(3); and predict, before running it, exactly what will be printed and how many lines there will be. Run it and check.

13.2 The Stack

13.2.1 The Last In First Out Discipline

An examiner marking a bundle of answer scripts puts each finished script face down on a pile. When a colleague asks to see one, the script that comes off is the one that went down last. The pile is not sorted, not indexed and not searched. It has exactly one working surface, and everything enters and leaves through it.

The undo command in a text editor behaves the same way. Every change you make is remembered; pressing undo reverses the most recent one first, then the one before it. Undo cannot reach into the middle of your editing history and reverse a change from ten minutes ago while leaving the later ones intact.

A stack is this arrangement made into a data structure.

NoteDefinition

A stack is a linear data structure in which insertion and deletion are permitted at one end only, so that the element removed at any moment is always the one most recently inserted and not yet removed.

The single working end is called the top of the stack. The other end is closed. Because the element that goes in last is the one that comes out first, a stack is said to follow the last in first out (LIFO) principle.

Fig. 13.2-1 shows a stack held inside an array of six cells after three elements have been placed in it.

Diagram

Fig. 13.2-1 – an array of six cells drawn as a vertical column, indices written to the right of each cell from 0 at the bottom to 5 at the top. Cells 0, 1 and 2 contain 15, 28 and 37; cells 3, 4 and 5 are empty. A horizontal arrow on the left points at cell 2, labelled “top”. The bottom of the column is marked “closed end”. Above the column, two curved arrows at the same opening, one pointing down into the column labelled “push” and one pointing up out of it labelled “pop”

Notice what is being given up. The array underneath is an ordinary array and cell 0 is perfectly reachable; the stack simply refuses to reach it. That refusal is not a weakness to be worked around — it is the reason the structure is useful, because it guarantees the order in which things come back out.

Table 13.2 – An array vs a stack

Feature Array Stack
Where an element may be inserted At any index within the array At the top only
Where an element may be removed At any index within the array At the top only
How an element is reached Directly, by its index Only by removing everything above it
What decides the order of removal The programmer, freely The order of insertion, reversed
What you must remember to use it The index of every element you care about One index — the top
Typical use Storing a fixed collection to be examined in any order Storing work that must be undone or completed in reverse order

13.2.2 Push and Pop

The two operations of a stack have fixed names. Placing an element on the stack is called a push; removing the topmost element is called a pop.

Both are described most clearly against an array st and an integer top that holds the index of the topmost element. When the stack is empty there is no topmost element, so top is set to −1 — one less than the first valid index. This convention matters, and every method in the class must agree on it: top always names the cell holding the topmost element, never the next free cell.

Work out the method before writing any Java.

push an element onto a stack Input: an array st of capacity SIZE holding the stack, an integer top giving the index of the topmost element (−1 if the stack is empty), and a value ITEM to be inserted. Output: ITEM stored as the new topmost element, or a message reporting that no room is left. Method:

1. If top = SIZE - 1
       display "Stack overflow"
       stop
2. Set top = top + 1
3. Set st[top] = ITEM
4. Stop

Why it works: top always names the cell holding the topmost element, so the cell just above it, at index top + 1, is the first free cell. Raising top by one and storing there keeps that property true. Cost: the same three steps are performed whatever the stack contains, so the best, average and worst cases are identical. In the standard notation for describing cost this is written \(O(1)\), where \(n\) counts the elements currently in the stack: the work does not grow as \(n\) grows.

Popping reverses the movement, and the order of the two steps is not interchangeable.

pop an element from a stack Input: an array st holding the stack and an integer top giving the index of the topmost element (−1 if the stack is empty). Output: the value that was topmost, removed from the stack; or a message reporting that there was nothing to remove. Method:

1. If top = -1
       display "Stack underflow"
       stop
2. Set ITEM = st[top]
3. Set top = top - 1
4. Return ITEM

Why it works: the element at top is the most recently pushed one still present, so it is the one the LIFO rule requires. Lowering top by one makes the element below it the new topmost element; the cell just vacated is no longer part of the stack, and whatever value is still lying in it will be overwritten by the next push. Cost: \(O(1)\) in the best, average and worst case, where \(n\) counts the elements currently in the stack.

Step 2 must come before step 3. Read the value while top still points at it; lower top afterwards.

raising top after storing instead of before What students write:

public void push(int v)
{
    st[top] = v;
    top = top + 1;
}

What happens: this version treats top as the index of the next free cell, while pop() treats it as the index of the topmost element. The two meanings cannot both be right. With top starting at −1, the very first push attempts to write to st[-1] and the program stops with an array index out of bounds error. The fix: raise top first, then store — top = top + 1; followed by st[top] = v;. Decide what top means, and make every method in the class obey that meaning.

13.2.3 Overflow, Underflow and Checking Emptiness

A stack built inside an array has a fixed capacity, so two boundary situations arise, and both must be tested for rather than allowed to happen.

If top has reached the last index of the array, there is no free cell above it and a push cannot be carried out. This is called stack overflow. If top is −1 there is no topmost element and a pop cannot be carried out. This is called stack underflow. Fig. 13.2-2 shows both boundary states.

Diagram

Fig. 13.2-2 – two vertical arrays of four cells side by side, indices 0 to 3 marked from bottom to top. Left array: all four cells filled with 15, 28, 37, 46; arrow pointing at cell 3 labelled “top = 3”; caption beneath “full — any push overflows”. Right array: all four cells empty; arrow pointing to the space just below cell 0 labelled “top = -1”; caption beneath “empty — any pop underflows”

Testing for emptiness deserves a method of its own rather than a comparison scattered through the code. A method isEmpty() that returns top == -1 states the intention in one place, and every other method can then be written to read almost like the algorithm it came from.

There is a further question a pop must answer: what should it return when the stack is empty? A method declared to return int has to return some int. The usual practice, and the one the ISC practical papers follow, is to return an agreed impossible value such as −9999 after printing the underflow message.

This is a workable convention and an imperfect one, and it is worth seeing why. If the stack is holding temperatures, or profits and losses, or any quantity that could legitimately be −9999, the caller cannot tell a real value from a failure report. The convention works because the values stored are usually known to lie in a smaller range — not because the design is sound in general.

Q. Why check isFull() before pushing, when Java would report an array index out of bounds error anyway?

A. Because that error stops the program. A full stack is a normal, expected condition — the caller should be told about it and allowed to carry on, not have the run abandoned. Testing for it turns a crash into a message.

13.2.4 Implementing a Stack Through a Class

Everything needed is now settled: the specification, the two algorithms and the two boundary tests. Following 13.1, the specification goes into an interface and the machinery into a class that implements it.

a stack of integers with a fixed capacity Problem: Define an interface listing the operations of a stack of integers — push, pop, a test for fullness and a test for emptiness. Implement it with a class that stores the elements in an array of a capacity fixed when the object is created, reporting overflow and underflow rather than stopping. A pop from an empty stack returns −9999. Demonstrate the class on a stack of capacity 3, including one push too many and one pop too many. All values are fixed inside the program.

/*
    Purpose: a stack of integers of fixed capacity, offering
    push and pop at one end only, with overflow and underflow
    reported instead of allowed to occur.
*/

interface IntStack
{
    void push(int v);
    int pop();
    boolean isEmpty();
    boolean isFull();
}

class ArrayStack implements IntStack
{
    private int st[];
    private int top;

    ArrayStack(int capacity)
    {
        st = new int[capacity];
        top = -1;
    }

    public boolean isEmpty()
    {
        return top == -1;
    }

    public boolean isFull()
    {
        return top == st.length - 1;
    }

    public void push(int v)
    {
        if (isFull())
        {
            System.out.println("Stack overflow");
        }
        else
        {
            top = top + 1;
            st[top] = v;
        }
    }

    public int pop()
    {
        if (isEmpty())
        {
            System.out.println("Stack underflow");
            return -9999;
        }
        int v = st[top];
        top = top - 1;
        return v;
    }

    void display()
    {
        if (isEmpty())
        {
            System.out.println("Stack is empty");
        }
        else
        {
            System.out.print("Top to bottom:");
            for (int i = top; i >= 0; i--)
            {
                System.out.print(" " + st[i]);
            }
            System.out.println();
        }
    }
}

class StackDemo
{
    public static void main(String args[])
    {
        ArrayStack s = new ArrayStack(3);
        s.push(15);
        s.push(28);
        s.push(37);
        s.display();
        s.push(46);
        System.out.println("Popped: " + s.pop());
        s.display();
        System.out.println("Popped: " + s.pop());
        System.out.println("Popped: " + s.pop());
        System.out.println("Empty now: " + s.isEmpty());
        System.out.println("Popped: " + s.pop());
    }
}

Sample output:

Top to bottom: 37 28 15
Stack overflow
Popped: 37
Top to bottom: 28 15
Popped: 28
Popped: 15
Empty now: true
Stack underflow
Popped: -9999

Variable description:

Variable Type Purpose
st int[] Array in which the elements of the stack are held
top int Index of the topmost element; −1 when the stack is empty
capacity int Formal argument of the constructor, the size of the array to create
v int Formal argument of push(), the value to be inserted; also the local variable in pop() holding the value being removed
i int Loop counter in display(), running from the top downwards
s ArrayStack The stack object created in main()

Notes: display() is not listed in IntStack, because printing is not part of what a stack is — a class is free to offer more than its interface promises. It can be called here only because s is declared of type ArrayStack rather than of type IntStack.

The loop in display() runs downwards from top to 0, so the stack is printed top first. Running it upwards would print the same elements in the reverse order and give a misleading picture of which one comes out next.

The last two lines of output are worth a second look. In System.out.println("Popped: " + s.pop()), the call to s.pop() is evaluated before anything is printed, so the underflow message that pop() prints appears on the line above the “Popped:” line, not below it.

the first six calls made by main(), on a stack of capacity 3

Call top before top after Contents of st after (index 0, 1, 2) Printed
push(15) −1 0 15, –, – —
push(28) 0 1 15, 28, – —
push(37) 1 2 15, 28, 37 —
display() 2 2 15, 28, 37 Top to bottom: 37 28 15
push(46) 2 2 15, 28, 37 Stack overflow
pop() 2 1 15, 28, 37 returns 37

Result: after the six calls the stack holds 15 and 28 with top at 1. The trace shows the two things worth noticing about a stack in an array. The refused push changed nothing at all — top is untouched and no value was written. And the successful pop did not erase 37 from cell 2; it only lowered top, which is enough, because a cell above top is not part of the stack and will be overwritten by the next push.

Add a method int peek() to ArrayStack that returns the topmost value without removing it, returning −9999 if the stack is empty. Call it twice in succession in main() and confirm that both calls return the same value — which is what tells you it is not a disguised pop().

13.2.5 Where Stacks Are Used

The stack turns up wherever work has to be finished, or undone, in the reverse of the order it was started.

The undo history of an editor is the plainest case. So is the back button of a web browser: each page you leave is pushed, and going back pops the most recent one. In both, the discipline matches the problem exactly — you never want to undo a change from the middle of the sequence while keeping the later ones.

A less visible case is the one your own programs already rely on. When a method is called, the point it must return to has to be remembered, and if that method calls another, the second return point must be remembered without losing the first. The returns then happen in reverse order of the calls, which is precisely LIFO. This is why the memory area holding those return points is called the call stack, and why a recursion with no base case eventually exhausts it.

The same reasoning explains why a browser’s back button is easy and its forward button is fussy. Going back is a pure pop. Going forward means the popped page has to have been kept somewhere — on a second stack — and that second stack has to be thrown away the moment you follow a new link, because the old forward history no longer belongs to the path you are on.

The use that matters most for this chapter is the one the opening story pointed at: evaluating and rewriting arithmetic expressions. That is the subject of the next section, and it is worth saying in advance what the stack contributes there. When a computer reads A + B * C from left to right, it meets the + before it can know whether the + should be performed yet. Something must hold the + until its fate is decided. A structure that returns pending operators in the reverse of the order they were set aside is exactly what the job needs.

13.3 Infix, Prefix and Postfix Notations

13.3.1 The Three Ways of Writing an Expression

Write A + B and you are following a habit so old that it feels like the only possibility: the operator sits between the two things it acts on. It is not the only possibility, and for a machine it is not the most convenient one.

There are three places an operator can be put relative to its operands, and each gives a complete notation. Throughout this section expressions are shown in a fixed-width form — A + B, AB+ — so that the exact order of the symbols is visible.

Infix notation puts the operator between its two operands: A + B, P * Q, M - N. This is ordinary mathematical writing.

Prefix notation puts the operator before its two operands: +AB, *PQ, -MN. It is also called Polish notation, after the Polish logician whose work introduced it.

Postfix notation puts the operator after its two operands: AB+, PQ*, MN-. Because it is the reverse arrangement of prefix, it is also called reverse Polish notation.

NoteDefinition

In infix notation a binary operator is written between its two operands; in prefix notation it is written before both of them; in postfix notation it is written after both of them. In all three the operands themselves keep their order relative to one another.

Diagram

Fig. 13.3-1 – three horizontal rows, each showing the same two operand boxes labelled A and B and one small operator box labelled +. Row 1 labelled “prefix”: operator box, then A, then B. Row 2 labelled “infix”: A, then operator box, then B. Row 3 labelled “postfix”: A, then B, then operator box. In each row a brace drawn under the two operand boxes is joined by a line to the operator box, showing which operands the operator acts on

Why the other two notations need no brackets

Now take an expression with two operators: A + B * C. Read it as infix and you have a genuine difficulty. The operand B sits between a + and a *, and nothing in the symbols says which of the two operators gets it. To settle the question you must either bring in an outside rule — that * is performed before + — or add brackets: A + (B * C).

That difficulty is created by the notation itself. An infix operator is separated from one of its operands by whatever else lies in between, so two operators can compete for the same operand.

Now write the same thing in postfix: ABC*+. Reading left to right, A, B and C are operands. The * comes immediately after B and C, so it takes those two. The + then comes after A and after the result of B * C, so it takes those two. There is nothing to decide, and no bracket could change the reading.

Q1. How can a way of writing expressions that uses no brackets at all never become ambiguous?

A. Brackets exist in infix notation to settle a question that infix notation itself creates. Because an infix operator stands between its operands, an operand can lie between two operators and belong to either — so precedence rules or brackets are needed to say which. In prefix and postfix each operator stands immediately alongside the two things it acts on, so the position of the symbols by itself fixes what belongs to what. Brackets are not banned in those notations; there is simply nothing left for them to do.

Table 13.4 – The three notations compared

Feature Infix Prefix Postfix
Position of the operator Between its two operands Before both operands After both operands
Other name — Polish notation Reverse Polish notation
A + B * C is written as A + B * C +A*BC ABC*+
Brackets needed Yes, wherever the intended order differs from the precedence rules Never Never
Precedence rules needed to read it Yes No No
How it is processed Must be converted before evaluation Scanned right to left with a stack Scanned left to right with a stack

13.3.2 Precedence and Associativity in Conversion

Converting an infix expression means recovering the order the writer intended, and that order comes from two rules.

Precedence decides which operator acts first when two different operators compete for an operand. Associativity decides which acts first when two operators of the same precedence compete.

Table 13.5 – Precedence and associativity of the arithmetic operators, highest precedence first

Precedence Operators Associativity
Highest ^ (exponentiation, also written ↑) Right to left
Middle * and / Left to right
Lowest + and - Left to right

Brackets override both. Anything inside a bracket is settled before the bracket takes part in anything outside it.

Associativity is the rule students most often skip, and it changes answers. In A - B - C, the two subtractions have equal precedence and associate left to right, so the expression means (A - B) - C, not A - (B - C). Try it with 10, 4 and 3: the first reading gives 3, the second gives 9.

Exponentiation is the exception in the table, and it is the exception in mathematics too. A ^ B ^ C means A ^ (B ^ C), because ^ associates right to left.

Q. In P / Q * R, which operation is performed first?

A. The division. / and * share the same precedence, so associativity decides, and it is left to right — the operator standing further left acts first. The expression means (P / Q) * R.

13.3.3 Converting Infix to Postfix

The bracketing method

The most reliable way to convert by hand is to make the intended order visible before touching the symbols, in three steps.

First, put in every bracket the precedence and associativity rules imply, until each operator has exactly one pair of brackets around itself and its two operands. Second, working from the innermost bracket outwards, move each operator to just after its two operands. Third, remove all the brackets.

Take P + Q * R - S / T. Fully bracketed by the rules of Table 13.5, this is ((P + (Q * R)) - (S / T)). Fig. 13.3-2 shows the order in which the brackets are then resolved.

Diagram

Fig. 13.3-2 – the expression ((P + (Q * R)) - (S / T)) written across the top with each of its three bracket pairs underlined at a different depth. Three numbered stages listed below it: stage 1 “(Q * R) becomes QR” with an arrow to the rewritten expression ((P + QR) - (S / T)); stage 2”(P + QR) becomes PQR+” with an arrow to (PQR+ - (S / T)); stage 3 “(S / T) becomes ST/, then the outer bracket becomes PQR+ST/-” with an arrow to the final answer PQR*+ST/-

Working through the stages: (Q * R) becomes QR*; then (P + QR*) becomes PQR*+; then (S / T) becomes ST/; and finally the outermost bracket, whose two operands are now PQR*+ and ST/ with the operator -, becomes PQR*+ST/-.

Check the answer by reading it forwards. P, Q, R, then * takes Q and R; then + takes P and Q * R; then S, T, then / takes them; then - takes the two results. That is the expression we started with.

The stack method

A machine cannot bracket an expression by eye, so it uses a stack to hold operators whose fate is not yet decided. The idea is the one 13.2 ended on: when an operator arrives, you cannot yet know whether it should act, because a higher-precedence operator may be waiting further right. So you set it aside, and you take it back out when something arrives that settles the matter.

convert an infix expression to postfix using a stack Input: an infix expression, correctly bracketed, whose operands are single symbols. Assume every operator is one of those in Table 13.5. Output: the equivalent postfix expression. Method:

1. Push '(' onto the stack, and append ')' to the end of the
   input expression.
2. Scan the symbols of the expression from left to right and
   repeat steps 3 to 6 until the stack becomes empty.
3. If the symbol is an operand, append it to the output.
4. If the symbol is '(' , push it onto the stack.
5. If the symbol is an operator, then
       while the top of the stack is an operator whose
       precedence is equal to or higher than that of the
       incoming operator, pop it and append it to the output
       push the incoming operator onto the stack
6. If the symbol is ')' , then
       pop operators from the stack and append each to the
       output until '(' is reached
       pop the '(' and discard it, appending nothing
7. Stop. The output is the postfix expression.

Why it works: an operand is never delayed, because its position in the output is already final. An operator is delayed exactly as long as it might still be outranked by something to its right, and released the moment an operator of equal or lower precedence, or a closing bracket, proves that it cannot be. The extra ( pushed in step 1 and the extra ) appended guarantee that everything still on the stack is flushed at the end. Cost: every symbol is appended once and every operator is pushed once and popped once, so the work is proportional to the length of the expression — \(O(n)\) in the best, average and worst case, where \(n\) counts the symbols in the expression.

One refinement belongs with step 5. “Equal or higher precedence” is right for the left-associative operators, which is all of them except ^. Because ^ associates right to left, an incoming ^ pops only operators of strictly higher precedence — and since nothing outranks ^, it pops nothing.

converting (A + B) * C - D ^ E to postfix by the stack method

Symbol scanned Stack, bottom to top Output so far
— (start) (
( ( (
A ( ( A
+ ( ( + A
B ( ( + AB
) ( AB+
* ( * AB+
C ( * AB+C
- ( - AB+C*
D ( - AB+C*D
^ ( - ^ AB+C*D
E ( - ^ AB+C*DE
) empty AB+C*DE^-

Result: the postfix form is AB+C*DE^-. Two rows repay a second look. At the -, the stack held *, whose precedence is higher, so * was popped into the output before - was pushed. At the ^, the stack held -, whose precedence is lower, so nothing was popped — the - had to keep waiting, and it left the stack only at the final ).

Converting the same expression by the bracketing method gives ((A + B) * C) - (D ^ E), hence AB+, then AB+C*, then DE^, then AB+C*DE^-. The two methods agree, which is the check worth making whenever you are unsure.

13.3.4 Converting Infix to Prefix

The bracketing method converts to prefix just as well. Only the second step changes: instead of moving each operator to just after its two operands, move it to just before them.

Take the same expression, (A + B) * C - D ^ E. Fully bracketed it is (((A + B) * C) - (D ^ E)), and the brackets are resolved from the inside out exactly as before:

  • (A + B) becomes +AB
  • (+AB * C) becomes *+ABC
  • (D ^ E) becomes ^DE
  • the outermost bracket, with operands *+ABC and ^DE and operator -, becomes -*+ABC^DE

So the prefix form is -*+ABC^DE.

Checking a prefix answer is done by reading right to left, or by unfolding from the left. Unfolding: the leading - needs two operands. The first is *+ABC, which is a * applied to +AB and C, that is (A + B) * C. The second is ^DE, that is D ^ E. So the whole is ((A + B) * C) - (D ^ E), which is where we started.

Two things are worth noticing about the two answers, AB+C*DE^- and -*+ABC^DE.

They are not reverses of one another. Reversing AB+C*DE^- gives -^EDC*+BA, in which the operands appear in the wrong order. It is the arrangement of operator and operands that reverses, not the string; the operands themselves keep their original left-to-right order in all three notations.

And in both answers the operators appear in the order in which they will be performed — in postfix reading left to right, in prefix reading right to left. That is not a coincidence; it is what makes both notations directly executable.

reversing the string to get the prefix form What students write:

String prefix = new StringBuffer(postfix).reverse().toString();

What happens: for AB+C*DE^- this produces -^EDC*+BA, which is not the prefix form. It is not even a valid expression: - would take ^ED and C as its operands, and BA is left over with nothing to do. The fix: convert from the infix form again, moving each operator before its operands instead of after them. Prefix and postfix are two conversions of the same infix expression, not two arrangements of the same string.

13.3.5 Evaluating a Postfix Expression With a Stack

Converting is only half the point. A postfix expression can be evaluated in a single left-to-right scan, with a stack holding values instead of operators.

evaluate a postfix expression Input: a postfix expression whose operands are numbers and whose operators are binary. Output: the value of the expression. Method:

1. Scan the symbols of the expression from left to right and
   repeat steps 2 and 3 until the end of the expression.
2. If the symbol is an operand, push it onto the stack.
3. If the symbol is an operator, then
       pop one value and call it B
       pop the next value and call it A
       apply the operator as A operator B
       push the result onto the stack
4. Pop the stack. That value is the answer.

Why it works: at every moment the stack holds exactly those values that have been computed but not yet used. Because postfix places an operator immediately after its two operands, the two values an operator needs are always the two most recently produced — which is to say, the top two on the stack. A correctly formed expression therefore leaves exactly one value on the stack at the end. Cost: each symbol causes at most one push and at most two pops, so the work is proportional to the length of the expression — \(O(n)\) in all cases, where \(n\) counts the symbols.

Step 3 has a trap in it. The first value popped is the operand that stood on the right of the operator, because it was pushed later. Applying the operator the other way round costs nothing for + and * and is silently wrong for - and /.

applying the operator to the popped values in the wrong order What students write:

int a = pop();
int b = pop();
push(a - b);

What happens: for the expression 82-, meaning 8 - 2, the first pop returns 2 and the second returns 8, so this computes 2 - 8 and pushes −6 instead of 6. Addition and multiplication hide the fault completely, so the program looks correct until a subtraction or a division appears. The fix: pop the right-hand operand first — int b = pop(); int a = pop(); push(a - b);. Name the variables so the order is visible in the code.

evaluating the postfix expression 732*-84/+

Symbol Action Stack after, bottom to top
7 operand, push 7
3 operand, push 7, 3
2 operand, push 7, 3, 2
* pop 2, pop 3, push 3 × 2 7, 6
- pop 6, pop 7, push 7 − 6 1
8 operand, push 1, 8
4 operand, push 1, 8, 4
/ pop 4, pop 8, push 8 ÷ 4 1, 2
+ pop 2, pop 1, push 1 + 2 3

Result: the final pop returns 3. The infix form of the expression is 7 - 3 * 2 + 8 / 4, which is indeed 7 − 6 + 2 = 3. Notice that the stack never held more than three values even though the expression has five operands — a value is consumed as soon as its operator arrives.

evaluating a postfix expression of single-digit operands Problem: Read a postfix expression from the keyboard as a single string. Its operands are single digits and its operators are +, -, * and /, with no spaces anywhere. Evaluate it using a stack and print the value. Arithmetic is on whole numbers.

/*
    Purpose: evaluate a postfix expression whose operands are
    single digits, using a stack of intermediate results.
*/

import java.util.Scanner;

class PostfixEval
{
    private int st[];
    private int top;

    PostfixEval(int capacity)
    {
        st = new int[capacity];
        top = -1;
    }

    private void push(int v)
    {
        top = top + 1;
        st[top] = v;
    }

    private int pop()
    {
        int v = st[top];
        top = top - 1;
        return v;
    }

    int evaluate(String exp)
    {
        for (int i = 0; i < exp.length(); i++)
        {
            char c = exp.charAt(i);
            if (c >= '0' && c <= '9')
            {
                push(c - '0');
            }
            else
            {
                int b = pop();
                int a = pop();
                if (c == '+')
                {
                    push(a + b);
                }
                else if (c == '-')
                {
                    push(a - b);
                }
                else if (c == '*')
                {
                    push(a * b);
                }
                else
                {
                    push(a / b);
                }
            }
        }
        return pop();
    }

    public static void main(String args[])
    {
        Scanner in = new Scanner(System.in);
        System.out.print("Enter a postfix expression: ");
        String exp = in.next();
        PostfixEval ob = new PostfixEval(exp.length());
        System.out.println("Value = " + ob.evaluate(exp));
    }
}

Sample output:

Enter a postfix expression: 732*-84/+
Value = 3

Variable description:

Variable Type Purpose
st int[] Stack holding the values computed so far but not yet used
top int Index of the topmost value; −1 when the stack is empty
capacity int Formal argument of the constructor, the size of the stack to create
exp String The postfix expression being evaluated
i int Loop counter, the position of the symbol being scanned
c char The symbol at position i
a int Left-hand operand of the current operator, popped second
b int Right-hand operand of the current operator, popped first
v int Value being pushed or popped inside the helper methods
ob PostfixEval The evaluator object created in main()

Notes: c - '0' converts a digit character to its numeric value. Characters are held as codes, and the codes of '0' to '9' are consecutive, so subtracting the code of '0' from the code of '7' gives 7. Writing push(c) instead would push the character’s code, which is a much larger number.

The stack is created with a capacity equal to the length of the expression. That is generous — the trace above never needed more than three cells — but it is guaranteed to be enough, since nothing can be pushed more often than there are symbols to read.

The class is honest only about correct input, and it is worth naming what it does not do. There is no check for underflow, so an ill-formed expression such as 7+ will stop the program with an array index error. There is no check that the final stack holds exactly one value. And the final else treats every unrecognised character as division, so a stray letter would be silently mistaken for a /. A program meant for real use would validate the expression before evaluating it; this one shows the method, not the defences.

One further point about the arithmetic: / on two int values is integer division, so 7 2 / yields 3, not 3.5. Changing the array and the method to double would fix that, and would change nothing else in the logic.

Run the program on 95-3* and on 59-3*, and predict each answer before you press Enter. Then change int b = pop(); int a = pop(); to int a = pop(); int b = pop();, run both expressions again, and note which of the two answers changes and which does not.

13.4 The Queue

13.4.1 The First In First Out Discipline

Three students send documents to the single printer in the school computer laboratory. The printer cannot print three things at once, so the jobs wait — and the one that arrives first is printed first. Nobody would accept a printer that printed the most recent job first and left the earliest one waiting indefinitely, yet that is exactly what a stack would do.

The lunch counter behaves the same way, and so does a railway reservation window. Arrivals join at the back; service happens at the front. The two ends do different jobs and neither can do the other’s.

A queue is this arrangement made into a data structure.

NoteDefinition

A queue is a linear data structure in which insertion is permitted at one end only, called the rear, and deletion is permitted at the other end only, called the front, so that the element removed at any moment is always the one that has been present the longest.

The end at which elements enter is the rear; the end from which they leave is the front. Because the element that goes in first is the one that comes out first, a queue is said to follow the first in first out (FIFO) principle.

Diagram

Fig. 13.4-1 – an array of six cells drawn as a horizontal row, indices 0 to 5 written beneath the cells. Cells 1, 2 and 3 contain 101, 102 and 103; cells 0, 4 and 5 are empty. An arrow above cell 1 labelled “front”, an arrow above cell 3 labelled “rear”. An arrow entering the row from the right at cell 4 labelled “insertion”, an arrow leaving the row to the left from cell 1 labelled “deletion”

Two position markers are needed rather than one. A stack could get by with top alone, because both operations happened there. A queue has two working ends, so it needs front and rear, and they move independently — and, as 13.4.4 will show, that independence is the source of the structure’s one real weakness.

Table 13.7 – A stack vs a queue

Feature Stack Queue
Principle Last in first out (LIFO) First in first out (FIFO)
Where insertion happens At the top At the rear
Where deletion happens At the top At the front
Position markers needed One, top Two, front and rear
Which element leaves next The most recently inserted The longest present
Order in which items come out The reverse of the order they went in The same as the order they went in
Suits problems where Work must be undone or completed in reverse Work must be served in the order it arrived

13.4.2 Insertion and Deletion

The two operations are called insertion and deletion. They are also called enqueue and dequeue respectively, and that second pair of names needs a warning: the syllabus’s fourth structure, met in 13.6, is itself called a dequeue. To keep the two apart, this chapter says insertion and deletion for the operations, and keeps the word dequeue for the structure.

As with the stack, both markers are set to −1 when the queue is empty, and the queue is empty exactly when front is −1.

insert an element into a queue Input: an array q of capacity SIZE holding the queue, the integers front and rear (both −1 if the queue is empty), and a value ITEM to be inserted. Output: ITEM stored as the new rearmost element, or a message reporting that no room is left. Method:

1. If rear = SIZE - 1
       display "Queue overflow"
       stop
2. If front = -1
       set front = 0
       set rear = 0
   otherwise
       set rear = rear + 1
3. Set q[rear] = ITEM
4. Stop

Why it works: rear always names the cell holding the most recently inserted element, so the next free cell at that end is rear + 1. The special case in step 2 exists because an empty queue has no rearmost element to move past; the first insertion must place both markers on cell 0 together. Cost: \(O(1)\) in the best, average and worst case, where \(n\) counts the elements currently in the queue.

delete an element from a queue Input: an array q holding the queue and the integers front and rear (both −1 if the queue is empty). Output: the value that was at the front, removed from the queue; or a message reporting that there was nothing to remove. Method:

1. If front = -1
       display "Queue underflow"
       stop
2. Set ITEM = q[front]
3. If front = rear
       set front = -1
       set rear = -1
   otherwise
       set front = front + 1
4. Return ITEM

Why it works: the element at front has been present longer than any other, so it is the one FIFO requires. Moving front forward makes the next element the new front. The test front = rear catches the case where the element just removed was the only one left: there is now no front and no rear, so both markers must return to −1, which is what makes the queue report itself empty afterwards. Cost: \(O(1)\) in all cases, where \(n\) counts the elements currently in the queue.

Step 3 is the step that gets left out, and leaving it out does not fail immediately.

not resetting the markers when the last element is deleted What students write:

public int delete()
{
    int v = q[front];
    front = front + 1;
    return v;
}

What happens: deleting the only remaining element leaves front one place beyond rear instead of −1. The queue holds nothing, but isEmpty() still reports false, display() runs a loop from front to rear that produces nothing, and the next delete() cheerfully returns whatever value is lying in the cell beyond the last one. The fix: test for front == rear before moving front, and set both markers back to −1 when it is true. An empty queue must look empty in exactly one way.

13.4.3 Implementing a Queue Through a Class

a queue of token numbers with a fixed capacity Problem: Define an interface listing the operations of a queue of integers — insertion, deletion, a test for fullness and a test for emptiness. Implement it with a class that stores the elements in an array of a capacity fixed when the object is created, reporting overflow and underflow rather than stopping. A deletion from an empty queue returns −9999. Demonstrate the class on a queue of capacity 4 holding token numbers, with deletions interleaved between the insertions. All values are fixed inside the program.

/*
    Purpose: a queue of integers of fixed capacity, with
    insertion at the rear and deletion from the front only.
*/

interface IntQueue
{
    void insert(int v);
    int delete();
    boolean isEmpty();
    boolean isFull();
}

class ArrayQueue implements IntQueue
{
    private int q[];
    private int front;
    private int rear;

    ArrayQueue(int capacity)
    {
        q = new int[capacity];
        front = -1;
        rear = -1;
    }

    public boolean isEmpty()
    {
        return front == -1;
    }

    public boolean isFull()
    {
        return rear == q.length - 1;
    }

    public void insert(int v)
    {
        if (isFull())
        {
            System.out.println("Queue overflow");
        }
        else
        {
            if (isEmpty())
            {
                front = 0;
                rear = 0;
            }
            else
            {
                rear = rear + 1;
            }
            q[rear] = v;
        }
    }

    public int delete()
    {
        if (isEmpty())
        {
            System.out.println("Queue underflow");
            return -9999;
        }
        int v = q[front];
        if (front == rear)
        {
            front = -1;
            rear = -1;
        }
        else
        {
            front = front + 1;
        }
        return v;
    }

    void display()
    {
        if (isEmpty())
        {
            System.out.println("Queue is empty");
        }
        else
        {
            System.out.print("Front to rear:");
            for (int i = front; i <= rear; i++)
            {
                System.out.print(" " + q[i]);
            }
            System.out.println();
        }
    }
}

class QueueDemo
{
    public static void main(String args[])
    {
        ArrayQueue tokens = new ArrayQueue(4);
        tokens.insert(101);
        tokens.insert(102);
        tokens.insert(103);
        tokens.display();
        System.out.println("Served: " + tokens.delete());
        System.out.println("Served: " + tokens.delete());
        tokens.display();
        tokens.insert(104);
        tokens.display();
        tokens.insert(105);
        System.out.println("Full? " + tokens.isFull());
    }
}

Sample output:

Front to rear: 101 102 103
Served: 101
Served: 102
Front to rear: 103
Front to rear: 103 104
Queue overflow
Full? true

Variable description:

Variable Type Purpose
q int[] Array in which the elements of the queue are held
front int Index of the element that will be deleted next; −1 when the queue is empty
rear int Index of the most recently inserted element; −1 when the queue is empty
capacity int Formal argument of the constructor, the size of the array to create
v int Formal argument of insert(), the value being inserted; also the local variable in delete() holding the value being removed
i int Loop counter in display(), running from front to rear
tokens ArrayQueue The queue object created in main()

Notes: display() runs its loop upwards from front to rear, so the queue prints in service order — the next token to be served appears first. This is the opposite direction from the stack’s display(), and for the same reason: in both cases the element printed first is the element that will leave first.

The last three lines of output are the interesting ones, and they are the subject of 13.4.4. The queue held two elements and had two free cells, yet the insertion was refused.

the first six calls made by main(), on a queue of capacity 4

Call front before rear before front after rear after Contents of q after (0, 1, 2, 3) Printed
insert(101) −1 −1 0 0 101, –, –, – —
insert(102) 0 0 0 1 101, 102, –, – —
insert(103) 0 1 0 2 101, 102, 103, – —
display() 0 2 0 2 101, 102, 103, – Front to rear: 101 102 103
delete() 0 2 1 2 101, 102, 103, – returns 101
delete() 1 2 2 2 101, 102, 103, – returns 102

Result: the queue now holds one element, 103, with both markers at 2. The trace shows the two markers behaving quite differently from a stack’s single top. Insertion moved only rear; deletion moved only front. Neither deletion erased anything — cells 0 and 1 still hold 101 and 102 — but both cells now lie behind front, and nothing in this class will ever look at them again.

13.4.4 The Limitation of a Simple Queue

Look again at what happened at the end of the demonstration. The queue had capacity 4. It held 103 and 104 in cells 2 and 3, with cells 0 and 1 standing empty. The insertion of 105 was refused.

The refusal is not a mistake in the program. It is exactly what isFull() was written to test: rear == q.length - 1. And that test is the only one available, because insert() can put an element in one place only — the cell just after rear. Once rear has reached the last cell of the array, there is no such cell, whatever is going on at the other end. Fig. 13.4-2 shows the position.

Diagram

Fig. 13.4-2 – an array of four cells drawn as a horizontal row, indices 0 to 3 written beneath. Cells 0 and 1 empty and shaded, cells 2 and 3 containing 103 and 104. An arrow above cell 2 labelled “front”, an arrow above cell 3 labelled “rear”. An arrow arriving from the right beyond cell 3 labelled “105” is drawn crossed out. A caption beneath the two shaded cells reads “free, but unreachable”

The word “full” is therefore a misnomer for a simple queue. isFull() does not report that the queue has no room; it reports that the rear has run out of array. Those were the same thing at the start, and they stop being the same thing the moment the first deletion happens.

The effect worsens with use. Every deletion moves front one place forward and abandons a cell permanently. A simple queue of capacity 100 that has served 100 tokens is finished, no matter how few tokens it is holding at that moment. Its usable capacity is not 100 elements at a time; it is 100 elements in total, ever.

Q. Could the problem be solved by shifting every element one place left after each deletion, so that front always stays at 0?

A. It could, and the queue would then use its array fully. But the cost changes. Deletion stops being a single step and becomes one move per element still in the queue — work proportional to \(n\) rather than \(O(1)\), where \(n\) counts the elements held. A queue that is deleted from constantly would spend most of its time shifting. The next section fixes the waste without paying that price.

There are two honest ways out. One is to abandon the array and hold the elements in a structure that can grow and shrink at either end without moving anything — that is the linked list of 13.7. The other is to keep the array and stop treating its last cell as the end of the road, by letting rear wrap round to the beginning. That is the circular queue, and it is the subject of 13.5.

13.5 The Circular Queue

13.5.1 Wrapping the Front and Rear Pointers

The waste described in 13.4.4 comes from a single assumption: that the cell after the last cell of the array does not exist. Drop that assumption — decide that the cell after the last one is the first one — and the whole difficulty disappears.

Nothing about the array changes. The array is still a straight run of cells in memory, and cell 3 is still followed in memory by nothing at all. What changes is the arithmetic used to move the markers. Instead of

rear = rear + 1;

the class writes

rear = (rear + 1) % q.length;

The remainder operator does all the work. For an array of length 4, this gives 1, 2, 3 and then 0 again — the marker walks round the array endlessly instead of falling off the end. A queue whose markers move this way is called a circular queue.

NoteDefinition

A circular queue is a queue held in an array in which the positions are treated as forming a ring, so that the position after the last cell of the array is the first cell, and a cell freed by a deletion at the front becomes available again for a later insertion at the rear.

It is helpful to draw the array as a ring, as in Fig. 13.5-1, but this is a picture of the arithmetic and not of the memory.

Diagram

Fig. 13.5-1 – four cells arranged as a ring, drawn as four boxes at the twelve, three, six and nine o’clock positions, labelled with indices 0, 1, 2 and 3 clockwise. Cells 2 and 3 contain 103 and 104; cells 0 and 1 are empty. A curved arrow runs clockwise from cell 3 to cell 0, labelled “(rear + 1) % 4”. An arrow outside the ring points at cell 2 labelled “front”, and another points at cell 3 labelled “rear”

Q2. A queue can have empty cells lying unused and still refuse to accept a new element. How is that possible, and how is it cured?

A. In a simple queue, insertion can only place an element in the cell immediately after rear. Every deletion moves front forward and abandons the cell it leaves behind. So once rear has reached the last cell of the array, insertion has nowhere to go, even though the abandoned cells at the front are empty and untouched — the queue is not out of room, it is out of array. The cure is to make the positions wrap: replace rear + 1 with (rear + 1) % SIZE, so that the cell after the last one is the first one and the abandoned cells come back into use. That structure is the circular queue.

Table 13.8 – A simple queue vs a circular queue

Feature Simple queue Circular queue
How rear advances rear + 1 (rear + 1) % SIZE
When rear is at the last cell Insertion is refused Insertion continues at cell 0, if that cell is free
Cells freed by deletion Abandoned permanently Available again for later insertion
Meaning of “full” The rear has reached the end of the array Every cell holds an element
Test for fullness rear == SIZE - 1 (rear + 1) % SIZE == front
Total elements it can ever serve SIZE, however many are deleted Unlimited; only the number held at once is limited
Cost of insertion and deletion \(O(1)\) \(O(1)\)

13.5.2 Insertion and Deletion in a Circular Queue

Wrapping the markers solves one problem and creates another: telling a full queue from an empty one. In a simple queue, rear == SIZE - 1 said “full” and front == -1 said “empty”, and no confusion was possible. In a ring, front and rear can sit in the same relative position for two entirely different reasons.

The test used here is that the queue is full when the cell the next insertion would use is the cell the next deletion would take — that is, when (rear + 1) % SIZE equals front. Emptiness is still signalled separately, by front being −1, and the two conditions cannot both hold: when front is −1 and rear is −1, (rear + 1) % SIZE is 0, which is not −1.

Keeping a separate signal for emptiness is what allows every cell of the array to be used. Implementations that mark emptiness by front == rear instead have to leave one cell permanently empty to avoid the ambiguity, and can hold only SIZE − 1 elements.

insert an element into a circular queue Input: an array q of capacity SIZE holding the queue, the integers front and rear (both −1 if the queue is empty), and a value ITEM to be inserted. Output: ITEM stored as the new rearmost element, or a message reporting that the queue is full. Method:

1. If front is not -1 and (rear + 1) mod SIZE = front
       display "Queue overflow"
       stop
2. If front = -1
       set front = 0
       set rear = 0
   otherwise
       set rear = (rear + 1) mod SIZE
3. Set q[rear] = ITEM
4. Stop

Why it works: step 1 refuses the insertion exactly when the cell it would use is the one holding the front element, which is the only situation in which the ring has no free cell. Otherwise the cell at (rear + 1) mod SIZE is free, either because it was never used or because a deletion released it. Cost: \(O(1)\) in the best, average and worst case, where \(n\) counts the elements currently held.

delete an element from a circular queue Input: an array q of capacity SIZE holding the queue and the integers front and rear (both −1 if the queue is empty). Output: the value that was at the front, removed from the queue; or a message reporting that there was nothing to remove. Method:

1. If front = -1
       display "Queue underflow"
       stop
2. Set ITEM = q[front]
3. If front = rear
       set front = -1
       set rear = -1
   otherwise
       set front = (front + 1) mod SIZE
4. Return ITEM

Why it works: the front element is still the one that has been present longest, so the FIFO rule is untouched — wrapping changes where the markers go, not which element leaves. The test in step 3 is the same last-element test as in a simple queue, and it is what returns the ring to its empty state. Cost: \(O(1)\) in all cases, where \(n\) counts the elements currently held.

Notice how little has changed. Two occurrences of + 1 became (... + 1) mod SIZE, and one test was rewritten. The discipline of the structure — insert at the rear, delete from the front — is exactly as it was.

keeping the simple queue’s test for fullness What students write:

public boolean isFull()
{
    return rear == q.length - 1;
}

What happens: the wrapping is defeated. The queue still refuses every insertion once rear reaches the last cell, which is the very fault the circular queue exists to remove, so the class behaves exactly like a simple queue with more complicated arithmetic. Worse, after rear has wrapped round to 0 the test becomes false again, so insertions resume and begin overwriting elements that have not yet been deleted, silently. The fix: test the ring, not the array — return (rear + 1) % q.length == front;. In a circular queue, fullness is a relationship between the two markers, not a property of either one alone.

13.5.3 Implementing a Circular Queue Through a Class

a circular queue of token numbers Problem: Implement the same queue interface as in 13.4 with a class in which the front and rear positions wrap round the array, so that cells freed by deletion can be reused. Report overflow and underflow rather than stopping; a deletion from an empty queue returns −9999. Demonstrate the class on a circular queue of capacity 4, using the same token numbers and the same first three operations as the simple queue of 13.4, so that the two can be compared. All values are fixed inside the program.

/*
    Purpose: a circular queue of integers, in which the front and
    rear positions wrap round to the start of the array so that
    every cell freed by a deletion can be used again.
*/

interface IntQueue
{
    void insert(int v);
    int delete();
    boolean isEmpty();
    boolean isFull();
}

class CircularQueue implements IntQueue
{
    private int q[];
    private int front;
    private int rear;

    CircularQueue(int capacity)
    {
        q = new int[capacity];
        front = -1;
        rear = -1;
    }

    public boolean isEmpty()
    {
        return front == -1;
    }

    public boolean isFull()
    {
        return (rear + 1) % q.length == front;
    }

    public void insert(int v)
    {
        if (isFull())
        {
            System.out.println("Queue overflow");
        }
        else
        {
            if (isEmpty())
            {
                front = 0;
                rear = 0;
            }
            else
            {
                rear = (rear + 1) % q.length;
            }
            q[rear] = v;
        }
    }

    public int delete()
    {
        if (isEmpty())
        {
            System.out.println("Queue underflow");
            return -9999;
        }
        int v = q[front];
        if (front == rear)
        {
            front = -1;
            rear = -1;
        }
        else
        {
            front = (front + 1) % q.length;
        }
        return v;
    }

    void display()
    {
        if (isEmpty())
        {
            System.out.println("Queue is empty");
        }
        else
        {
            System.out.print("Front to rear:");
            int i = front;
            while (i != rear)
            {
                System.out.print(" " + q[i]);
                i = (i + 1) % q.length;
            }
            System.out.println(" " + q[rear]);
        }
    }
}

class CircularQueueDemo
{
    public static void main(String args[])
    {
        CircularQueue cq = new CircularQueue(4);
        cq.insert(101);
        cq.insert(102);
        cq.insert(103);
        System.out.println("Served: " + cq.delete());
        System.out.println("Served: " + cq.delete());
        cq.display();
        cq.insert(104);
        cq.insert(105);
        cq.insert(106);
        cq.display();
        cq.insert(107);
        System.out.println("Full? " + cq.isFull());
        System.out.println("Served: " + cq.delete());
        cq.display();
    }
}

Sample output:

Served: 101
Served: 102
Front to rear: 103
Front to rear: 103 104 105 106
Queue overflow
Full? true
Served: 103
Front to rear: 104 105 106

Variable description:

Variable Type Purpose
q int[] Array in which the elements of the circular queue are held
front int Index of the element that will be deleted next; −1 when the queue is empty
rear int Index of the most recently inserted element; −1 when the queue is empty
capacity int Formal argument of the constructor, the size of the array to create
v int Formal argument of insert(), the value being inserted; also the local variable in delete() holding the value being removed
i int Position marker in display(), walking round the ring from front
cq CircularQueue The circular queue object created in main()

Notes: Compare the fourth line of output with the corresponding moment in 13.4. Both queues have capacity 4 and both were given tokens 101 to 106 with two deletions in between. The simple queue was refusing insertions while holding two elements; this one holds four, which is every cell it has.

isFull() is safe to call on an empty queue even though it does not test emptiness first. When the queue is empty both markers are −1, so (rear + 1) % q.length is 0 while front is −1, and the test correctly returns false.

display() cannot use a simple for loop from front to rear, because in a wrapped queue front may be greater than rear — in the fourth line of output, front is 2 and rear is 1. The while loop instead walks round the ring from front, stopping when it reaches rear, and the last element is printed after the loop. The loop is written this way, rather than as “walk while i is not past rear”, because front == rear is a legitimate state meaning one element, not zero.

The reuse is not free. Everything about a circular queue is harder to read than the simple version — the arithmetic, the fullness test and the display loop — and every one of those places is somewhere a fault can hide. What you buy with that complication is an array whose cells can be used indefinitely rather than once each.

Practical link: circular queue implemented through a class — file this under the queue assignments in your work file. Write a class CircularBuffer that holds the last few readings taken by a laboratory sensor. The constructor fixes the capacity. A method record(int reading) inserts a reading at the rear; if the buffer is already full it first deletes the oldest reading, so that recording never fails and the buffer always holds the most recent readings only. A method showAll() prints the readings held, oldest first, and a method oldest() returns the reading at the front without removing it. Test with a capacity of 5 and the readings 21, 23, 22, 26, 25, 27, 24 recorded in that order — the buffer should then show 22, 26, 25, 27, 24. Test also with the unseen input of only three readings recorded into a buffer of capacity 5.

13.6 The Dequeue

13.6.1 A Queue Open at Both Ends

A stack has one working end. A queue has two, but each is allowed to do one job only — arrivals at the rear, departures at the front. The next question almost asks itself. What if both ends were allowed to do both jobs?

Consider the “recently opened files” list that most editors keep. Open a file and it goes to the top of the list. Open enough files and the oldest entry falls off the bottom to keep the list at a fixed length. Additions happen at one end; removals happen at both — deliberately at the top when a file is reopened and moves up, and automatically at the bottom when the list is full. Neither a stack nor a queue can do that.

Or take an ordinary school office counter. Students are normally served from the front. But a student who reaches the counter and finds a form missing steps aside and is put back at the front of the line once the form is fetched, rather than sent to the back. That is an insertion at the front of a queue, which a queue forbids.

A structure that permits insertion and deletion at both ends is called a dequeue, short for double ended queue. It is written deque in many books; the syllabus spells it dequeue, and this chapter follows the syllabus.

NoteDefinition

A dequeue is a linear data structure in which insertion and deletion are both permitted at either end, so that an element may be added at the front or at the rear, and removed from the front or from the rear.

Diagram

Fig. 13.6-1 – an array of five cells drawn as a horizontal row, indices 0 to 4 written beneath. Cells 1, 2 and 3 contain 20, 30 and 40; cells 0 and 4 are empty. An arrow above cell 1 labelled “front” and an arrow above cell 3 labelled “rear”. At the left-hand end, two curved arrows: one pointing into the row labelled “insert at front”, one pointing out of the row labelled “delete from front”. At the right-hand end, two matching curved arrows labelled “insert at rear” and “delete from rear”

A dequeue therefore has four operations where a queue has two: insertion at the front, insertion at the rear, deletion from the front and deletion from the rear.

Table 13.9 – A queue vs a dequeue

Feature Queue Dequeue
Insertion permitted at The rear only Either end
Deletion permitted at The front only Either end
Number of operations Two Four
Order in which elements leave Fixed by the order they arrived Chosen by the caller, one end at a time
What the structure guarantees Strict first in first out service Only that elements are taken from an end, never from the middle
Can imitate Nothing else Both a stack and a queue

That last row is worth expanding, because it cuts both ways.

Use a dequeue’s insertFront and deleteFront only, and it behaves exactly as a stack: everything enters and leaves at one end. Use insertRear and deleteFront only, and it behaves exactly as a queue. A dequeue can therefore stand in for either.

But that flexibility is bought by giving up a guarantee. A queue cannot serve out of order, and in a program that must be fair — print jobs, tokens at a counter — being unable to do the wrong thing is the whole value of the structure. A dequeue makes no such promise; nothing in it prevents a caller from removing the newest element every time. Choosing a dequeue where a queue would do is not a free upgrade. It removes a safeguard.

Q. If a dequeue can imitate both a stack and a queue, why does anyone still write a stack class?

A. Because a class that offers only push and pop cannot be misused, while a dequeue offering four operations can. Restricting what a structure allows is not a limitation to be worked around — it is a statement of what the program is permitted to do, checked by the compiler rather than by a comment.

13.6.2 Insertion and Deletion at Either End

Half the work is already done. Inserting at the rear and deleting from the front are precisely the two operations of 13.5 — the algorithms, the tests and the wrap-round arithmetic are unchanged. What is new is the pair that works at the opposite end.

The dequeue built here is circular, for the reason 13.5 gave: without wrapping, the waste is not merely repeated but doubled, since front would run out of array at index 0 while rear ran out at the last index. Both markers therefore move by the remainder arithmetic, and both must be able to move in either direction.

Moving front backwards is the one genuinely new piece of arithmetic. From cell 0 the previous cell is the last one, and Fig. 13.6-2 shows the step.

Diagram

Fig. 13.6-2 – an array of five cells drawn as a horizontal row, indices 0 to 4 beneath. An arrow above cell 0 labelled “front, before”, a dashed curved arrow leaving the left edge of the row, passing under it, and re-entering at the right edge to point at cell 4, labelled “(front - 1 + 5) % 5”. An arrow above cell 4 labelled “front, after”

insert an element at the front of a dequeue Input: an array dq of capacity SIZE holding the dequeue, the integers front and rear (both −1 if the dequeue is empty), and a value ITEM to be inserted. Output: ITEM stored as the new front element, or a message reporting that the dequeue is full. Method:

1. If front is not -1 and (rear + 1) mod SIZE = front
       display "Dequeue overflow"
       stop
2. If front = -1
       set front = 0
       set rear = 0
   otherwise
       set front = (front - 1 + SIZE) mod SIZE
3. Set dq[front] = ITEM
4. Stop

Why it works: the fullness test is the same as for a circular queue, because fullness is a property of the ring and does not depend on which end is being added to. Step 2 moves front one place backwards round the ring; the cell it lands on is free whenever the dequeue is not full, since the elements occupy an unbroken run from front forward to rear. Cost: \(O(1)\) in the best, average and worst case, where \(n\) counts the elements currently held.

delete an element from the rear of a dequeue Input: an array dq of capacity SIZE holding the dequeue and the integers front and rear (both −1 if the dequeue is empty). Output: the value that was at the rear, removed from the dequeue; or a message reporting that there was nothing to remove. Method:

1. If front = -1
       display "Dequeue underflow"
       stop
2. Set ITEM = dq[rear]
3. If front = rear
       set front = -1
       set rear = -1
   otherwise
       set rear = (rear - 1 + SIZE) mod SIZE
4. Return ITEM

Why it works: the rear element is the last one in the run, so removing it shortens the run from that end and leaves the front untouched. As always, the front = rear test catches the removal of the only element, which must return both markers to −1. Cost: \(O(1)\) in all cases, where \(n\) counts the elements currently held.

The addition of SIZE before taking the remainder is not decoration.

stepping a marker backwards without adding SIZE What students write:

front = (front - 1) % dq.length;

What happens: when front is 0 this computes (-1) % 5. In Java the remainder takes the sign of the left-hand operand, so the answer is −1, not 4. The very next statement, dq[front] = v;, then attempts to write to dq[-1] and the program stops with an array index error. On paper the same expression looks correct, because in ordinary arithmetic −1 modulo 5 is taken to be 4 — which is why this fault survives a careful desk check and fails at once on the machine. The fix: add the capacity before taking the remainder — front = (front - 1 + dq.length) % dq.length;. Adding SIZE cannot change the answer for any value that was already in range, and it lifts the one value that was not, −1, into range.

13.6.3 Implementing a Dequeue Through a Class

a circular dequeue of integers Problem: Define an interface listing the four operations of a dequeue of integers together with tests for emptiness and fullness. Implement it with a class holding the elements in an array of a capacity fixed when the object is created, with both markers wrapping round the array. Report overflow and underflow rather than stopping; a deletion from an empty dequeue returns −9999. Demonstrate the class on a dequeue of capacity 5, using all four operations and filling every cell. All values are fixed inside the program.

/*
    Purpose: a double ended queue of integers held in an array
    whose positions wrap round, allowing insertion and deletion
    at both the front and the rear.
*/

interface IntDequeue
{
    void insertFront(int v);
    void insertRear(int v);
    int deleteFront();
    int deleteRear();
    boolean isEmpty();
    boolean isFull();
}

class Dequeue implements IntDequeue
{
    private int dq[];
    private int front;
    private int rear;

    Dequeue(int capacity)
    {
        dq = new int[capacity];
        front = -1;
        rear = -1;
    }

    public boolean isEmpty()
    {
        return front == -1;
    }

    public boolean isFull()
    {
        return (rear + 1) % dq.length == front;
    }

    public void insertFront(int v)
    {
        if (isFull())
        {
            System.out.println("Dequeue overflow");
        }
        else
        {
            if (isEmpty())
            {
                front = 0;
                rear = 0;
            }
            else
            {
                front = (front - 1 + dq.length) % dq.length;
            }
            dq[front] = v;
        }
    }

    public void insertRear(int v)
    {
        if (isFull())
        {
            System.out.println("Dequeue overflow");
        }
        else
        {
            if (isEmpty())
            {
                front = 0;
                rear = 0;
            }
            else
            {
                rear = (rear + 1) % dq.length;
            }
            dq[rear] = v;
        }
    }

    public int deleteFront()
    {
        if (isEmpty())
        {
            System.out.println("Dequeue underflow");
            return -9999;
        }
        int v = dq[front];
        if (front == rear)
        {
            front = -1;
            rear = -1;
        }
        else
        {
            front = (front + 1) % dq.length;
        }
        return v;
    }

    public int deleteRear()
    {
        if (isEmpty())
        {
            System.out.println("Dequeue underflow");
            return -9999;
        }
        int v = dq[rear];
        if (front == rear)
        {
            front = -1;
            rear = -1;
        }
        else
        {
            rear = (rear - 1 + dq.length) % dq.length;
        }
        return v;
    }

    void display()
    {
        if (isEmpty())
        {
            System.out.println("Dequeue is empty");
        }
        else
        {
            System.out.print("Front to rear:");
            int i = front;
            while (i != rear)
            {
                System.out.print(" " + dq[i]);
                i = (i + 1) % dq.length;
            }
            System.out.println(" " + dq[rear]);
        }
    }
}

class DequeueDemo
{
    public static void main(String args[])
    {
        Dequeue d = new Dequeue(5);
        d.insertRear(20);
        d.insertRear(30);
        d.insertFront(10);
        d.display();
        d.insertFront(5);
        d.insertRear(40);
        d.display();
        System.out.println("Removed from front: " + d.deleteFront());
        System.out.println("Removed from rear: " + d.deleteRear());
        d.display();
        d.insertRear(50);
        d.insertRear(60);
        d.display();
        d.insertRear(70);
    }
}

Sample output:

Front to rear: 10 20 30
Front to rear: 5 10 20 30 40
Removed from front: 5
Removed from rear: 40
Front to rear: 10 20 30
Front to rear: 10 20 30 50 60
Dequeue overflow

Variable description:

Variable Type Purpose
dq int[] Array in which the elements of the dequeue are held
front int Index of the element at the front; −1 when the dequeue is empty
rear int Index of the element at the rear; −1 when the dequeue is empty
capacity int Formal argument of the constructor, the size of the array to create
v int Formal argument of the insertion methods, the value being inserted; also the local variable in the deletion methods holding the value being removed
i int Position marker in display(), walking round the ring from front
d Dequeue The dequeue object created in main()

Notes: The four operations pair up neatly. insertRear and deleteFront are the circular queue’s two methods, copied without change; insertFront and deleteRear are their mirror images, with every + 1 replaced by - 1 + dq.length. Reading the class as two mirrored pairs rather than as four separate methods makes it much shorter to remember.

The output repays a careful reading against the array. After the third call the elements 10, 20 and 30 are held in cells 4, 0 and 1 — insertFront pushed the front marker from 0 backwards to 4. The printed order is still 10, 20, 30, because display() walks the ring from front rather than the array from index 0. The order elements are stored in and the order they are served in have parted company completely, and only the markers know the difference.

The overflow at the end comes with every one of the five cells occupied, which is the point of the circular arithmetic. Many textbooks show a dequeue built on a plain, non-wrapping array; in that version insertFront must refuse as soon as front reaches 0, so a dequeue of capacity 5 that has had one element inserted can accept nothing at all at the front.

Add a method int peekRear() that returns the rear element without removing it, and then use insertFront, deleteFront and peekRear alone to make the class behave as a stack. Push 11, 22 and 33 through insertFront, then call deleteFront three times, and confirm that the values come back in the order 33, 22, 11.

13.7 The Single Linked List

13.7.2 Creating and Traversing a Single Linked List

The rest of this section builds one class, SingleList, a method at a time. Each method is shown as it is explained; the complete, runnable class is collected at the end of 13.7.7.

The class holds one field, and it is the only thing it needs to hold.

class SingleList
{
    private Node start;

    SingleList()
    {
        start = null;
    }
}

A new list has no nodes, so start is null. Note that there is no capacity, no size and no array. The list grows one node at a time, and how far it can grow is decided by the memory available when it grows, not by a number chosen in advance.

To do anything with the list, a program must be able to walk along it. Walking a linked list from the first node to the last, doing something at each one, is called traversal.

traverse a single linked list Input: start, the reference to the first node of the list. Output: every node visited once, in order from the first to the last. Method:

1. Set PTR = start
2. Repeat while PTR is not null
       process the data field of PTR
       set PTR = the link field of PTR
   endwhile
3. Stop

Why it works: start names the first node, and each node names the next, so following the link field repeatedly reaches every node exactly once and in order. The last node’s link is null, which is what stops the loop; because a new list has start equal to null, the same loop correctly does nothing at all on an empty list. Cost: one step per node, so \(O(n)\) in the best, average and worst case, where \(n\) counts the nodes in the list.

PTR must be a separate variable. Walking with start itself would move start along and leave it pointing at the last node, losing every node before it.

    void display()
    {
        if (isEmpty())
        {
            System.out.println("List is empty");
        }
        else
        {
            Node ptr = start;
            System.out.print("List:");
            while (ptr != null)
            {
                System.out.print(" " + ptr.data);
                ptr = ptr.link;
            }
            System.out.println();
        }
    }

Q3. If the items of a list are scattered at unrelated places in the memory, how does a program still reach the fifth one?

A. By walking. There is no formula that turns a position into an address, as there is for an array, because the addresses are unrelated. What the program has instead is a trail: start gives the address of the first node, that node’s link field gives the address of the second, and so on. Reaching the fifth element means following the link four times. The scattering costs nothing in storage and everything in access — an array reaches its fifth element in one step regardless of position, while a linked list reaches it in four, and reaches its five-hundredth in four hundred and ninety-nine.

Creating a list means making nodes and linking them, which is just repeated insertion. Rather than write a separate creation method, this class builds a list by calling its insertion methods, which 13.7.4 develops.

13.7.3 Checking Emptiness

Of all the operations the syllabus names for a linked list, this is the shortest, and it is worth a method of its own for the same reason a stack’s isEmpty() was.

    boolean isEmpty()
    {
        return start == null;
    }

A list is empty exactly when start holds null. There is no separate counter to keep correct, no marker to reset, and no way for the test to disagree with the facts — which is one quiet advantage of this structure over the array-based ones, where emptiness had to be signalled by a convention that every method was obliged to maintain.

Note the comparison operator. start == null compares a reference with null; writing start.equals(null) would attempt to call a method on a reference that may hold no object at all.

13.7.4 Insertion into a Single Linked List

Insertion always follows the same three movements: make the new node, point it at whatever should follow it, and make whatever should precede it point at the new node. The three cases differ only in what “precede” and “follow” mean.

13.7.4.1 Insertion at the Beginning

insert a node at the beginning of a single linked list Input: start, the reference to the first node, and a value ITEM. Output: a new node holding ITEM placed before all existing nodes, with start naming it. Method:

1. Create a new node TEMP with data field ITEM
2. Set the link field of TEMP = start
3. Set start = TEMP
4. Stop

Why it works: step 2 makes the old first node follow the new one, and step 3 makes the new one the node start names. Order matters: if start were changed first, the address of the old first node would be gone and with it the rest of the list. On an empty list, start is null, so step 2 sets the new node’s link to null — exactly right for a list of one node, with no special case needed. Cost: \(O(1)\) in all cases; the work does not depend on the number of nodes.

    void insertBeginning(int v)
    {
        Node temp = new Node(v);
        temp.link = start;
        start = temp;
    }

13.7.4.2 Insertion at the End

Adding at the other end is not symmetrical, and the reason is the one 13.7.1 gave: nothing in the list names the last node, so it has to be found.

insert a node at the end of a single linked list Input: start, the reference to the first node, and a value ITEM. Output: a new node holding ITEM placed after all existing nodes. Method:

1. Create a new node TEMP with data field ITEM
2. If start is null
       set start = TEMP
       stop
3. Set PTR = start
4. Repeat while the link field of PTR is not null
       set PTR = the link field of PTR
   endwhile
5. Set the link field of PTR = TEMP
6. Stop

Why it works: step 4 stops on the node whose link is null, which is the last node by definition. Note the test — link of PTR is not null, not PTR is not null. Walking until PTR itself becomes null would run one step too far and leave nothing to attach the new node to. The empty list is a genuine special case here, because there is no last node to find. Cost: the loop visits every node, so \(O(n)\) in the best, average and worst case, where \(n\) counts the nodes already in the list.

    void insertEnd(int v)
    {
        Node temp = new Node(v);
        if (isEmpty())
        {
            start = temp;
        }
        else
        {
            Node ptr = start;
            while (ptr.link != null)
            {
                ptr = ptr.link;
            }
            ptr.link = temp;
        }
    }

Diagram

Fig. 13.7-2 – two panels, one above the other. Upper panel labelled “insertion at the beginning”: three existing nodes containing 41, 52, 63 linked left to right with null in the last link; a new node containing 30 drawn below and to the left; an arrow numbered 1 from the link box of node 30 to node 41; the “start” box drawn with its old arrow to node 41 crossed out and a new arrow numbered 2 to node 30. Lower panel labelled “insertion at the end”: the same three nodes; a new node containing 74 drawn below and to the right; the link box of node 63 shown with its null crossed out and an arrow numbered 1 to node 74; the link box of node 74 marked null; a marker labelled “ptr” above node 63 with a caption “found by walking from start”

the two ends of a single linked list Here \(n\) counts the nodes in the list. Inserting at the beginning changes two link fields and never looks at a node, so it costs \(O(1)\). Inserting at the end must first find the last node, which means following \(n - 1\) links, so it costs \(O(n)\). The two ends of the structure are not equally cheap, and a program that repeatedly appends to a long list pays for it every time. Keeping a second reference to the last node would make both ends \(O(1)\), at the cost of one more field to keep correct in every method.

13.7.4.3 Insertion in the Middle

To place a node after the n-th node, that node must be found by counting along from start. Once found, the two link changes are the same two as before, and their order is the thing to get right.

insert a node after the n-th node of a single linked list Input: start, a value ITEM, and a position N counted from 1 at the first node. Output: a new node holding ITEM placed immediately after the N-th node, or a message if there is no N-th node. Method:

1. If N < 1
       display "No such position"
       stop
2. Set PTR = start and COUNT = 1
3. Repeat while PTR is not null and COUNT < N
       set PTR = the link field of PTR
       set COUNT = COUNT + 1
   endwhile
4. If PTR is null
       display "No such position"
       stop
5. Create a new node TEMP with data field ITEM
6. Set the link field of TEMP = the link field of PTR
7. Set the link field of PTR = TEMP
8. Stop

Why it works: the loop in step 3 advances PTR exactly N − 1 times, so PTR ends on the N-th node, or on null if the list is shorter than N. Step 6 makes the new node point at whatever used to follow the N-th node; step 7 makes the N-th node point at the new one. The list is unbroken before step 6 and unbroken after step 7. Cost: the loop makes at most N − 1 steps, so \(O(n)\) in the worst case and \(O(1)\) in the best, where \(n\) counts the nodes in the list.

Diagram

Fig. 13.7-3 – four nodes in a row containing 30, 41, 52 and 63, linked left to right, null in the last link, with the “start” box and its arrow to node 30. A marker labelled “ptr” above node 52 with the caption “the 3rd node”. A new node containing 57 drawn below, between nodes 52 and 63. An arrow numbered 1 from the link box of node 57 to node 63, labelled “temp.link = ptr.link”. The existing arrow from the link box of node 52 to node 63 is crossed out, and an arrow numbered 2 runs from the link box of node 52 to node 57, labelled “ptr.link = temp”

making the two link changes in the wrong order What students write:

ptr.link = temp;
temp.link = ptr.link;

What happens: the first statement makes the third node point at the new node. The address of the fourth node was held only in ptr.link, and it has just been overwritten, so it is gone. The second statement then reads ptr.link — which now holds the address of temp — and sets temp.link to temp. The new node points at itself, everything after the insertion point is lost, and display() runs forever. The fix: attach the new node to the rest of the list before detaching the rest of the list — temp.link = ptr.link; and then ptr.link = temp;. The general rule for every linked list operation is that the last thing you may overwrite is the only remaining copy of an address.

    void insertAfter(int v, int n)
    {
        if (n < 1)
        {
            System.out.println("No such position");
            return;
        }
        Node ptr = start;
        int count = 1;
        while (ptr != null && count < n)
        {
            ptr = ptr.link;
            count = count + 1;
        }
        if (ptr == null)
        {
            System.out.println("No such position");
        }
        else
        {
            Node temp = new Node(v);
            temp.link = ptr.link;
            ptr.link = temp;
        }
    }

13.7.5 Deletion from a Single Linked List

Deleting the n-th node means making the node before it point past it. That is the difficulty: from the n-th node there is no way back to the (n−1)-th, so the walk must carry a second reference one step behind.

delete the n-th node of a single linked list Input: start and a position N counted from 1 at the first node. Output: the N-th node removed from the list, or a message if there is no N-th node. Method:

1. If start is null or N < 1
       display "No such position"
       stop
2. If N = 1
       set start = the link field of start
       stop
3. Set PTR = start, PREV = null and COUNT = 1
4. Repeat while PTR is not null and COUNT < N
       set PREV = PTR
       set PTR = the link field of PTR
       set COUNT = COUNT + 1
   endwhile
5. If PTR is null
       display "No such position"
       stop
6. Set the link field of PREV = the link field of PTR
7. Set the link field of PTR = null
8. Stop

Why it works: PREV is always one node behind PTR, so when the loop stops with PTR on the N-th node, PREV is on the node before it. Step 6 joins that node to whatever followed the N-th, which removes the N-th from the chain. Deleting the first node is a separate case in step 2 because it has no predecessor; the reference that must change is start itself. Cost: \(O(n)\) in the worst case and \(O(1)\) in the best, where \(n\) counts the nodes in the list.

Diagram

Fig. 13.7-4 – four nodes in a row containing 30, 41, 52 and 63, with the “start” box and its arrow to node 30 and null in the last link. A marker labelled “prev” above node 41 and a marker labelled “ptr” above node 52. The existing arrow from the link box of node 41 to node 52 is crossed out; a curved arrow numbered 1 runs from the link box of node 41 over node 52 to node 63, labelled “prev.link = ptr.link”. The arrow from the link box of node 52 to node 63 is crossed out and its link box marked null, numbered 2. Node 52 is drawn in outline with the caption “no longer reachable”

Step 7 is not strictly necessary — the node has already left the list at step 6 — but it is good practice, because it leaves no stray link into a list the node is no longer part of.

Nothing else is needed. In Java, an object that no reference names any longer is reclaimed automatically by the memory manager, so there is no instruction to free the deleted node and none is written. Books written for languages without automatic reclamation include such a step; in Java it would not compile.

13.7.6 Extracting an Element or a Sublist

Extracting a single element is traversal that stops early. Walk from start, counting, and when the count reaches the wanted position, return that node’s data field. The list is not changed.

    int elementAt(int n)
    {
        if (n < 1)
        {
            System.out.println("No such position");
            return -9999;
        }
        Node ptr = start;
        int count = 1;
        while (ptr != null && count < n)
        {
            ptr = ptr.link;
            count = count + 1;
        }
        if (ptr == null)
        {
            System.out.println("No such position");
            return -9999;
        }
        return ptr.data;
    }

Extracting a sublist means producing a list of the elements lying between two given positions. There is a decision to make first, and it is a design decision rather than a coding one: should the extracted portion be cut out of the original list, or copied from it?

Cutting it out is quicker — two link changes and no new nodes — but it destroys the original. Copying leaves the original intact and costs one new node per element extracted. This class copies, because a method that silently damages the list it was asked to read is a poor neighbour.

extract a sublist from a single linked list Input: start, a starting position FROM and an ending position TO, both counted from 1. Output: a new list holding copies of the elements from position FROM to position TO inclusive; an empty list if the positions do not describe a run of elements that exists. Method:

1. Create an empty list OUT
2. If FROM < 1 or TO < FROM
       return OUT
3. Set PTR = start and COUNT = 1
4. Repeat while PTR is not null and COUNT < FROM
       set PTR = the link field of PTR
       set COUNT = COUNT + 1
   endwhile
5. Repeat while PTR is not null and COUNT <= TO
       insert the data field of PTR at the end of OUT
       set PTR = the link field of PTR
       set COUNT = COUNT + 1
   endwhile
6. Return OUT

Why it works: the first loop walks to the FROM-th node, the second copies elements until either the TO-th node has been copied or the list runs out. Both loops test PTR is not null first, so a FROM or TO beyond the end of the list produces a shorter result rather than a failure. Cost: with \(n\) the number of nodes in the original list and \(k\) the number extracted, the walking costs \(O(n)\). Each append to OUT walks the part of OUT already built, so the appends cost about \(k^2 / 2\) steps in total — acceptable for the small sublists this method is meant for, and worth replacing with a kept reference to OUT’s last node if \(k\) is ever large.

    SingleList sublist(int from, int to)
    {
        SingleList out = new SingleList();
        if (from < 1 || to < from)
        {
            return out;
        }
        Node ptr = start;
        int count = 1;
        while (ptr != null && count < from)
        {
            ptr = ptr.link;
            count = count + 1;
        }
        while (ptr != null && count <= to)
        {
            out.insertEnd(ptr.data);
            ptr = ptr.link;
            count = count + 1;
        }
        return out;
    }

13.7.7 Reversing a Single Linked List

Reversal is the operation that shows what a linked list really is. Reversing an array means moving values about. Reversing a linked list means moving no values at all: every node stays exactly where it is in memory, and only the direction of the arrows changes.

The method walks the list once, and at each node turns that node’s link field round to point at the node behind instead of the node ahead. Doing so destroys the only record of what came ahead — so before turning a link round, the address it holds must be saved. Three references are therefore in play at once.

reverse a single linked list Input: start, the reference to the first node. Output: the same nodes linked in the opposite order, with start naming what was the last node. Method:

1. Set PREV = null and PTR = start
2. Repeat while PTR is not null
       set NEXT = the link field of PTR
       set the link field of PTR = PREV
       set PREV = PTR
       set PTR = NEXT
   endwhile
3. Set start = PREV
4. Stop

Why it works: at the top of each pass, PREV names the head of the portion already reversed and PTR names the head of the portion not yet touched. Saving NEXT first preserves the way forward; turning PTR’s link to PREV moves one more node from the untouched portion to the reversed one; the last two assignments move both markers on by one. When PTR becomes null, every node has been turned and PREV names what was the last node, which is now the first. The original first node was given a link of null on the very first pass, which is correct — it is now the last. Cost: \(O(n)\) in the best, average and worst case, where \(n\) counts the nodes. No new node is created and no data value is copied.

Diagram

Fig. 13.7-5 – four nodes containing 41, 52, 57 and 63 drawn in a row, showing one pass of the loop. The links of nodes 41 and 52 already point leftwards, with node 41’s link box marked null. Markers above the nodes: “prev” above node 52, “ptr” above node 57, “next” above node 63. The arrow from node 57 to node 63 is crossed out, and a new arrow from the link box of node 57 points left to node 52, labelled “ptr.link = prev”. Two small captions: to the left of node 52, “already reversed”; to the right of node 63, “not yet touched”

    void reverse()
    {
        Node prev = null;
        Node ptr = start;
        while (ptr != null)
        {
            Node next = ptr.link;
            ptr.link = prev;
            prev = ptr;
            ptr = next;
        }
        start = prev;
    }

reversing the list 41 → 52 → 57 → 63

Pass ptr at top of pass next saved Link of ptr set to prev at end of pass ptr at end of pass
1 node 41 node 52 null node 41 node 52
2 node 52 node 57 node 41 node 52 node 57
3 node 57 node 63 node 52 node 57 node 63
4 node 63 null node 57 node 63 null

Result: the loop ends with ptr null and prev naming node 63, so start is set to node 63 and the list reads 63, 57, 52, 41. The trace shows why next cannot be dropped: on pass 3, the address of node 63 exists in exactly one place before the link is turned — in ptr.link — and the very next statement overwrites it. Saving it into next first is the only thing standing between this method and a list that ends after two nodes.

The complete class

a single linked list of integers with all its operations Problem: Implement a single linked list of integers offering a test for emptiness, traversal, insertion at the beginning, at the end and after a stated position, deletion of a stated position, extraction of the element at a stated position, extraction of a sublist between two stated positions, and reversal in place. Positions are counted from 1 at the first node. An extraction from a position that does not exist reports the fact and returns −9999. Demonstrate every operation. All values are fixed inside the program.

/*
    Purpose: a single linked list of integers, supporting
    traversal, a test for emptiness, insertion at the beginning,
    at the end and after a given position, deletion of a given
    position, extraction of an element and of a sublist, and
    reversal of the list in place.
*/

class Node
{
    int data;
    Node link;

    Node(int d)
    {
        data = d;
        link = null;
    }
}

class SingleList
{
    private Node start;

    SingleList()
    {
        start = null;
    }

    boolean isEmpty()
    {
        return start == null;
    }

    void display()
    {
        if (isEmpty())
        {
            System.out.println("List is empty");
        }
        else
        {
            Node ptr = start;
            System.out.print("List:");
            while (ptr != null)
            {
                System.out.print(" " + ptr.data);
                ptr = ptr.link;
            }
            System.out.println();
        }
    }

    void insertBeginning(int v)
    {
        Node temp = new Node(v);
        temp.link = start;
        start = temp;
    }

    void insertEnd(int v)
    {
        Node temp = new Node(v);
        if (isEmpty())
        {
            start = temp;
        }
        else
        {
            Node ptr = start;
            while (ptr.link != null)
            {
                ptr = ptr.link;
            }
            ptr.link = temp;
        }
    }

    void insertAfter(int v, int n)
    {
        if (n < 1)
        {
            System.out.println("No such position");
            return;
        }
        Node ptr = start;
        int count = 1;
        while (ptr != null && count < n)
        {
            ptr = ptr.link;
            count = count + 1;
        }
        if (ptr == null)
        {
            System.out.println("No such position");
        }
        else
        {
            Node temp = new Node(v);
            temp.link = ptr.link;
            ptr.link = temp;
        }
    }

    void delete(int n)
    {
        if (isEmpty() || n < 1)
        {
            System.out.println("No such position");
            return;
        }
        if (n == 1)
        {
            start = start.link;
            return;
        }
        Node ptr = start;
        Node prev = null;
        int count = 1;
        while (ptr != null && count < n)
        {
            prev = ptr;
            ptr = ptr.link;
            count = count + 1;
        }
        if (ptr == null)
        {
            System.out.println("No such position");
        }
        else
        {
            prev.link = ptr.link;
            ptr.link = null;
        }
    }

    int elementAt(int n)
    {
        if (n < 1)
        {
            System.out.println("No such position");
            return -9999;
        }
        Node ptr = start;
        int count = 1;
        while (ptr != null && count < n)
        {
            ptr = ptr.link;
            count = count + 1;
        }
        if (ptr == null)
        {
            System.out.println("No such position");
            return -9999;
        }
        return ptr.data;
    }

    SingleList sublist(int from, int to)
    {
        SingleList out = new SingleList();
        if (from < 1 || to < from)
        {
            return out;
        }
        Node ptr = start;
        int count = 1;
        while (ptr != null && count < from)
        {
            ptr = ptr.link;
            count = count + 1;
        }
        while (ptr != null && count <= to)
        {
            out.insertEnd(ptr.data);
            ptr = ptr.link;
            count = count + 1;
        }
        return out;
    }

    void reverse()
    {
        Node prev = null;
        Node ptr = start;
        while (ptr != null)
        {
            Node next = ptr.link;
            ptr.link = prev;
            prev = ptr;
            ptr = next;
        }
        start = prev;
    }
}

class ListDemo
{
    public static void main(String args[])
    {
        SingleList marks = new SingleList();
        System.out.println("Empty at the start: " + marks.isEmpty());
        marks.insertEnd(41);
        marks.insertEnd(52);
        marks.insertEnd(63);
        marks.display();
        marks.insertBeginning(30);
        marks.display();
        marks.insertAfter(57, 3);
        marks.display();
        System.out.println("Element at position 2: " + marks.elementAt(2));
        marks.delete(1);
        marks.display();
        System.out.println("Sublist from position 2 to 3:");
        SingleList part = marks.sublist(2, 3);
        part.display();
        marks.reverse();
        marks.display();
        System.out.println("Empty at the end: " + marks.isEmpty());
    }
}

Sample output:

Empty at the start: true
List: 41 52 63
List: 30 41 52 63
List: 30 41 52 57 63
Element at position 2: 41
List: 41 52 57 63
Sublist from position 2 to 3:
List: 52 57
List: 63 57 52 41
Empty at the end: false

Variable description:

[Table:] Table 13.10 – variables of the single linked list program [/Table] | Variable | Type | Purpose | |—|—|—| | data | int | Field of Node, the element stored in that node | | link | Node | Field of Node, the address of the next node, or null in the last node | | d | int | Formal argument of the Node constructor, the value to store | | start | Node | Field of SingleList, the address of the first node, or null if the list is empty | | ptr | Node | Local reference used to walk the list without disturbing start | | prev | Node | Local reference kept one node behind ptr, in delete() and reverse() | | next | Node | Local reference in reverse(), holding the address about to be overwritten | | temp | Node | Local reference to the newly created node in the insertion methods | | v | int | Formal argument of the insertion methods, the value to be stored | | n | int | Formal argument giving a position, counted from 1 at the first node | | count | int | Counter of nodes visited during a walk | | from, to | int | Formal arguments of sublist(), the first and last positions to extract | | out | SingleList | The new list built and returned by sublist() | | marks | SingleList | The list created in main() | | part | SingleList | The sublist returned by sublist() in main() |

Notes: The fields of Node are left without an access modifier, so that SingleList can reach them. Making them private would compile but would leave SingleList unable to follow a single link, since a class cannot see the private members of a different class. start is private, and that is the boundary that matters: nothing outside SingleList ever touches a node.

Every method that takes a position walks from start with a counter, and every one of them tests ptr != null before count < n. The order of those two tests is not decoration. Java evaluates && from left to right and stops as soon as the answer is settled, so if ptr is null the second test is never attempted. Reversing the two would attempt to read a field of a reference holding no object.

There is no size() method and no node count anywhere in the class. Adding one would mean adjusting it correctly in five different methods, and the class has no need of it. When a structure can answer a question by walking, storing the answer as well is one more thing that can drift out of step with the truth.

13.7.8 The Linked List Compared With an Array

Both hold a sequence of elements. Everything else about them differs, and the differences all follow from one fact: an array’s elements are neighbours in memory and a linked list’s are not.

Table 13.11 – An array vs a single linked list

Feature Array Single linked list
Where the elements sit In consecutive memory locations Anywhere; each node records where the next one is
Size Fixed when the array is created Grows and shrinks one node at a time as the program runs
Memory per element The element only The element plus a link field
Reaching the k-th element One step, by computing its position from the index k − 1 steps, by following links from start
Inserting or deleting in the middle Every later element must be shifted, \(O(n)\) Two link fields change once the place is found, \(O(1)\)
Inserting at the beginning Every element must be shifted, \(O(n)\) \(O(1)\)
Direction of movement Either, freely Forward only
Wasted space Unused cells of an over-large array None from unused cells; one link field per element instead

Read the table as a trade and not as a scoreboard. The linked list wins on inserting and deleting; the array wins on reaching an element by position; and neither advantage is small.

Which matters depends entirely on the problem. A structure that is built once and then read many times at arbitrary positions — a table of bus fares looked up by route number — belongs in an array, where the linked list’s walking would be pure loss. A structure that is constantly added to and removed from in the middle, and always read from one end anyway, belongs in a linked list.

It is also worth noticing which structures of this chapter each one suits. A stack and a queue only ever touch their ends, so they never pay the linked list’s access cost, and building them from nodes removes the fixed capacity that forced the circular queue into existence. The array was never the natural home for those structures; it was the convenient one.

Q. The table says a linked list inserts in the middle in \(O(1)\), yet 13.7.4.3 gave the cost of insertAfter() as \(O(n)\). Which is right?

A. Both, because they measure different things. The insertion is two link changes, \(O(1)\). Finding the place is a walk, \(O(n)\). If a program already holds a reference to the node it wants to insert after — as it does when it is walking the list anyway — it pays only the \(O(1)\). If it must count from start every time, it pays the walk as well. An array reverses this exactly: finding the place is free and making room is the expensive part.

Add a method int size() to SingleList that returns the number of nodes by walking the list, and a method boolean contains(int v) that returns true if any node holds the value v. Test contains() on a value that is in the list, one that is not, and on an empty list — and check that the last of these returns false rather than stopping the program.

13.8 Binary Trees

13.8.1 From Linear Structures to a Hierarchy

Every structure in this chapter so far has been linear. Each element has one element before it and one after it, and the whole thing can be laid out along a line. That shape is exactly right for a waiting list or a pile of scripts, and exactly wrong for a great many other things.

Open the file manager on any computer and look at a folder. It contains files, and it contains other folders, and those contain files and folders in turn. There is no sense in which one item “follows” another. What the arrangement records is containment: a downward branching from a single top-level folder, with no way back up except the way you came.

A tree is a structure of that shape.

Its parts have their own names. Each item is a node, and the line joining a node to one directly below it is an edge. Every node except the topmost one hangs from exactly one node above it, and the topmost node hangs from nothing.

Because a node may have several nodes below it, a tree is a non-linear data structure: there is no single “next” element, and no single line along which the whole structure can be laid out.

This chapter deals with the case in which the branching is limited to two, which gives the binary tree.

NoteDefinition

A binary tree is a tree in which every node has at most two children, distinguished as the left child and the right child, so that a node may have no child, a left child only, a right child only, or both.

The words “left” and “right” are part of the structure, not part of the drawing. A node whose only child is a left child is a different tree from a node whose only child is a right child, even though both drawings show one node with one node below it. This distinction has no counterpart in any linear structure, and it is what the traversals of 13.8.4 depend on.

One tree serves as the example for the whole of this section. Fig. 13.8-1 shows it, and every term defined from here on is illustrated against it.

Diagram

Fig. 13.8-1 – nine nodes drawn as circles, arranged in four rows and joined by straight edges. Row 1, centred: P. Row 2: Q to the left and R to the right, each joined to P — Q is the left child of P and R is the right child of P. Row 3: S and T beneath Q, S being its left child and T its right child; U beneath R and drawn to the right of it, U being the right child of R, with R having no left child. Row 4: V beneath S and drawn to its left, V being the left child of S, with S having no right child; W beneath T and drawn to its right, W being the right child of T, with T having no left child; X beneath U and drawn to its left, X being the left child of U, with U having no right child. The node P carries a label “root” with a short arrow, and one edge, between P and Q, carries the label “edge”

13.8.2 The Vocabulary of a Binary Tree

Most questions about a tree are answered by naming its parts correctly, so the vocabulary is worth learning precisely rather than approximately.

13.8.2.1 Root, Parent, Child and Siblings

The root is the single node at the top of the tree, the one that hangs from nothing. In Fig. 13.8-1 the root is P. A tree has exactly one root, and every other node can be reached from it by following edges downward.

A node joined directly above another is its parent node; the node below is a child node. P is the parent of Q and of R; S and T are the children of Q. Every node except the root has exactly one parent — which is what makes a tree a tree, rather than a tangle.

Two nodes with the same parent are siblings. In the example, Q and R are siblings, and so are S and T. V, W, X and U each have no sibling, because each is the only child of its parent.

The terms are relative, and a node holds several of them at once. Q is a child of P, the parent of S and T, and a sibling of R, all at the same time. Asking “is Q a parent?” without saying parent of what is asking an incomplete question.

13.8.2.2 Internal Nodes, External Nodes and Sub Trees

A node with no children at all is an external node, also called a leaf. A node with at least one child is an internal node.

In Fig. 13.8-1 the external nodes are V, W and X. Every other node — P, Q, R, S, T and U — is internal. Every node of a tree is one or the other and never both, so the two counts always add up to the number of nodes.

Notice that leaves need not all lie at the bottom row. In this tree they happen to, but a node higher up with no children would be a leaf too.

The most useful idea in the whole vocabulary is the sub tree. Take any node, together with every node reachable from it by going downwards, and what you have is itself a tree, with that node as its root.

The sub tree rooted at Q consists of Q, S, T, V and W. The sub tree rooted at R consists of R, U and X. The sub tree rooted at V consists of V alone — a single leaf is a perfectly good tree of one node.

This is why trees are so often described in terms of themselves. Every node of a binary tree has a left sub tree and a right sub tree, either of which may be empty; the left sub tree of P is the tree rooted at Q, and the left sub tree of R is empty. Almost every statement about a binary tree can be made by saying what is true of the root and then saying that the same thing is true of both sub trees.

13.8.2.3 Height, Depth, Level, Size and Degree

These five are the terms most often mixed up, and four of them are counts of the same thing measured from different places.

The level of a node is the number of edges on the path from the root down to it, with the root itself at level 0. In Fig. 13.8-1, P is at level 0; Q and R at level 1; S, T and U at level 2; V, W and X at level 3.

The depth of a node is the same count: the number of edges from the root to that node. Depth and level are two words for one measurement, taken downwards from the root.

The height of a node is measured the other way — the number of edges on the longest path from that node down to a leaf. A leaf has height 0, since there is no path below it.

NoteDefinition

The height of a node is the number of edges on the longest downward path from that node to a leaf. The depth of a node is the number of edges on the path from the root to that node. The height of a tree is the height of its root, which is also the depth of its deepest node.

Height and depth agree for the tree as a whole and disagree for almost every individual node. Take Q: its depth is 1, because it is one edge below P; its height is 2, because the longest path below it is Q → S → V. Take V: its depth is 3 and its height is 0.

For the whole tree, the height is the height of the root, that is 3, since the longest root-to-leaf path is P → Q → S → V and it has three edges. The depth of the tree is the depth of its deepest node, also 3. The level of the tree is the level of its lowest row, again 3.

Q. If the height of a tree equals its depth, why keep two words at all?

A. Because they part company as soon as you speak of a single node rather than the whole tree. Depth counts downwards from the root and is therefore fixed by where the node sits; height counts upwards from the leaves and is fixed by what hangs below it. Adding a node at the bottom changes the height of every node above it and the depth of none of them.

The size of a tree is simply the number of nodes it contains. The tree in Fig. 13.8-1 has size 9.

The degree of a node is the number of children it has. The degree of a tree is the largest degree of any of its nodes. In a binary tree no node may have more than two children, so the degree of a binary tree is never more than 2.

Table 13.12 sets out every one of these measurements for every node of the example tree at once, which is the quickest way to see how they behave.

Table 13.12 – every node of the tree in Fig. 13.8-1, with its relations and measurements

Node Children Degree Sibling Depth (= level) Height Internal or external
P Q, R 2 none 0 3 internal
Q S, T 2 R 1 2 internal
R U 1 Q 1 2 internal
S V 1 T 2 1 internal
T W 1 S 2 1 internal
U X 1 none 2 1 internal
V none 0 none 3 0 external
W none 0 none 3 0 external
X none 0 none 3 0 external

Reading down the two count columns shows the pattern at a glance: depth rises as you go down the tree and height falls, and the two meet only where a longest path passes through.

Diagram

Fig. 13.8-2 – the same nine-node tree as Fig. 13.8-1, with four dashed horizontal lines drawn across it, one through each row of nodes, labelled at the left-hand margin “level 0”, “level 1”, “level 2” and “level 3”. Beside node Q, a bracket with two labels: an arrow upward to P labelled “depth 1” and an arrow downward along the path Q to S to V labelled “height 2”. At the right-hand margin, a vertical brace spanning all four rows labelled “height of the tree = 3”

One convention needs stating because questions depend on it: here the root is at level 0. Some books number the root as level 1, which raises every level and every depth by one while leaving every height unchanged. Whenever a level or a depth is quoted, the convention has to be quoted with it.

13.8.3 Completeness and Balancing

Two trees can hold the same nodes and be shaped quite differently, and shape here is not a matter of appearance. Two named properties describe how well a binary tree is filled out, and both come down to the same question: how deep does the tree have to reach?

Completeness describes a tree filled in without gaps, level by level from the top down and from the left across.

NoteDefinition

A binary tree is complete if every level except possibly the deepest contains the greatest number of nodes that level can hold, and the nodes at the deepest level occupy the leftmost positions available.

Diagram

Fig. 13.8-3 – two small binary trees side by side, drawn as circles joined by edges. Left tree, captioned “complete”: a root with two children; the left child has two children of its own; the right child has a left child only, its absent right position drawn as a short stub ending in a small hollow circle — so the deepest level holds three nodes occupying the leftmost positions. Right tree, captioned “not complete”: a root with two children; the left child has a right child only, its absent left position drawn as a stub ending in a hollow circle; the right child has two children. A note beneath the right tree reads “the gap lies to the left of nodes that exist”

The tree of Fig. 13.8-1 is not complete. Level 2 holds three nodes where it could hold four, and level 3 has nodes on it all the same — a level was left unfilled while a deeper one was begun, which is exactly what completeness forbids.

Completeness is worth naming for a practical reason. Because a complete binary tree has no gaps, it can be held in a plain array with no link fields at all, the children of the node at index \(i\) sitting at indices \(2i + 1\) and \(2i + 2\). A tree with gaps can use that arrangement only by leaving cells empty.

Balancing is about the two sides of each node rather than about gaps.

NoteDefinition

A binary tree is balanced if, at every node, the heights of its left and right sub trees differ by no more than one, an empty sub tree being counted as having height −1 so that a leaf has height 0.

Test the tree of Fig. 13.8-1 node by node. At P and at Q the two sub trees are of equal height, a difference of 0; at S, at T and at U the difference is 1, which is allowed. At R it is not: the left sub tree is empty, of height −1, while the right sub tree rooted at U has height 1, a difference of 2. One node is enough, so the tree is unbalanced.

A tree whose two sides differ widely is unbalanced, and the extreme case — every node having at most one child, so that the whole tree runs down in a single line — is called skewed.

Why this matters is best seen by asking what a tree is for. The point of a hierarchy is that the number of nodes can grow far faster than the height. A binary tree of height 3 with every level completely filled holds 15 nodes; of height 10 it holds 1023. Reaching any node from the root costs at most as many steps as the height, so a well-shaped tree of a thousand nodes is ten steps deep, not a thousand.

A skewed tree throws all of that away. Fig. 13.8-4 shows the two extremes with the same number of nodes.

Diagram

Fig. 13.8-4 – two trees of seven nodes side by side. Left tree, captioned “balanced — height 2”: a root with two children, each of which has two children, drawn as a perfect tree of three levels; a note beneath reads “at most 2 edges from the root to any node”. Right tree, captioned “skewed — height 6”: seven nodes in a single descending diagonal line, each node having only a right child; a note beneath reads “up to 6 edges from the root to the last node”

The skewed tree on the right is a linked list wearing a tree’s vocabulary. Every node has one child, every search from the root walks the whole chain, and none of the advantage of branching survives. This is why balancing is a property worth naming and worth maintaining, and why a structure that keeps its tree balanced as nodes are added is worth the extra work it costs.

the cost of reaching a node \(n\) counts the nodes in the tree. Reaching a given node means walking down from the root, one edge per step, so the worst case is the height of the tree. A balanced tree of \(n\) nodes has a height of about \(\log_2 n\), giving \(O(\log n)\); a skewed tree of the same \(n\) nodes has height \(n - 1\), giving \(O(n)\). The nodes are identical in the two cases and only the shape differs, which is why shape is worth caring about.

13.8.4 Tree Traversals

Visiting every node of a structure exactly once is called traversal, and for the linear structures of this chapter there was nothing to decide: you start at one end and follow the only route there is.

A tree offers no such route. Standing at the root, there are two ways down, and whichever is taken first, the other must be taken later. The order in which the nodes come out therefore depends on a choice, and three choices are standard.

All three are described in the same way, in terms of three jobs to be done at every node: visit the node itself, deal with its entire left sub tree, and deal with its entire right sub tree. What separates the three traversals is only the order in which those jobs are done, and the same order is then used at every node of the tree without exception.

Diagram

Fig. 13.8-5 – a single circle labelled “node” near the top, with two triangles below it, the left one labelled “left sub tree” and the right one labelled “right sub tree”, each joined to the circle by an edge. A dashed curve is drawn around the whole figure, starting above and to the left of the circle, running down the outside of the left triangle, up between the two triangles, down the outside of the right triangle and back up to the top. Three small marks on the curve: one to the left of the circle labelled “pre-order records the node here”, one directly beneath the circle between the two triangles labelled “in-order records the node here”, one to the right of the circle labelled “post-order records the node here”

That figure also gives a reliable way to work a traversal out by hand. Draw a curve that starts at the root and hugs the outside of the whole tree, going down the left of everything, round the bottom and back up the right. The curve passes every node three times — once on its left, once underneath, once on its right. Record each node on its first pass and you have the pre-order; on the second pass, the in-order; on the third, the post-order.

Q5. Three students read the same tree and write down three different orders of the same nodes. Which of them has made a mistake?

A. Possibly none of them. A tree has no single natural order, because at every node there are two ways down and one must be taken before the other. Three standard orders exist — pre-order, in-order and post-order — and they differ only in whether a node is recorded before, between, or after its two sub trees. All three visit every node exactly once, and all three are correct answers to different questions. The three students have made a mistake only if they were asked for the same traversal.

13.8.4.1 Pre-order Traversal

In pre-order traversal the node is recorded first, then its left sub tree is traversed, then its right sub tree. The order is root, left, right.

Applied to the tree of Fig. 13.8-1, start at P and record it. Then traverse P’s left sub tree, the one rooted at Q: record Q, traverse Q’s left sub tree rooted at S — record S, traverse S’s left sub tree, giving V, and S’s right sub tree, which is empty. That finishes S’s sub tree as S, V. Then Q’s right sub tree rooted at T gives T followed by W. So P’s left sub tree yields Q, S, V, T, W. Finally P’s right sub tree, rooted at R: record R, its left sub tree is empty, its right sub tree rooted at U gives U then X.

The pre-order traversal is P, Q, S, V, T, W, R, U, X.

Notice the first node. In a pre-order traversal the root always comes out first, which makes pre-order the natural way to copy a tree — the root is available before anything needs to be hung beneath it.

13.8.4.2 In-order Traversal

In in-order traversal the left sub tree is traversed first, then the node is recorded, then the right sub tree is traversed. The order is left, root, right.

Working through the same tree: P cannot be recorded until the whole of its left sub tree has been dealt with. That sub tree is rooted at Q, and Q cannot be recorded until its left sub tree, rooted at S, is done; S cannot be recorded until V is, and V has no left sub tree, so V is the first node out. Then S, then S’s empty right sub tree. Q follows, then Q’s right sub tree rooted at T: T’s left sub tree is empty, so T is recorded, then W. That completes P’s left sub tree as V, S, Q, T, W. Now P is recorded. Then P’s right sub tree: R’s left sub tree is empty, so R is recorded, then its right sub tree rooted at U, which gives X before U.

The in-order traversal is V, S, Q, T, W, P, R, X, U.

The root of a tree always appears in the middle of its in-order traversal, with everything from its left sub tree before it and everything from its right sub tree after it. That property is what makes in-order the traversal used whenever a tree is arranged so that “smaller” values are placed to the left.

13.8.4.3 Post-order Traversal

In post-order traversal both sub trees are traversed first and the node is recorded last. The order is left, right, root.

For the same tree: nothing can be recorded until the deepest left-hand node is reached, so V comes first, then S, since S’s right sub tree is empty. Then Q’s right sub tree: T’s left sub tree is empty and its right sub tree gives W, so W then T. Only now is Q recorded. P’s left sub tree therefore yields V, S, W, T, Q. On the right, R’s left sub tree is empty and its right sub tree rooted at U gives X then U, after which R is recorded. P comes last of all.

The post-order traversal is V, S, W, T, Q, X, U, R, P.

The root always comes out last, and every node appears after both of its children. That is what makes post-order the traversal used when something must be done to the parts before it can be done to the whole — clearing a tree away, or working out a total that depends on the totals of both sub trees.

Table 13.13 – the three traversals of the tree in Fig. 13.8-1

Feature Pre-order In-order Post-order
Order of the three jobs root, left, right left, root, right left, right, root
Where the root appears First Between the two sub trees Last
Sequence for the example tree P, Q, S, V, T, W, R, U, X V, S, Q, T, W, P, R, X, U V, S, W, T, Q, X, U, R, P
Which node comes out first The root The leftmost node reachable by going left The leftmost node reachable by going left
Suited to Copying or listing a tree from the top down Reading the nodes in left-to-right order Working on the parts before the whole

Two rows of that table repay attention.

The first nodes of the in-order and post-order traversals are the same, V, and they are the same for every tree. Both traversals go as far left as they can before recording anything, so both begin at the same node — but they diverge immediately afterwards.

And all three sequences contain the same nine letters. A traversal never adds or omits a node; it only chooses an order. If a traversal you have written down has eight entries or ten, it is wrong before anything else is checked.

writing a traversal by reading the drawing row by row What students write: for the tree of Fig. 13.8-1, the sequence P, Q, R, S, T, U, V, W, X, offered as a pre-order traversal. What happens: this is the tree read left to right along each level in turn, which is a real traversal in its own right but is none of the three defined here. It happens to begin with the root, so it looks plausible, and it goes wrong at the third node — pre-order goes all the way down P’s left sub tree before it touches R at all. The fix: never read across the drawing. Work down one node at a time, applying the same rule at every node, and finish a sub tree completely before starting its sibling. The tracing curve of Fig. 13.8-5 enforces this automatically, because it cannot reach the right sub tree without having gone round the whole of the left one first.

Draw a binary tree of six nodes whose pre-order traversal is A, B, D, C, E, F and whose in-order traversal is D, B, A, E, C, F. Then write down its post-order traversal, and state its height, its size, and which of its nodes are external.

13.9 Summary

Data structures and abstract data types
  1. A data structure is an arrangement of data in memory together with the operations permitted on it. Two structures can hold identical values in identical memory and behave completely differently, because the permitted operations, and no others, decide how data enters and leaves.

  2. A linear data structure holds its elements in a single sequence. The stack, the queue, the circular queue, the dequeue and the single linked list are linear; the binary tree is not.

  3. An abstract data type describes a structure entirely in terms of its operations and what each one does, saying nothing about storage. A data structure is one implementation of it. The same abstract data type can have many different implementations.

  4. In Java the specification is written as an interface, which lists method headings with no bodies, and the implementation as a class that implements it. Methods declared in an interface are automatically public, so every method implementing one must be declared public.

  5. Two classes implementing the same interface may store entirely different things and still answer every question the interface asks. Choosing between them is a trade: a structure that stores less is cheaper and can answer fewer questions later.

The stack
  1. A stack permits insertion and deletion at one end only, called the top. The element removed is always the one most recently inserted and not yet removed, so a stack follows the last in first out (LIFO) principle.

  2. Inserting is called a push and removing is called a pop. Held in an array, a stack needs one marker, the index of the topmost element, set to −1 when the stack is empty.

  3. That marker must mean the same thing in every method. A push raises it and then stores; a pop reads and then lowers it. Reversing either order breaks the class.

  4. Attempting to push onto a stack whose array is full is a stack overflow; attempting to pop from an empty one is a stack underflow. Both are normal conditions to be tested for and reported, not errors to be allowed to happen.

  5. Push and pop are \(O(1)\) in the best, average and worst case, where \(n\) counts the elements currently held; neither operation looks at any element other than the topmost.

  6. Stacks are used wherever work must be undone or completed in the reverse of the order it was begun — editor undo, browser history, the return points of nested method calls, and the evaluation of arithmetic expressions.

Infix, prefix and postfix notations
  1. A binary operator can be written between its operands (infix), before both of them (prefix, also called Polish notation), or after both of them (postfix, also called reverse Polish notation). The operands keep their relative order in all three.

  2. Infix needs precedence rules and brackets because an operand can lie between two operators and belong to either. In prefix and postfix each operator stands alongside the two things it acts on, so the positions of the symbols alone fix the reading and brackets have nothing to do.

  3. Precedence, highest first, is exponentiation, then multiplication and division, then addition and subtraction. Associativity is left to right for multiplication, division, addition and subtraction, and right to left for exponentiation. Brackets override both.

  4. The bracketing method converts by hand: put in every bracket the rules imply, then, working from the innermost bracket outwards, move each operator after its two operands for postfix or before them for prefix, then remove the brackets.

  5. The stack method converts an infix expression to postfix in one left-to-right scan. Operands go straight to the output; an operator is held on the stack until an operator of equal or lower precedence, or a closing bracket, proves it can wait no longer. The cost is \(O(n)\), where \(n\) counts the symbols in the expression.

  6. Prefix is not the reverse of the postfix string. Reversing a postfix expression produces the operands in the wrong order and is not a valid expression at all.

  7. A postfix expression is evaluated in one left-to-right scan with a stack of values: push each operand, and on each operator pop two values, apply it and push the result. The first value popped is the right-hand operand, because it was pushed later. Getting that order wrong is invisible for addition and multiplication and silently wrong for subtraction and division. The cost is \(O(n)\), where \(n\) counts the symbols.

The queue
  1. A queue permits insertion at one end, the rear, and deletion at the other, the front. The element removed is always the one present longest, so a queue follows the first in first out (FIFO) principle.

  2. Two markers are needed rather than one, and they move independently — insertion moves only the rear, deletion only the front. Both are set to −1 when the queue is empty.

  3. When the element deleted is the last one present, both markers must be reset to −1. Omitting this leaves a queue that holds nothing but does not report itself empty.

  4. Insertion and deletion are both \(O(1)\), where \(n\) counts the elements currently held.

  5. A simple queue held in an array has a permanent flaw. Insertion can only use the cell after the rear, and every deletion abandons a cell at the front, so once the rear reaches the last cell no further insertion is possible however many cells stand empty. Such a queue can serve only as many elements in total as its array has cells.

The circular queue
  1. A circular queue removes that waste by treating the array positions as a ring: the position after the last cell is the first cell. The rear advances by taking the remainder of one more than its value on division by the capacity.

  2. Fullness becomes a relationship between the two markers rather than a property of one: the queue is full when the cell the next insertion would use is the cell holding the front element. Emptiness is still signalled separately, by the front marker being −1, and keeping that separate signal is what allows every cell of the array to be used.

  3. Insertion and deletion remain \(O(1)\), where \(n\) counts the elements held. What is bought is unlimited reuse of the cells; what is paid is arithmetic and tests that are harder to read and easier to get wrong.

The dequeue
  1. A dequeue, or double ended queue, permits both insertion and deletion at either end, so it has four operations where a queue has two.

  2. Inserting at the rear and deleting from the front are the queue operations unchanged. Inserting at the front and deleting from the rear are their mirror images, moving a marker backwards round the ring by adding the capacity before taking the remainder — a step needed because in Java the remainder of a negative number is negative.

  3. All four operations are \(O(1)\), where \(n\) counts the elements held.

  4. Restricted to one end, a dequeue behaves exactly as a stack; restricted to insertion at the rear and deletion at the front, exactly as a queue. That flexibility costs a guarantee: a queue cannot serve out of order, and a dequeue makes no such promise.

The single linked list
  1. An array holds its elements next to one another, which fixes its size and makes insertion in the middle cost a shift of every later element. A linked list gives up that adjacency and both difficulties go with it.

  2. A node holds two parts: a data field storing one element, and a link field storing the address of the next node, or null in the last node. A reference variable outside the list holds the address of the first node; lose it and the whole list is lost.

  3. A node’s link field is a reference, not a node inside a node. The nodes may lie anywhere in memory and need not be near one another.

  4. A single linked list can be walked forwards only. A node records the address of the node after it and nothing about the node before it, so the address needed to move back was never written down.

  5. Traversal walks from the first node to the last with a separate reference, following link fields until null. It costs \(O(n)\), where \(n\) counts the nodes. A list is empty exactly when the start reference holds null, which needs no counter and no convention to maintain.

  6. Insertion at the beginning changes two links and never examines a node, so it is \(O(1)\). Insertion at the end must first find the last node by following \(n-1\) links, so it is \(O(n)\). The two ends of the structure are not equally cheap.

  7. Insertion after a stated node is two link changes once the node is found, and finding it costs a walk. The two changes must be made in the right order: attach the new node to the rest of the list before letting the preceding node point at it. The general rule is that the last thing you may overwrite is the only remaining copy of an address.

  8. Deletion of a node requires the node before it, which cannot be reached from the node itself, so the walk carries a second reference one step behind. Deleting the first node is a separate case, because the reference that must change is the one holding the start of the list. No node is explicitly freed; a node no reference names is reclaimed automatically.

  9. Extracting an element is a traversal that stops at the wanted position and leaves the list unchanged. Extracting a sublist may either cut the portion out or copy it; copying costs one new node per element and leaves the original intact.

  10. Reversal moves no data at all. Every node stays where it is and only the direction of the links changes, one node at a time, with three references in play — the reversed portion behind, the node being turned, and the address about to be overwritten, which must be saved before the link is changed. Reversal is \(O(n)\), where \(n\) counts the nodes.

  11. Against an array, a linked list wins on inserting and deleting and loses on reaching an element by position: an array reaches its k-th element in one step, a linked list in \(k-1\). Neither advantage is small, and which matters depends on whether the structure is mostly rebuilt or mostly read.

Binary trees
  1. A tree is a non-linear structure: each node may have several nodes below it, so there is no single “next” element. Nodes are joined by edges, and every node except the topmost hangs from exactly one node above it.

  2. A binary tree is a tree in which every node has at most two children, distinguished as the left child and the right child. That distinction is part of the structure, not of the drawing.

  3. The root is the single node at the top. A node directly above another is its parent and the one below is its child; two nodes with the same parent are siblings. A node with no children is an external node or leaf, and a node with at least one child is an internal node.

  4. Any node together with everything reachable below it forms a sub tree, itself a tree with that node as its root. Every node of a binary tree has a left sub tree and a right sub tree, either of which may be empty.

  5. The level of a node, and equally its depth, is the number of edges from the root down to it, with the root at level 0. The height of a node is the number of edges on the longest path from that node down to a leaf, so a leaf has height 0. For the tree as a whole, height and depth are the same number; for an individual node they generally differ.

  6. The size of a tree is its number of nodes. The degree of a node is its number of children, and the degree of a tree is the largest degree of any node — never more than 2 in a binary tree.

  7. A binary tree is complete when every level except possibly the deepest holds as many nodes as that level can hold, and the nodes of the deepest level occupy the leftmost positions with no gaps between them. Because a complete tree has no gaps, it can be held in a plain array with no link fields at all.

  8. A tree is balanced when, at every node, the heights of its left and right sub trees differ by at most one, an empty sub tree counting as height −1. A tree in which every node has at most one child is skewed. Shape decides cost: reaching a node means walking down from the root, so the worst case is the height of the tree — about \(\log_2 n\) for a balanced tree, giving \(O(\log n)\), but \(n - 1\) for a skewed tree, giving \(O(n)\), where \(n\) counts the nodes. A skewed tree is a linked list in a tree’s vocabulary, and throws the advantage of branching away.

  9. Traversal visits every node exactly once. A tree offers no single route, so an order must be chosen, and the choice is made once and applied at every node: pre-order records the node, then the left sub tree, then the right; in-order takes the left sub tree, then the node, then the right; post-order takes both sub trees and records the node last.

  10. The root comes out first in pre-order, between the two sub trees in in-order, and last in post-order. In-order and post-order both begin at the same node, the leftmost one reachable by going left. All three sequences contain exactly the same nodes; a traversal chooses an order and never adds or omits anything.

13.10 Key Terms

  1. abstract data type (ADT) — a description of a data structure given entirely in terms of the operations it permits and what each one does, saying nothing about how the data is stored.

  2. ADT — abstract data type; see the entry above.

  3. associativity — the rule deciding which of two operators of equal precedence acts first; left to right for *, /, + and -, right to left for exponentiation.

  4. balancing — the property of a binary tree in which, at every node, the heights of the left and right sub trees differ by at most one.

  5. binary tree — a tree in which every node has at most two children, the two being distinguished as the left child and the right child.

  6. child node — a node joined directly below another node.

  7. circular queue — a queue held in an array whose positions are treated as a ring, so that the position after the last cell is the first cell and freed cells can be reused.

  8. completeness — the property of a binary tree in which every level except possibly the deepest is filled to capacity, and the deepest level is filled from the left with no gaps.

  9. data field — the part of a node that holds the element being stored.

  10. data structure — an arrangement of data in memory together with a fixed rule about which operations are permitted on it.

  11. degree — of a node, its number of children; of a tree, the largest degree found among its nodes.

  12. deletion — the removal of an element from a queue, which takes place at the front.

  13. depth — of a node, the number of edges from the root down to it; of a tree, the number of edges on the longest root-to-leaf path.

  14. dequeue — a double ended queue; a linear structure permitting both insertion and deletion at either end.

  15. double ended queue — the full name of the dequeue.

  16. edge — the line joining a node to one directly below it in a tree.

  17. external node — a node of a tree with no children; also called a leaf.

  18. FIFO — first in first out; see the entry below.

  19. first in first out (FIFO) — the rule followed by a queue, under which the element removed is always the one that has been present longest.

  20. front — the end of a queue from which elements are deleted.

  21. height — of a node, the number of edges on the longest path from it down to a leaf; of a tree, the height of its root.

  22. infix notation — the way of writing an expression in which a binary operator stands between its two operands.

  23. in-order — the traversal that deals with the left sub tree, then the node, then the right sub tree.

  24. insertion — the addition of an element to a queue, which takes place at the rear.

  25. internal node — a node of a tree with at least one child, the root included.

  26. last in first out (LIFO) — the rule followed by a stack, under which the element removed is always the most recently inserted one still present.

  27. leaf — another name for an external node of a tree.

  28. level — the number of edges from the root down to a node, the root itself being at level 0.

  29. LIFO — last in first out; see the entry above.

  30. linear data structure — a structure whose elements form a single sequence, each with at most one element before it and one after it.

  31. link field — the part of a node that holds the address of the next node, or null in the last node.

  32. node — an object holding one element of a list together with the address of the next node; also, an item of a tree.

  33. non-linear data structure — a structure in which an element may have several elements below it, so that there is no single “next” element; the tree is the example in this chapter.

  34. parent node — the node joined directly above another node.

  35. Polish notation — another name for prefix notation.

  36. pop — the operation that removes the topmost element of a stack.

  37. postfix notation — the way of writing an expression in which a binary operator stands after both of its operands.

  38. post-order — the traversal that deals with the left sub tree, then the right sub tree, and takes the node last.

  39. precedence — the rule deciding which of two different operators acts first when both compete for the same operand.

  40. prefix notation — the way of writing an expression in which a binary operator stands before both of its operands.

  41. pre-order — the traversal that takes the node first, then deals with the left sub tree, then the right.

  42. push — the operation that places an element on the top of a stack.

  43. queue — a linear structure permitting insertion at the rear only and deletion from the front only.

  44. rear — the end of a queue at which elements are inserted.

  45. reverse Polish notation — another name for postfix notation.

  46. root — the single node at the top of a tree, the one node having no parent.

  47. siblings — two nodes of a tree having the same parent.

  48. single linked list — a structure whose elements are held in separate nodes, each carrying one link to the next, with an external reference to the first node and null in the link of the last.

  49. size — the number of nodes in a tree.

  50. skewed — describing a tree in which every node has at most one child, so that the whole tree runs down in a single line.

  51. stack — a linear structure permitting insertion and deletion at one end only, called the top.

  52. stack overflow — the condition reached when a push is attempted on a stack whose array has no free cell.

  53. stack underflow — the condition reached when a pop is attempted on an empty stack.

  54. sub tree — any node of a tree together with every node below it, which is itself a tree.

  55. top — the single working end of a stack, at which both insertion and deletion take place.

  56. traversal — visiting every element of a structure exactly once in a fixed order; along the links in a linked list, and in one of three standard orders in a binary tree.

  57. tree — a non-linear structure of nodes joined by edges, in which one node is the root and every other node has exactly one parent.

13.11 Points to Ponder

  1. A dequeue can do everything a stack can do and everything a queue can do. Yet 13.6 argued that a class offering only push and pop is sometimes the better choice precisely because it can do less. When is the ability to do less a property worth paying for — and who is being protected by it, the program or the programmer?

  2. Two classes in 13.1 implemented the same interface, returned the same answers to every question asked of them, and differed in that one had thrown the data away and the other had not. If a program using them through the interface cannot tell them apart, in what sense are they the same structure, and in what sense are they clearly not? What kind of question would force the difference into the open?

  3. A pop from an empty stack returns −9999 by agreement, and the agreement holds only as long as −9999 is not a value anyone would legitimately store. What else could a method return to report that it has nothing to give — and whatever you propose, what work does it move onto the caller?

  4. Postfix notation needs no brackets, needs no precedence rules, and can be evaluated in a single left-to-right scan by a machine. Infix needs all three. Why, then, does almost everybody still write, read and think in infix — and is that a fact about notation or a fact about people?

  5. A single linked list of \(n\) integers stores \(n\) links as well as \(n\) values, and so can occupy more memory than an array holding the same values. Under what circumstances does a structure that uses more memory per element nevertheless leave a program using less memory overall?

  6. Given only the pre-order traversal of a binary tree, several different trees could have produced it, so the traversal alone is not enough to rebuild the tree. What would have to be added to make the reconstruction certain, and why does one extra traversal help where a longer one would not?

13.12 Objective Questions

  1. The principle followed by a queue is: [1 mark]
  1. LIFO
  2. FIFO
  3. LILO
  4. FILO
  1. In a circular queue of capacity SIZE with markers front and rear, insertion must be refused when: [1 mark]
  1. rear == SIZE - 1
  2. front == rear
  3. (rear + 1) % SIZE == front
  4. front == -1
  1. The notation in which an operator is written after both of its operands is called: [1 mark]
  1. infix notation
  2. prefix notation
  3. Polish notation
  4. reverse Polish notation
  1. A stack is held in an array with top naming the index of the topmost element. The value of top for an empty stack is: [1 mark]
  1. 0
  2. −1
  3. the capacity of the array
  4. one less than the capacity of the array
  1. In a single linked list, deleting the first node requires a change to: [1 mark]
  1. the link field of the last node
  2. the link field of the second node
  3. the reference start
  4. the data field of the first node
  1. The degree of an external node of a binary tree is: [1 mark]
  1. 0
  2. 1
  3. 2
  4. equal to its level
  1. A binary tree has 11 nodes and its root is at level 0. Its size is: [1 mark]
  1. 10
  2. 11
  3. 12
  4. cannot be determined without the height
  1. Assertion (A): In a stack, the element inserted last is the first to be removed. Reason (R): A stack permits insertion and deletion at one end only. Choose the correct option: [1 mark]
  1. Both A and R are true and R is the correct explanation of A.
  2. Both A and R are true but R is not the correct explanation of A.
  3. A is true but R is false.
  4. A is false but R is true.
  1. Assertion (A): A single linked list can be traversed from its last node back to its first. Reason (R): Each node of a single linked list stores the address of the node that follows it. Choose the correct option: [1 mark]
  1. Both A and R are true and R is the correct explanation of A.
  2. Both A and R are true but R is not the correct explanation of A.
  3. A is true but R is false.
  4. A is false but R is true.
  1. Match each structure in Column A with the property in Column B that describes it. [1 mark]
Column A Column B
a. Stack i. insertion at the rear, deletion from the front
b. Queue ii. at most two children per node
c. Dequeue iii. insertion and deletion at one end only
d. Binary tree iv. insertion and deletion at both ends
  1. Fill in the blank: the traversal of a binary tree in which the root of every sub tree is taken last is called ____________ traversal. [1 mark]

  2. Fill in the blank: in a dequeue held in an array of capacity SIZE, the front marker is stepped backwards by the expression ____________ . [1 mark]

  3. State whether each of the following is true or false. [1 mark]

  1. The link field of the last node of a single linked list holds the address of the first node.
  2. Every leaf of a binary tree is an external node.
  1. Give the full form of each of the following: LIFO, FIFO, ADT. [1 mark]

13.13 Check Your Concepts

  1. Define an abstract data type, and state one thing an abstract data type does not tell you about a structure. [2 marks]

  2. What is meant by stack underflow? State the condition, in terms of top, under which it occurs. [2 marks]

  3. State what the link field of the last node of a single linked list contains, and explain why it must contain that value. [2 marks]

  4. Define the height of a binary tree and the depth of a node of a binary tree, making clear how the two are measured differently. [2 marks]

  5. Differentiate between a stack and a queue, giving three points of difference. [3 marks]

  6. Convert the infix expression A + B * (C - D) / E into its postfix form, showing the intermediate steps of your working. [3 marks]

  7. Convert the infix expression (P - Q) * R + S / T into its prefix form, showing the intermediate steps of your working. [3 marks]

  8. Give reasons why a simple queue held in an array may refuse an insertion while several of its cells stand empty, and state how a circular queue removes the fault. [3 marks]

  9. Study the binary tree in Fig. 13.13-1 and answer the questions that follow. [4 marks]

Diagram

Fig. 13.13-1 – a binary tree drawn with circular nodes joined by straight edges. Root A at the top with a left child B and a right child C. B has a left child D and a right child E. C has no left child and a right child F; draw the absent left position as a short stub ending in a small hollow circle. E has a left child G and no right child, again with a stub for the absent position. F has no left child and a right child H, with a stub for the absent position

  1. Write the pre-order traversal of the tree.
  2. Name the external nodes of the tree.
  3. State the height of the tree when the root is at level 0.
  4. Name the siblings of node D.
  1. Write the algorithm to insert an element at the rear of a circular queue held in an array of capacity SIZE, with front and rear both holding −1 when the queue is empty. State the cost of your algorithm in big O notation, saying what \(n\) counts. [4 marks]

  2. When a node is inserted at the beginning of a single linked list, two link assignments are made. Explain, with an example, why performing them in the wrong order destroys the list. [4 marks]

  3. A programmer must choose between an array and a single linked list to hold a collection of examination marks. Compare the two structures under the headings of storage, cost of reaching the \(k\)th element, cost of inserting at the beginning, and memory used per element. Conclude by stating one situation in which each of the two is the better choice, with your reason. [6 marks]

  4. Explain in full the method by which a stack converts an infix expression into postfix form, stating what is done for an operand, for an operator, for a left bracket and for a right bracket. Apply your method to the expression (A + B) / C * (D - E), showing the contents of the stack and of the output expression after each symbol is scanned. [6 marks]

13.14 Output & Debugging

  1. Study the following program and state exactly what it prints. [2 marks]
class Pile
{
    public static void main(String args[])
    {
        int st[] = new int[5];
        int top = -1;
        for (int i = 1; i <= 4; i++)
        {
            top = top + 1;
            st[top] = i * i;
        }
        System.out.print(st[top] + " ");
        top = top - 1;
        System.out.print(st[top] + " ");
        top = top - 1;
        System.out.println(st[top]);
    }
}
  1. State what the following program prints, and say in one sentence what the variable r represents. [2 marks]
class Ring
{
    public static void main(String args[])
    {
        int size = 5;
        int r = 3;
        for (int k = 1; k <= 4; k++)
        {
            r = (r + 1) % size;
            System.out.print(r + " ");
        }
        System.out.println();
    }
}
  1. The following program is intended to store 10, 20, 30 and 40 in an array whose positions wrap round, moving the marker backwards each time. It does not run. Identify the single error, state what happens when the program is run, and correct the line at fault. [3 marks]
class Backward
{
    public static void main(String args[])
    {
        int dq[] = new int[4];
        int f = 0;
        dq[f] = 10;
        for (int k = 1; k <= 3; k++)
        {
            f = (f - 1) % dq.length;
            dq[f] = 10 * (k + 1);
        }
        System.out.println(dq[0] + " " + dq[1]);
    }
}
  1. State the output of the following program. The total of the three values stored is 31, so explain why the program does not print that, and correct the fault. [3 marks]
class Node
{
    int data;
    Node next;

    Node(int d)
    {
        data = d;
        next = null;
    }
}

class AddUp
{
    public static void main(String args[])
    {
        Node start = new Node(7);
        start.next = new Node(11);
        start.next.next = new Node(13);
        int total = 0;
        Node ptr = start;
        while (ptr.next != null)
        {
            total = total + ptr.data;
            ptr = ptr.next;
        }
        System.out.println("Total = " + total);
    }
}
  1. State what the following program prints, and write the infix expression that the string exp represents. [3 marks]
class Evaluate
{
    public static void main(String args[])
    {
        int st[] = new int[10];
        int top = -1;
        String exp = "852-/";
        for (int i = 0; i < exp.length(); i++)
        {
            char c = exp.charAt(i);
            if (c >= '0' && c <= '9')
            {
                top = top + 1;
                st[top] = c - '0';
            }
            else
            {
                int b = st[top];
                top = top - 1;
                int a = st[top];
                top = top - 1;
                if (c == '-')
                {
                    top = top + 1;
                    st[top] = a - b;
                }
                else
                {
                    top = top + 1;
                    st[top] = a / b;
                }
            }
        }
        System.out.println(st[top]);
    }
}
  1. The following program inserts one element into a queue and then deletes it, leaving the queue holding nothing. State what it prints, explain why the last line printed is misleading, and give the correction needed. [3 marks]
class Counter
{
    public static void main(String args[])
    {
        int q[] = new int[5];
        int front = -1;
        int rear = -1;
        front = 0;
        rear = 0;
        q[0] = 21;
        int v = q[front];
        front = front + 1;
        System.out.println("Deleted " + v);
        System.out.println("front = " + front + ", rear = " + rear);
        System.out.println("Empty? " + (front == -1));
    }
}

13.15 Programming Practice

  1. A library issue counter records the accession numbers of books as they are placed on a returns trolley. A book can be added to the trolley or taken from it at the top only.

Define a class Trolley with the following details:

Item Detail
Class name Trolley
bk[ ] array to hold the accession numbers
cap maximum capacity of the array
top index of the topmost element
Trolley(int m) constructor to initialise cap = m and top = -1, and to create the array
void addBook(int n) adds an accession number at the top if possible, otherwise prints “Trolley is full”
int takeBook( ) removes and returns the topmost accession number if any, otherwise prints “Trolley is empty” and returns −9999
void display( ) prints the accession numbers held, topmost first

Specify the class Trolley, giving the details of the constructor and of the three member functions. The main( ) function and the algorithm need not be written. Name the data structure the class implements and state the principle it follows. [6 marks]

  1. Define a class TokenRing that holds token numbers in a circular queue.
Item Detail
Class name TokenRing
tk[ ] array to hold the token numbers
lim maximum capacity of the array
front index of the front end
rear index of the rear end
TokenRing(int m) constructor to initialise lim = m and front = rear = -1, and to create the array
void addToken(int n) adds a token number at the rear end if possible, otherwise prints “Ring is full”
int callToken( ) removes and returns the token number at the front end if any, otherwise prints “Ring is empty” and returns −9999

Specify the class TokenRing, giving the details of the constructor and of void addToken(int) and int callToken( ). Assume the display method has already been defined. The main( ) function and the algorithm need not be written. State also how the class would differ if the queue were not circular. [6 marks]

  1. Write a complete program that reads an infix expression from the keyboard as a single string, containing single-letter operands, the operators +, -, * and /, and round brackets, with no spaces. Using a stack, convert the expression into its postfix form and print the result.

Submit the algorithm, the class, and a variable description table. Show the output for the sample input (A+B)*C-D and test your program also on the unseen input A*(B+C/D). [6 marks]

  1. A single linked list is formed from the objects of the class below.
class Node
{
    int mark;
    Node next;
}

Write an algorithm OR a method to count the nodes of an existing linked list whose mark field holds a value greater than a stated limit, and to return that count. The method declaration is:

int countAbove(Node start, int limit)

State also, in big O notation, the cost of your method, saying what \(n\) counts. [5 marks]

  1. A word is a palindrome if it reads the same forwards and backwards. Write a complete program that reads one word from the keyboard, inserts each of its letters at the rear of a dequeue, and then decides whether the word is a palindrome by repeatedly removing one letter from the front and one from the rear and comparing them, stopping when fewer than two letters remain.

Submit the class, including the dequeue operations it uses, and a variable description table. Print Palindrome or Not a palindrome. Show the output for the sample input MALAYALAM and test your program also on the unseen input LEVELS. [5 marks]