In this recitation, you will experiment with the use of a Stack and a Queue for searching a maze. During this recitation, you will
Deque,Stack and a Queue class using Deque,Maze and Position classes,SolveMazeStackclass,SolveMazeQueue that uses a queue instead of a stack,First, we will need our usual Node class.
class Node<T> { T item; Node<T> next; Node<T> previous; Node() { item = null; next = null; previous = null; } Node(T newItem, Node<T> previousArg, Node<T> nextArg) { item = newItem; previous = previousArg; next = nextArg; } public String toString() { return item.toString(); } }
Notice, no member variables or methods are declared publici or private or protected. This means they can be used by any classes in this package.
Now for the Deque.
public class Deque<T> { Node<T> head; int size; public Deque() { head = new Node<T>(); head.next = head; head.previous = head; size = 0; } public boolean isEmpty() { return size == 0; } public int size() { return size; } public void addFirst(T newItem) { Node<T> newNode = new Node<T>(newItem, head, head.next); head.next.previous = newNode; head.next = newNode; size++; } public void addLast(T newItem) { Node<T> newNode = new Node<T>(newItem, head.previous, head); head.previous.next = newNode; head.previous = newNode; size++; } public T peekFirst() throws DequeException { if (isEmpty()) throw new DequeException("DequeException: peekFirst() attempt on empty deque"); else return head.next.item; } public T removeFirst() throws DequeException { if (isEmpty()) throw new DequeException("DequeException: removeFirst() attempt on empty deque"); else { T item = peekFirst(); head.next = head.next.next; head.next.previous = head; return item; } } public T peekLast() throws DequeException { if (isEmpty()) throw new DequeException("DequeException: peekLast() attempt on empty deque"); else return head.previous.item; } public T removeLast() throws DequeException { if (isEmpty()) throw new DequeException("DequeException: removeLast() attempt on empty deque"); else { T item = peekLast(); head.previous = head.previous.previous; head.previous.next = head; return item; } } public String toString() { String result = ""; for (Node<T> current = head.next; current != head; current = current.next) result += current.item + " "; return result; } // Test public static void main(String[] args){ Deque<String> ds = new Deque<String>(); ds.addFirst("front1"); ds.addFirst("front2"); ds.addLast("last1"); System.out.println(ds); String a = ds.removeLast(); System.out.println("removeLast = " + a); System.out.println(ds); a = ds.removeFirst(); System.out.println("removeFirst = " + a); System.out.println(ds); Deque<Integer> di = new Deque<Integer>(); di.addLast(10); di.addFirst(20); System.out.println(di); } }
Looks like we need a DequeException class.
public class DequeException extends java.lang.RuntimeException { public DequeException(String s) { super(s); } }
Download these, compile them, and run the Deque class to test it.
public class Stack<T> { Deque<T> deque; public Stack() { deque = new Deque<T>(); } public boolean isEmpty() { return deque.isEmpty(); } public void push(T newItem) { deque.addFirst(newItem); } public T pop() throws StackException { try { T item = deque.removeFirst(); return item; } catch (DequeException e) { throw new StackException("StackException: pop() attempt on empty stack"); } } public T peek() throws StackException { try { T item = deque.peekFirst(); return item; } catch (DequeException e) { throw new StackException("StackException: peek() attempt on empty stack"); } } public int size() { return deque.size(); } public String toString() { String result = "Stack: "; result += deque; return result; } // Test public static void main(String[] args) { Stack<String> ss = new Stack<String>(); ss.push("one"); ss.push("two"); System.out.println(ss); String a = ss.pop(); System.out.println("popped " + a); System.out.println(ss); Stack<Double> sd = new Stack<Double>(); sd.push(42.33); sd.push(-132.2333); System.out.println(sd); double b = sd.pop(); System.out.println("popped " + b); System.out.println(sd); } }
And the accompanying exception class
public class StackException extends java.lang.RuntimeException {
public StackException(String s) {
super(s);
}
}
Download and test the Stack.
Now the Queue implementation and its exception class.
public class Queue<T> { Deque<T> deque; public Queue() { deque = new Deque<T>(); } public boolean isEmpty() { return deque.isEmpty(); } public void enqueue(T newItem) { deque.addLast(newItem); } public T dequeue() throws QueueException { try { T item = deque.removeFirst(); return item; } catch (DequeException e) { throw new QueueException("QueueException: dequeue() attempt on empty stack"); } } public T peek() throws QueueException { try { T item = deque.peekFirst(); return item; } catch (DequeException e) { throw new QueueException("QueueException: peek() attempt on empty stack"); } } public int size() { return deque.size(); } public String toString() { String result = "Queue: "; result += deque; return result; } // Test public static void main(String[] args) { Queue<String> ss = new Queue<String>(); ss.enqueue("one"); ss.enqueue("two"); System.out.println(ss); String a = ss.dequeue(); System.out.println("dequeueed " + a); System.out.println(ss); Queue<Double> sd = new Queue<Double>(); sd.enqueue(42.33); sd.enqueue(-132.2333); System.out.println(sd); double b = sd.dequeue(); System.out.println("dequeueed " + b); System.out.println(sd); } }
public class QueueException extends java.lang.RuntimeException { public QueueException(String s) { super(s); } }
Finally, we can talk about how to represent a maze, and a position in a maze. Here is an example of the kind of mazes we will be searching.
12 10 ########## # G # # # # # # # # # # # # # # # # # ### # # # # S # # # ##########
S is the starting position, and G is the goal. Walls are marked by #.
The maze can be stored as a two-dimensional character array.
A Position is just a row and column index.
The Maze class constructor will read a maze like this from a text file whose name is given as a command line argument.
public class Position { protected int row; protected int column; public Position(int rowArg, int columnArg) { row = rowArg; column = columnArg; } public String toString() { return "(" + row + "," + column + ")"; } public int getRow() { return row; } public int getColumn() { return column; } public Position up() { return new Position(row-1,column); } public Position down() { return new Position(row+1,column); } public Position right() { return new Position(row,column+1); } public Position left() { return new Position(row,column-1); } // Test it. public static void main(String[] args) { Position a = new Position(0,0); Position b = new Position(10,20); System.out.println(a + " " + b); } }
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.StringTokenizer; public class Maze { protected char [][] maze; protected int nRows; protected int nCols; protected String filename; final char WALL = '#'; final char GOAL = 'G'; final char START = 'S'; final char MARK = '*'; final char OPEN = ' '; Maze(String filenameArg) { filename = filenameArg; BufferedReader input; String line; StringTokenizer tokenizer; nRows = 0; try { /* File is expected to look like this. Here is maze with 12 rows and 10 columns. 12 10 ########## # G # # # # # # # # ### # # # # # # S # # # ########## */ int currentRow = 0; input = new BufferedReader(new FileReader(filename)); while ((line = input.readLine()) != null) { if (nRows == 0) { // Assume first line of file is nRows nCols tokenizer = new StringTokenizer(line); nRows = Integer.parseInt(tokenizer.nextToken()); nCols = Integer.parseInt(tokenizer.nextToken()); maze = new char[nRows][nCols]; } else { // Other rows look like // # G # for (int c = 0; c < nCols; c++) maze[currentRow][c] = line.charAt(c); currentRow ++; } } } catch (IOException e) { e.printStackTrace(); System.exit(1); } } public void clear() { for (int r=0; r < nRows; r++) for (int c=0; c < nCols; c++) if (maze[r][c] == MARK) maze[r][c] = OPEN; } public Position findStart() { for (int r = 0; r < nRows; r++) { for (int c = 0; c < nCols; c++) { if (maze[r][c] == START) { return new Position(r,c); } } } return null; } public boolean isOpen(Position p) { char here = maze[p.row][p.column]; return here == OPEN; // || here == GOAL; // so goal position will be tried, and found! } public boolean isGoal(Position p) { return maze[p.row][p.column] == GOAL; } public void mark(Position p) { if (maze[p.row][p.column] == OPEN) maze[p.row][p.column] = MARK; } public int countMarks() { int count = 0; for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) if (maze[r][c] == MARK) ++count; return count; } public String toString() { String result = ""; for (int r = 0; r < nRows; r++) { for (int c = 0; c < nCols; c++) { result += maze[r][c]; } if (r < nRows-1) result += '\n'; } return result; } public static void main(String [] args) { Maze maze = new Maze(args[0]); System.out.println(maze); } }
Download these three files, compile Maze.java, and run it.
Finally, we are ready to solve a maze. To solve a maze, we will follow this algorithm:
Here is the first implementation, using a stack.
public class SolveMazeStack { public static void main(String [] args) { if (args.length < 1) { System.out.println("Usage: java SolveMazeStack mazeFileName.txt <debug>"); System.exit(1); } String filename = args[0]; Maze maze = new Maze(filename); boolean debug = false; if (args.length > 1) if (args[1].equals("debug")) debug = true; Position start = maze.findStart(); Stack<Position> unexplored = new Stack<Position>(); unexplored.push(start); boolean found = false; while( !unexplored.isEmpty() ) { Position tryFromHere = unexplored.pop(); maze.mark(tryFromHere); if (debug) { System.out.println(maze); // to show progress System.out.println("Size of unexplored is now " + unexplored.size()); } Position up = tryFromHere.up(); Position down = tryFromHere.down(); Position right = tryFromHere.right(); Position left = tryFromHere.left(); if (maze.isGoal(up) || maze.isGoal(right) || maze.isGoal(down) || maze.isGoal(left)) { found = true; break; } if (maze.isOpen(up)) unexplored.push(up); if (maze.isOpen(right)) unexplored.push(right); if (maze.isOpen(down)) unexplored.push(down); if (maze.isOpen(left)) unexplored.push(left); } if (found) { System.out.printf("With Stack: Goal found! %d steps tried. unexplored contains %d positions.\n", maze.countMarks() , unexplored.size()); System.out.println(maze); } else { System.out.printf("With Stack: Goal not found! %d steps tried. unexplored contains %d positions.\n", maze.countMarks() , unexplored.size()); System.out.println(maze); } } }
Download, compile, and run.
Now, see what happens when you use a Queue for the unexplored data structure instead of a Stack. To do this, do these steps.
SolveMazeQueue in file SolveMazeQueue.java that initially has thesame contents as SolveMazeStack.java.Now, discuss the effect of using a queue versus a stack. Try to come up with your own maze file that shows a much bigger difference in the number of steps tried for the stack versus the queue.
Discuss why the number of positions stored in the unexplored data structure may become large, even larger than the total number of open positions in the maze.
Show the instructor your working SolveMazeQueue code. Sign the attendance sheet.