1. Works Applications Programming Assignment
Chinmay Chauhan, 09005010, IIT-Bombay
Interface1 Implementation (ExamPeekableQueue):
File: ExamPeekableQueue.java
package jp.co.worksapp.recruiting;
public interface ExamPeekableQueue<E extends Comparable<E>> {
public void enqueue(E e);
public E dequeue();
public E peekMedian();
public E peekMaximum();
public E peekMinimum();
public int size();
}
I have used 2 approaches to solve the problem. Below is the code and logic used for both approaches.
Approach #1: We maintain a sorted list of elements in queue. For enqueue, whenever we insert an element in
queue, we find the correct location of that element in the sorted List using binarySearch and insert it at that
location. As per java documentation, ArrayList.add operation takes amortized O(1) time, binary search takes
O(lgN) time where N is the number of elements in the queue, so overall complexity of insertion is O(lgN)
amortized, and O(N) worst case. Same complexity holds for dequeue operation. So we have O(lgN) amortized
time for enqueue and dequeue operation, but worst case is O(N). Finding minimum, maximum and median once
we have a sorted list is O(1) worst case.
Code for Approach #1 (File: ExamPeekableQueueImpl.java)
package jp.co.worksapp.recruiting;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
public class ExamPeekableQueueImpl<E extends Comparable<E>> implements ExamPeekableQueue<E>{
private List<E> queue;
// For storing queue elements in sorted Order for efficiently getting Min, Max, Median
private List<E> sortedQueue;
public ExamPeekableQueueImpl(){
queue = new ArrayList<E>();
sortedQueue = new ArrayList<E>();
}
@Override
public void enqueue(E e){
2. if (e == null) {
throw new IllegalArgumentException();
}
queue.add(e); // Adds e to the tail of queue
int index = Collections.binarySearch(sortedQueue, e);
if (index < 0) { // Element e is not present in the queue
sortedQueue.add(-index - 1, e);
} else { // Element e is present in the queue
sortedQueue.add(index + 1, e);
}
}
@Override
public E dequeue(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
E e = queue.get(0);
queue.remove(0);
int index = Collections.binarySearch(sortedQueue, e);
sortedQueue.remove(index); // Remove e from sortedQueue
return e;
}
@Override
public E peekMedian(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
int size = this.size();
return sortedQueue.get(size/2);
}
@Override
public E peekMaximum(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
int size = this.size();
return sortedQueue.get(size-1);
}
@Override
public E peekMinimum(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
return sortedQueue.get(0);
}
@Override
public int size(){
return queue.size();
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append("[ ");
for (E e : queue) {
s.append(e + " ");
}
s.append("]");
s.append(" [ ");
3. for (E e : sortedQueue) {
s.append(e + " ");
}
s.append("]");
return s.toString();
}
}
Approach #2: We maintain 2 TreeSets, 1st
one contains N/2 (or N/2 + 1) smallest elements and 2nd
one contains
the rest. If we maintain this invariant throughout, we see that median has to be either the Largest element of
TreeSet1 or the smallest element of TreeSet2. Enqueue & Dequeue both operation take O(lgN) time since
inserting or deleting elements from any TreeSet of size N is O(lgN). Even in this case, the maximum, minimum
and median operation needs O(1) time. So we have significantly improved our complexity. However, since Java
TreeSet doesn’t support duplicates, so our code works only for distinct elements. If Java had a implementation
of MultiSet, then we could have support for non-distinct elements too.
Code for Approach #2 (File: ExamPeekableQueueImpl.java)
package jp.co.worksapp.recruiting;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.TreeSet;
public class ExamPeekableQueueImpl<E extends Comparable<E>> implements ExamPeekableQueue<E>{
private List<E> queue;
private TreeSet<E> left;
private TreeSet<E> right;
public ExamPeekableQueueImpl(){
queue = new ArrayList<E>();
left = new TreeSet<E>();
right= new TreeSet<E>();
}
@Override
public void enqueue(E e){
if (e == null) {
throw new IllegalArgumentException();
}
queue.add(e);
if(left.isEmpty()){ // We are adding the first element
left.add(e);
return;
}
if (left.size() == 1 && right.size() == 0) {
if (e.compareTo(left.last()) == -1) {
right.add(left.last());
left.remove(left.last());
left.add(e);
}
else {
right.add(e);
}
return;
4. }
if(e.compareTo(left.last()) == -1 || e.compareTo(left.last()) == 0){ // Less or Equal
if (left.size() == right.size()) {
left.add(e);
}
else if (left.size() == right.size()+1) {
right.add(left.last());
left.remove(left.last());
left.add(e);
}
}
else if (e.compareTo(left.last()) == 1 && e.compareTo(right.first()) == -1) {
if (left.size() == right.size()){
left.add(e);
}
else if (left.size() == right.size()+1) {
right.add(e);
}
}
else {
if (left.size() == right.size()) {
left.add(right.first());
right.remove(right.first());
right.add(e);
}
else if (left.size() == right.size()+1) {
right.add(e);
}
}
}
@Override
public E dequeue(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
E e = queue.get(0);
// Maintain the invariant (left.size - right.size) == 0 / 1
if(e.compareTo(left.last()) == -1 || e.compareTo(left.last()) == 0){ // Less or Equal
if (left.size() == right.size()) {
left.add(right.first());
left.remove(e);
right.remove(right.first());
}
else if (left.size() == right.size()+1) {
left.remove(e);
}
}
else {
if (left.size() == right.size()) {
right.remove(e);
}
else if (left.size() == right.size() + 1){
right.remove(e);
right.add(left.last());
left.remove(left.last());
}
}
// Remove element from FIFO queue
queue.remove(0);
// If queue is empty, empty left and right TreeSets
if (queue.isEmpty()) {
if (!left.isEmpty()) left.remove(0);
if (!right.isEmpty()) right.remove(0);
}
5. return e;
}
@Override
public E peekMedian(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
if((left.size()+right.size())%2 == 0) return right.first();
else return left.last();
}
@Override
public E peekMaximum(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
if(right.isEmpty()) return left.last();
else return right.last();
}
@Override
public E peekMinimum(){
if (queue.isEmpty()) {
throw new NoSuchElementException();
}
return left.first();
}
@Override
public int size(){
return queue.size();
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append("[ ");
for (E e : queue) {
s.append(e + " ");
}
s.append("]");
s.append(" [ ");
for (E e : left) {
s.append(e + " ");
}
s.append("]");
s.append(" [ ");
for (E e : right) {
s.append(e + " ");
}
s.append("]");
return s.toString();
}
}
6. Interface2 Implementation (ExamImmutableQueue):
Approach: We maintain 2 pointers enqueueHead, dequeueHead. We permanently add elements to the queue
using addElement(e) function. Functions enqueue(), dequeue(), peek() do not modify the queue and their
complexity is O(1) amortized. If we enqueue, elements are always appended at the start of enqueueHead list. If
while dequeuing, the dequeueHead is pointing to null, we move all elements from enqueueHead to
dequeueHead in the reverse Order. (More logic is explained as comments in source files)
File: ExamImmutableQueue.java
package jp.co.worksapp.recruiting;
public interface ExamImmutableQueue<E> {
public ExamImmutableQueue<E> enqueue(E e);
public ExamImmutableQueue<E> dequeue();
public E peek();
public int size();
}
File: ExamImmutableQueueImpl.java
package jp.co.worksapp.recruiting;
import java.util.NoSuchElementException;
/*
* Logic Used : We maintain 2 pointers enqueueHead, dequeueHead.
* We permanently add elements to the queue using addElement(e) function.
* Function enqueue/dequeue do not modify/mutate the queue.
* Functions enqueue(e) and dequeue() have O(1) amortized time complexity.
*
* If we enqueue, elements are always appended at the start of enqueueHead List, so
* if enqueueHead -> 3->2->1, and we do enqueueHead(10), then we return the appropriate
* pointer to 10->3->2->1, but DO NOT modify enqueueHead. So enqueueHead is still pointing to 3->2-
>1
* So enqueue(e) needs O(1) time in every case.
*
* If while dequeuing, the dequeueHead is pointing to null, we move all elements from
* enqueueHead to dequeueHead in the reverse Order.
* Eg. say enqueueHead -> 3->2->1, dequeueHead -> null, then on calling dequeue(),
* enqueueHead -> null, dequeueHead -> 1->2->3, so now dequeueHead points to the first
* element of queue. Only in this specific case we need O(N) time for dequeue(), in all
* other cases dequeue() is O(1). Hence the overall time complexity is O(1) amortized
*
* For peek(), if dequeueHead is not empty, then we return the first element that dequeueHead
* points to. Otherwise, we move all elements from enqueueHead list to dequeueHead (in reverse,
* and then return the first element that dequeueHead points to.
* Hence peek() has O(1) amortized time complexity.
*/
public class ExamImmutableQueueImpl<E> implements ExamImmutableQueue<E> {
class Node<T> {
public T data;
public Node<T> next;
public Node(T t){data = t; next = null;}
}
7. private Node<E> enqueueHead;
private Node<E> dequeueHead;
private int size;
public ExamImmutableQueueImpl() {
enqueueHead = null;
dequeueHead = null;
size = 0;
}
public ExamImmutableQueueImpl(Node<E> e, Node<E> d, int sz) {
enqueueHead = e;
dequeueHead = d;
size = sz;
}
@Override
public ExamImmutableQueue<E> enqueue(E e){
if (e == null) {
throw new IllegalArgumentException();
}
Node<E> temp = new Node<E>(e);
temp.next = enqueueHead;
return new ExamImmutableQueueImpl<E> (temp, dequeueHead, size+1);
}
@Override
public ExamImmutableQueue<E> dequeue(){
if (dequeueHead == null && enqueueHead == null){
throw new NoSuchElementException();
}
if(dequeueHead != null){
return new ExamImmutableQueueImpl<E> (enqueueHead, dequeueHead.next, size-1);
}
else {
while(enqueueHead != null){
Node<E> temp = new Node<E>(enqueueHead.data);
temp.next = dequeueHead;
dequeueHead = temp;
enqueueHead = enqueueHead.next;
}
return new ExamImmutableQueueImpl<E>(enqueueHead, dequeueHead.next, size-1);
}
}
// To permanently add some elements to the queue
public void addElement(E e) {
Node<E> temp = new Node<E>(e);
temp.next = enqueueHead;
enqueueHead = temp;
size++;
}
@Override
public E peek(){
if (enqueueHead == null && dequeueHead == null){
throw new NoSuchElementException();
}
else if (dequeueHead != null){
return dequeueHead.data;
}
else { // Move all elements of enqueueHead to dequeueHead in reverse order
while(enqueueHead != null){
Node<E> temp = new Node<E>(enqueueHead.data);
temp.next = dequeueHead;
dequeueHead = temp;
8. enqueueHead = enqueueHead.next;
}
return dequeueHead.data;
}
}
@Override
public int size(){
return size;
}
// For checking contents of the queue, Printing the queue contents
public String toString() {
if (dequeueHead != null) { // Print dequeueHead before enqueueHead
String s = "";
s = "[ " + s;
Node<E> tmp = dequeueHead;
while (tmp != null) {
s = s + tmp.data + " ";
tmp = tmp.next;
}
String t = "";
tmp = enqueueHead;
while (tmp != null) {
t = tmp.data + " " + t;
tmp = tmp.next;
}
s = s+t + "]";
return s;
}
else { // Since dequeueHead is null, just print enqueueHead in reverse
String s = "";
Node<E> tmp = enqueueHead;
while (tmp != null) {
s = tmp.data + " " + s;
tmp = tmp.next;
}
s = "[ " + s + "]";
return s;
}
}
}