Homework 10
Homework 10
Homework 10
R-7.1 Drawarepresentation,akintoExample7.1,ofaninitiallyemptylistLafterper-
forming the following sequence of operations: add(0, 4), add(0, 3), add(0, 2), add(2,
1), add(1, 5), add(1, 6), add(3, 7), add(0, 8).
Answer:
This final state is reflected in the linked list representation: 8 -> 2 -> 3 -> 6 -> 5 -> 1 -
> 7 -> 4.
R-7.3 Give an implementation of the deque ADT using an array list for storage.
Answer:
Here's an implementation of the Deque ADT using an ArrayList for storage in Java:
Java
public ArrayDeque() {
elements = new ArrayList<>();
front = 0;
rear = -1; // To account for empty deque initially
}
@Override
public void addFirst(E element) {
if (isEmpty()) {
elements.add(element);
rear = 0;
} else {
elements.add(front, element);
front--; // Decrement front to point to the new first element
}
}
@Override
public void addLast(E element) {
elements.add(element);
rear++;
}
@Override
public E removeFirst() {
if (isEmpty()) {
throw new NoSuchElementException("Deque is empty");
}
E element = elements.get(front);
elements.remove(front);
if (isEmpty()) {
rear = -1; // Reset rear for empty deque
} else {
front++;
}
return element;
}
@Override
public E removeLast() {
if (isEmpty()) {
throw new NoSuchElementException("Deque is empty");
}
E element = elements.get(rear);
elements.remove(rear);
rear--;
return element;
}
@Override
public E getFirst() {
if (isEmpty()) {
throw new NoSuchElementException("Deque is empty");
}
return elements.get(front);
}
@Override
public E getLast() {
if (isEmpty()) {
throw new NoSuchElementException("Deque is empty");
}
return elements.get(rear);
}
@Override
public boolean isEmpty() {
return front > rear;
}
@Override
public int size() {
return rear - front + 1; // Account for possible negative front index
}
}
This implementation utilizes an ArrayList for storing elements and keeps track of the
front and rear indices to efficiently access the first and last elements of the deque.
The methods implement the standard Deque operations:
R-7.5 The java.util.ArrayList includes a method, trimToSize(), that replaces the un-
derlying array with one whose capacity precisely equals the number of elements
currently in the list. Implement such a method for our dynamic version of the
ArrayList class from Section 7.2.
Answer:
The implementation of the trimToSize() method for your dynamic ArrayList class:
Java:
This method checks if the current capacity (elements.length) of the underlying array
is greater than the actual number of elements in the list (size()). If it is, the method
creates a new array using Arrays.copyOf() with a size equal to the current list size.
This effectively reduces the memory footprint of the ArrayList by removing unused
space in the underlying array.
• if (elements.length > size()): This condition checks if the capacity of the underlying
array is larger than the current number of elements in the list.
• elements = Arrays.copyOf(elements, size()): If the condition is true, this line
creates a new array using Arrays.copyOf(). It takes two arguments:
➢ The original array (elements)
➢ The new size of the array, which is obtained from the size() method.
• This new array is then assigned back to the elements field of the ArrayList object.
Important Note:
R-7.7 Consider an implementation of the array list ADT using a dynamic array, but
instead of copying the elements into an array of double the size (that is, from N to
2N) when its capacity is reached, we copy the elements into an array with ⌈N/4⌉
additional cells, going from capacity N to N + ⌈N/4⌉. Show that performing a
sequence of n push operations (that is, insertions at the end) still runs in O(n) time in
this case.
Answer:
Analysis:
Argument:
• In the worst case, when the list is full and needs to be resized, the cost of copying
elements to a new array is proportional to the current capacity (C).
• However, due to the growth rate, the capacity increases by at most ceiling(C / 4)
each time. This means the cost of resizing is spread out over at least 4 append
operations (since the capacity grows by at most a quarter each time).
• In simpler terms, for every additional element appended, the cost of resizing is
distributed over the next 4 elements.
Formalization:
Let T(n) be the total time to perform n append operations. We can break down the
cost into two parts:
• n - The base cost of appending each element (assuming constant time for basic
operations like incrementing size).
• R(n) - The total cost of resizing operations throughout the n appends.
Amortized Cost:
The amortized cost per append operation (A) can be expressed as:
A = T(n) / n = (n + R(n)) / n
To show that the overall cost is O(n), we need to show that R(n) grows slower than
linearly with n.
• Let k be the number of times the list needs to be resized during n appends.
• In the worst case, each resize incurs a cost proportional to the previous capacity
(C_i).
• Since the capacity grows by at most a quarter each time, we can express the cost
of the ith resize as C_i <= 4 * C_(i-1).
• Summing the costs of all k resizes:
R(n) = sum(C_i) for i = 1 to k <= sum(4 * C_(i-1)) for i = 1 to k
• Using the property of geometric series, this sum can be bounded by a constant
factor multiplied by the initial capacity (C).
Conclusion:
Since R(n) is bounded by a constant factor multiplied by the initial capacity, which is
itself bounded by a constant, it grows slower than linearly with n. Therefore, the
amortized cost per append operation (A) remains constant, and the overall time
complexity of n append operations remains O(n).
R-7.9 The add method for a dynamic array, as described in Code Fragment 7.5, has
the following inefficiency. In the case when a resize occurs, the resize operation
takes time to copy all the elements from the old array to a new array, and then the
subsequent loop in the body of add shifts some of them to make room for a new
element. Give an improved implementation of the add method, so that, in the case of
a resize, the elements are copied into their final place in the new array (that is, no
shifting is done).
Answer:
The inefficiency you described with the add method is caused by how the code
handles resizing the underlying array. In the original implementation (Code Fragment
7.5), when the array needs to be resized, all the elements are copied from the old
array to a new array with a larger capacity. Then, the element to be added is inserted
at the desired index, and all the elements after that index are shifted one position to
the right to make room for the new element.
Java
// Shift the elements to the right to make room for the new element
if (i < size) {
System.arraycopy(data, i, data, i + 1, size - i);
}
In this improved version, the resize method is called first to ensure enough space for
the new element. Then, the System.arraycopy method is used to efficiently copy the
elements from their current positions in the old array to their final positions in the new
array, starting from index i and shifting the existing elements to the right. Finally, the
new element is inserted at index i, and the size of the array is incremented.
By using this approach, the add method avoids the extra shifting step, improving the
efficiency of adding elements to a dynamic array when a resize is required.
Answer:
addLast:
Java
addBefore:
Java
// If the position is the first element, use addFirst (which is already defined)
if (p == first()) {
return addFirst(e);
}
// Get a reference to the position before the target position using before(p)
Position<E> before = before(p);
// Use addAfter to insert the new element after the position before the target
return addAfter(before, e);
}
Explanation:
• addLast:
➢ Checks if the list is empty. If so, it calls addFirst (which is assumed to
be already implemented).
➢ Otherwise, it retrieves the last position using last().
➢ It uses addAfter to insert the new element after the last position.
• addBefore:
➢ Checks if the provided position is valid (not null and within the list).
➢ If the position is the first element, it calls addFirst.
➢ Otherwise, it retrieves the position before the target position using
before(p).
➢ It uses addAfter to insert the new element after the position before the
target.
• isValidPosition: (helper method)
➢ Checks if the position is not null, the list is not empty, and the position
is either the first element or the position before it exists (within the list's
boundaries).
This implementation relies on the provided methods like addAfter, first, last, etc., to
achieve the desired functionality for addLast and addBefore. It avoids directly
modifying the existing methods and leverages the existing functionalities within the
provided set.
C-7.25 Give an array-based list implementation, with fixed capacity, treating the
array circularly so that it achieves O(1) time for insertions and removals at index 0,
as well as insertions and removals at the end of the array list. Your implementation
should also provide for a constant-time get method.
Answer:
Here's an array-based list implementation with fixed capacity that achieves constant
time for specific insertion/removal and get operations using a circular approach:
Java
// O(1)
public boolean isEmpty() {
return size == 0;
}
// O(1)
public E get(int i) throws IndexOutOfBoundsException {
checkIndex(i);
int index = (head + i) % data.length; // Wrap around for circular access
return data[index];
}
// O(1) - Amortized constant time (explained later)
public void addFirst(E e) {
checkCapacity();
head = (head - 1 + data.length) % data.length; // Decrement head circularly
data[head] = e;
size++;
}
// O(1)
public E removeFirst() throws IndexOutOfBoundsException {
if (isEmpty()) {
throw new IndexOutOfBoundsException("List is empty");
}
E element = data[head];
head = (head + 1) % data.length; // Increment head circularly
size--;
return element;
}
// O(1)
public E removeLast() throws IndexOutOfBoundsException {
if (isEmpty()) {
throw new IndexOutOfBoundsException("List is empty");
}
int tail = (head + size - 1) % data.length; // Calculate tail index circularly
E element = data[tail];
size--;
return element;
}
• Data Structure: The list uses an array data with a fixed capacity. An additional
variable head keeps track of the index of the first element in the circular array.
• get (O(1)): Accessing an element is constant time as it involves calculating the
actual index within the array using the head and the provided index, considering
the circular nature.
• addFirst & addLast (O(1) amortized): Both operations involve checking for
capacity and then updating the head or tail index circularly to accommodate the
new element. However, there's a caveat. If the list is full and we need to insert at
the beginning (shifting elements), or remove from the end (which might require
shifting elements to fill the gap), the operation becomes O(n) in the worst case.
This is because we might need to shift all elements one position to make space.
However, in an amortized sense, considering a sequence of operations, this
shifting won't happen frequently, resulting in an average constant time complexity
for these operations.
• removeFirst & removeLast (O(1)): Removing the first or last element involves
updating the head or tail index circularly and decrementing the size. This is a
constant time operation.
P-7.61 Write a simple text editor, which stores and displays a string of characters
using the positional list ADT, together with a cursor object that highlights a position in
the string. The editor must support the following operations:
left: Move cursor left one character (do nothing if at beginning). • right: Move
cursor right one character (do nothing if at end).
insert c: Insert the character c just after the cursor.
delete: Delete the character just after the cursor (if not at end).
Answer
Here's a simple text editor implementation using a positional list (simulated using an
array) and a cursor object:
Java
Explanation:
➢ The TextEditor class stores the text as an array of characters (text) and
keeps track of the cursor position (cursor).
➢ getText retrieves the current text up to the cursor position.
➢ left and right move the cursor left or right within the text boundaries.
➢ insert inserts a character c just after the cursor. It checks for available
space and then shifts existing characters to the right to make space for
the new character. Finally, it inserts the character and updates the
cursor position.
➢ delete removes the character just after the cursor (if not at the end). It
shifts the remaining characters to the left, effectively deleting the
character at the cursor's next position.
Note: This is a basic implementation. It assumes a fixed capacity for the text and
doesn't handle special characters or other functionalities a full-fledged text editor
might have.