Project 1: Deques

Prerequisites to running the code.

Instructions: https://fa22.datastructur.es/materials/proj/proj1/

Solution: https://github.com/tomthestrom/cs61b/tree/master/proj1

This is were the course got interesting for me.

Deque

  • stands for "double ended queue"

  • sequence container with dynamic size, that can be expanded or contracted on both ends

About the project

The main task was to implement a Deque ADT. The first implementation felt quite natural, since we were asked to use a Linked List based implementation. The second one used an array based implementation, so I had to figure out an implementation of a circular array.

In the second part, I used my implementation of the Deque API to be the underlying data structure of a Guitar Hero clone. This part only included writing a couple lines to hook up my data structures with the rest of the Guitar Hero clone code, that had been provided by the CS 61 B instructors.

The project also includes some unit tests for the Deque data structures, some of them provided by the authors, more extensive written by me.

Deque API

Specifically, any deque implementation must have exactly the following operations:

  • public void addFirst(T item): Adds an item of type T to the front of the deque. You can assume that item is never null.

  • public void addLast(T item): Adds an item of type T to the back of the deque. You can assume that item is never null.

  • public boolean isEmpty(): Returns true if deque is empty, false otherwise.

  • public int size(): Returns the number of items in the deque.

  • public void printDeque(): Prints the items in the deque from first to last, separated by a space. Once all the items have been printed, print out a new line.

  • public T removeFirst(): Removes and returns the item at the front of the deque. If no such item exists, returns null.

  • public T removeLast(): Removes and returns the item at the back of the deque. If no such item exists, returns null .

  • public T get(int index): Gets the item at the given index, where 0 is the front, 1 is the next item, and so forth. If no such item exists, returns null. Must not alter the deque!

In addition, we also want our two deques to implement these two special methods:

  • public Iterator<T> iterator(): The deques objects we’ll make are iterable (i.e. Iterable<T>) so we must provide this method to return an iterator.

  • public boolean equals(Object o): Returns whether or not the parameter o is equal to the deque. o is considered equal if it is a deque and if it contains the same contents (as determined by the generic T’s equals method) in the same order. (You’ll need to use the instance of keywords for this. Read here for more information)

1. Linked List Deque #

Additional requirements

Your operations are subject to the following rules:

  • add and remove operations must not involve any looping or recursion. A single such operation must take “constant time”, i.e. execution time should not depend on the size of the deque. This means that you cannot use loops that go over all/most elements of the deque.

  • get must use iteration, not recursion.

  • size must take constant time.

  • Iterating over the LinkedListDeque using a for-each loop should take time proportional to the number of items.

  • Do not maintain references to items that are no longer in the deque. The amount of memory that your program uses at any given time must be proportional to the number of items. For example, if you add 10,000 items to the deque, and then remove 9,999 items, the resulting memory usage should amount to a deque with 1 item, and not 10,000. Remember that the Java garbage collector will “delete” things for us if and only if there are no pointers to that object.

Implementation

About

The underlying implementation is a Doubly Linked List, it's items are instances of a nested class - DequeNode, in order to not deal with special cases when the Deque is empty, I used a sentinel node.

I did some extras such as:

  1. get(int index):

    • in case the element's index is in the second half of the Deque, iterate from the back - thus the real lookup is basically O(N/2)

  2. getRecursive(int index):

    • a recursive implementation of get

    • also searches from the back, in case the index is in the 2nd half of the Deque.

Code

2. Array Deque #

Additional requirements

For this implementation, your operations are subject to the following rules:

  • add and remove must take constant time, except during resizing operations.

  • get and size must take constant time.

  • The starting size of your array should be 8.

  • The amount of memory that your program uses at any given time must be proportional to the number of items. For example, if you add 10,000 items to the deque, and then remove 9,999 items, you shouldn’t still be using an array of length 10,000ish. For arrays of length 16 or more, your usage factor should always be at least 25%. This means that before performing a remove operation that will bring the number of elements in the array under 25% the length of the array, you should resize the size of the array down. For arrays under length 16, your usage factor can be arbitrarily low.

Implementation

About

An array based deque implemented using a circular array. In order for it to function, I needed to keep track of the first and last index.

Implementing this is a bit tricky, because it can easily happen, that the first index is actually at a larger index in the underlying array, than the last index.

For example, to get an element by its index, since the first element in the Deque is at firstIndex:

  • we calculate the requested element's index in the circular array by adding it to the firstIndex

    • in case that the sum of those two is larger than the length of the underlying circularArray

      • we need to subtract the length of the array from this sum - to see this in practice, check out the get(int index) method.

In case we're adding elements into the deque and we fill the circularArray, we resize via resizeUpIfNecessary private method - when resizing, the size of the underlying array is doubled.

In case we're removing elements from the deque we might need to resize via resizeDownIfNecessary - we do this in case our usage ratio (R) is <= 0.25 - by reducing circularArray's length to half.

Code

3. MaxArrayDeque #

Additional requirements

A MaxArrayDeque has all of the methods that an ArrayDeque has, but it also has 2 additional methods and a new constructor:

  • public MaxArrayDeque(Comparator<T> c): creates a MaxArrayDeque with the given Comparator.

  • public T max(): returns the maximum element in the deque as governed by the previously given Comparator. If the MaxArrayDeque is empty, simply return null.

  • public T max(Comparator<T> c): returns the maximum element in the deque as governed by the parameter Comparator c. If the MaxArrayDeque is empty, simply return null.

Implementation

About

Since MaxArrayDeque should have all the methods of the ArrayDeque, I simply extended ArrayDeque and implemented the required methods.

Code

Guitar Hero #

The code for most of the clone was provided, the only thing left to do was to edit GuitarString, a class which uses an Deque<Double> to implement the Karplus-Strong algorithm to synthesize a guitar string sound.

GuitarString

We want to finish the GuitarString file, which should use the deque package to replicate the sound of a plucked string. We’ll be using the Karplus-Strong algorithm, which is quite easy to implement with a Deque.

The Karplus-Algorithm is simply the following three steps:

  1. Replace every item in a Deque with random noise (double values between -0.5 and 0.5).

  2. Remove the front double in the Deque and average it with the next double in the Deque (hint: use removeFirst) and get()) multiplied by an energy decay factor of 0.996 (we’ll call this entire quantity newDouble). Then, add newDouble to the back of the Deque.

  3. Play the double (newDouble) that you dequeued in step 2. Go back to step 2 (and repeat forever).

Or visually, if the Deque is as shown on the top, we’d remove the 0.2, combine it with the 0.4 to form 0.2988, add the 0.2988, and play the 0.2.

Implementation

About

The instructions are quite clear, so implementing this wasn't too difficult.

Code

Last updated