CSSE 230
Stacks And Queues Programming Assignment - 100 Points
Overview and objectives
Stacks and queues are simple, yet very useful data structures.
In this assignment, you will:
- Implement the Queue ADT using a circular array that grows efficiently.
- Analyze four different implementations of the Queue ADT.
- Develop practical algorithms that require a stack by
creating a tool to evaluate simple mathematical expressions.
Check-out and Team Instructions
- Check your team number in the email you received from CATME or from the spreadsheet on Moodle.
- Accept the github classroom invitation link:
-
If a team with your team number (e.g.,
team-02
for team number 2) does not exist, then create one.
-
If the team already exists, then please simply join it.
-
Please coordinate with your teammate so that you create only one team.
- Commit your working code often to the repository as you go.
- Optional but highly recommended: To make sure communication is easier and to get the best help out of your instructor, do the following:
-
Create a group chat with all the members of your team on Microsoft Teams.
-
Add me to your group chat.
-
Rename the group chat to
CSSE230-StacksAndQueuesProject-TeamXX
-
I will randomly and often check these group chats to get an idea on how you're doing. I can also provide some
help and hints if I see fit.
Implement the Queue ADT using a growable circular array
Recall from day 1 of this course that when dealing with arrays
that must grow in size, it is most efficient to double the capacity
when the array is full, so that the expensive resizing operation occurs
infrequently.
Your GrowableCircularArrayQueue class will implement the
following operations specified by the Collections API:
- clear()
- contains()
- isEmpty()
- size()
The GrowableCircularArrayQueue will also implement the following operations specified by the Queue ADT:
- dequeue()
- enqueue()
- peek()
You will also implement a toString() method for testing and convenience.
I added all of these to a nice SimpleQueue interface - make the GrowableCircularArrayQueue class implement that interface. |
|
Implementation requirements and hints:
- To make your work easier, we are only requiring you to grow
the array when needed, and not requiring you to shrink it if many items
are removed.
- As you know, resizing the array takes amortized O(1) time.
We say amortized since doubling the capacity is O(n), but on average is O(1) since it occurs infrequently, as we learned on day 1.
- When implementing a stack, it is easy to make a remove
method that runs in O(1) time. It is also possible to do this when
implementing a queue, but it requires some thought . Note that when
dequeuing items, you may not shift the
array items over to fill the space, since that would require O(n) array
accesses. Instead, make use of the empty space by "wrapping around" the
array indices (like a circle) once you reach the end of the array. This
is called a "circular" queue; ours also grows, so we need to be careful
to track where to enqueue and dequeue items. Consider the following
scenario:
- Create a new queue (so internal capacity = 5):
[ | | | | ]
- Enqueue 8 items (so that the size is 8, but the
capacity has doubled to 10). [ a | b | c | d | e | f | g | h
| | ]
- Dequeue 5 of them (so that the size decreases to 3, but
the capacity is still 10). [ |
| | | | f | g | h |
| ]. (Of course, there is no need to delete the dequeued
ones.)
- Enqueue 3 of them, which wraps around instead of
growing the array. [ k | | | |
| f | g | h | i | j ].
- Enqueue 4 more, which will fill the array: [ k
| L | m | n | o | f | g | h |
i | j ].
- Enqueue one more, which causes the array to double in
capacity. You will probably find it simpler to re-order them: [ f | g |
h | i | j | k | L | m | n | o | | |
| | | | | |
| | ]
- You will need to decide which fields to use.
- I provided you with a couple of JUnit tests, including the
scenario above. You may want to add more JUnit tests to check various
expected behavior. If so, name your test cases in such a way that the
name explains the test, like testThatEnqueueResizesArray() and testDequeuingUntilEmptyThenEnqueing().
Implement the Queue ADT using two stacks
You can implement a Queue using two stacks. How
would you implement it? Don't worry too much about efficiency. We just
want you to get more practice using one data structure to implement
another one.
Like above, create a class, QueueFromStacks<T>
that implements SimpleQueue<T> (and thus its methods). You'll
need to add in toString() from scratch. Your implementation must use
two java.util.Stacks as instance fields.
How will you write enqueue and
dequeue? There are a few different ways to do it, depending on whether
you want enqueue or dequeue to be more efficient, or if you want to
balance them. (The way I wrote it reminded me of a "Slinky" toy.)
One thing is certain - you shouldn't use any Stack method that takes an index as a parameter since they really aren't part of the
Stack ADT; in fact, push(), pop(), and isEmpty() are sufficient.
|
|
Analyze four implementations of the Queue ADT
The growable circular queue is an interesting implementation
of a queue, but it is certainly not the only one. Here are three others and some questions to help you think:
- LinkedList. This is how Java
does it. Enqueuing and dequeuing items are both efficient. How
efficient? Why? Use what you know about linked lists.
- ArrayList. This implementation
is very easy. Just call the array list's .add(item) and .remove(0)
method to add to the tail of the list and remove from the head.
However, this suffers one major performance issue. What is it?
- Two stacks. How efficient is your implementation
above? There are a few correct answers here - of course, it will depend
on how you wrote it.
For this part of the assignment, state the Big-Theta runtime
of enqueue() and dequeue() for each of the four implementations. Then
explain how each operation works in enough detail to justify the stated
runtime. Record your answers in the Analysis.docx file in your Eclipse
project and commit your work.
Write an expression evaluator
Programming languages must evaluate expressions following the
syntax of the language. In this assignment, you will write a tool that
will evaluate simple mathematical expressions given in different forms
like 8 + 7 * 5 (infix) and 8 7 5 * + (postfix).
You are used to evaluating mathematical expressions that are
written using infix notation, that is, each
operator appears between the operands (numbers), such as
4 * 5
or
6 * ( 10 - 7 ).
However, that is not the only way to write expressions. In postfix
notation, the operator is written after the operands. The previous examples would be written as
4 5 *
and
6 10 7 - *,
respectively.
Postfix notation has two advantages over infix notation.
First, the order of operations is unambiguous without requiring
parentheses. As we will see, 3 + ( 4 * 5 ) can be
written as 3 4 5 * + , while ( 3 + 4 )
* 5 can be written as 3 4 + 5 *. Second, computers
can evaluate postfix expressions easily using a stack.
In the very first example above, you can save the operands (4 and 5) on a stack and when you finally read an operator,
just pop two operands and apply the operator. The second example works
similarly - push the three operands (6, 10, and 7). When you read the -
sign, pop the top two operands (7 and 10), apply the -, then push the
answer back. When you read the * sign, pop the top two (3 and 6) and
apply the * and push the answer. When you reach the end of the input,
pop the answer. Your stack should now be empty - if not, the expression
was malformed (too many operands). You can also detect if there were
too many operators - can you see how?
Of course, calculators and computers don't require you to
enter your expressions in postfix - they do it for you.
Surprisingly, there is an algorithm to convert infix expressions to
postfix that also uses a stack. Hint: in
this infix to postfix conversion algorithm, the stack holds operators,
not operands. If you try a couple examples, you'll see that this makes
sense, since the order of the operands does not change, but the
operators must somehow be stored and moved later into the output string.
So to evaluate infix, just convert it to postfix, then apply
the algorithm described above!
Your starting code includes an abstract class called
Evaluator, that has an abstract method to evaluate strings and return
an int.
You will write PostfixEvaluator and InfixEvaluator subclasses in which you will override this method. This is not the only way to do
this, but is good extensible, object-oriented design. (Perhaps we might
want to write a PrefixEvaluator later, who knows?)
We want you to use the infixToPostfix conversion algorithm as
the first step of evaluating infix expressions, so the tests include
specific tests to isolate that method. Note that this method takes a
String argument and returns a String.
Hint: The starting code has errors. I tried to make it so that
using Eclipse to fix the errors in the Main class will
give you stubs of the classes and methods. It should even guess that InfixEvaluator should extend Evaluator and that the return
type of infixToPostfix should be a String. If
not, you can make these adjustments. You'll also need to add your own
comments. |
|
Requirements for the Evaluator
- Your algorithm must work in linear time with respect to the
number of tokens (operators and operands). Thus, you may only read
through the String once and build up the output string once using a
loop - no backtracking or inserting output into arbitrary elements in
the input string. (Exception: you may use two passes to evaluate infix
expressions, one to convert to postfix and one to evaluate the postfix,
in keeping with my statement above.) This also means we must build up
the output using a StringBuilder, not a String - see the javadoc for
StringBuilder
for examples of how to use it if you haven't.
- Both algorithms must use a stack. For this section, you may use java.util.Stack instead of implementing your own.
- Neither algorithm can use recursion - recursion does use a
stack internally, but we want the use of the stack to be explicit here.
- Both algorithms must handle the four basic operators (+, -,
*, and /), plus exponents (^).
- For full credit, you must be able to handle parentheses in
infix expressions.
- For full credit, you must throw ArithmeticExceptions when
the input is malformed, as demonstrated in the tests.
- All math will be integer math, thus you will drop
remainders during division if they occur.
- You must follow the standard order of operations precisely.
Therefore you may not change the order things are added and you must
apply operations of the same precedence from left to right. (In other
words, you may not rely on addition and multiplication being
commutative and associative.)
Simplifying Assumptions and Hints
- You may assume the tokens (operators and operands) in the
input strings are all separated by spaces, thus making it easy to use a
Scanner to parse the String one token at a time.
- You may assume that the only operands in the input will be
non-negative integers.
- You may add whatever additional helper methods you need.
For example, I added one to compare the precedence of the infix
operators.
Grading
Grading of the implementations will be based on unit tests and
programming style (clear code including javadoc & internal comments).
I reserve the right to add unit tests and to change the point
values of the given unit tests.
Grading of the analysis will be based on the runtimes and
justification.
Point distribution: queue implementations: 34 points,
analysis: 16 points, evaluator: 50 points. Total = 100 points.