Homework 10

Download as pdf or txt
Download as pdf or txt
You are on page 1of 13

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:

8 -> 2 -> 3 -> 6 -> 5 -> 1 -> 7 -> 4

Let's break down the operations one by one,

1. add(0, 4): Adds 4 as the first element. L becomes: 4


2. add(0, 3): Adds 3 as the first element, pushing 4 to the second position. L
becomes: 3 -> 4
3. add(0, 2): Adds 2 as the first element, pushing 3 and 4 further. L becomes: 2 -> 3
-> 4
4. add(2, 1): Inserts 1 at index 2 (between 3 and 4). L becomes: 2 -> 3 -> 1 -> 4
5. add(1, 5): Inserts 5 at index 1 (between 2 and 3). L becomes: 2 -> 5 -> 3 -> 1 ->
4
6. add(1, 6): Inserts 6 at index 1 (between 2 and 5). L becomes: 2 -> 6 -> 5 -> 3 ->
1 -> 4
7. add(3, 7): Since index 3 is out of bounds (referring to the element after 1), we
assume this operation is ignored or throws an error. The list remains unchanged.
8. add(0, 8): Adds 8 as the first element, pushing all other elements one position
further. L becomes: 8 -> 2 -> 6 -> 5 -> 3 -> 1 -> 4

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 class ArrayDeque<E> implements Deque<E> {


private ArrayList<E> elements;
private int front;
private int rear;

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:

• addFirst: Adds an element at the front of the deque.


• addLast: Adds an element at the rear of the deque.
• removeFirst: Removes and returns the first element from the deque.
• removeLast: Removes and returns the last element from the deque.
• getFirst: Returns the first element without removing it.
• getLast: Returns the last element without removing it.
• isEmpty: Checks if the deque is empty.
• size: Returns the current size of the deque.

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:

public void trimToSize() {


if (elements.length > size()) {
elements = Arrays.copyOf(elements, size());
}
}

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.

Here's a breakdown of the code:

• 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:

• Calling trimToSize() frequently might impact performance due to the creation of


new arrays. It's generally recommended to use this method judiciously, especially
when dealing with frequently modified lists.

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:

Here's why performing a sequence of n append operations in this modified ArrayList


implementation still runs in O(n) time:

Analysis:

1. Initial Capacity: Let's assume the initial capacity of the ArrayList is C.


2. Growth Rate: When the capacity is reached (size == capacity), the new capacity
becomes C + ceiling(C / 4). This means the capacity grows by at most ceiling(C /
4).
3. Amortized Analysis: We can use the concept of amortized analysis to show
that the overall cost of n append operations remains O(n). This technique
considers the total cost of all operations and distributes it evenly across them.

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.

Reasoning about Resizing Cost:

• 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.

An improved implementation can address this inefficiency by copying the elements


from the old array to their final positions in the new array during the resize operation.
This eliminates the need for the additional shifting step later.

Here's the improved implementation of the add method:

Java

public void add(int i, E e) throws IndexOutOfBoundsException {


/** Inserts element e to be at index i, shifting all subsequent elements later.

if (size == data.length) { // not enough capacity


checkIndex(i, size + 1);
resize(2 * data.length); // double the current capacity
}

// 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);
}

// Insert the new element at the specified index


data[i] = e;
size++;
}

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.

R-7.11 Describe an implementation of the positional list methods addLast and


addBe- fore realized by using only methods in the set {isEmpty, first, last, before,
after, addAfter, addFirst}.

Answer:

Here's an implementation of addLast and addBefore using only the provided


methods:

addLast:

Java

public Position<E> addLast(E e) {


// If the list is empty, use addFirst (which is already defined)
if (isEmpty()) {
return addFirst(e);
}

// Get a reference to the last position using last()


Position<E> last = last();
// Use addAfter to insert the new element after the last position
return addAfter(last, e);
}

addBefore:

Java

public Position<E> addBefore(Position<E> p, E e) throws IllegalArgumentException {


// Check if the provided position is valid
if (p == null || !isValidPosition(p)) {
throw new IllegalArgumentException("Invalid position");
}

// 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);
}

// Helper method to check if a position is valid (within the list's boundaries)


private boolean isValidPosition(Position<E> p) {
return p != null && !isEmpty() && (p == first() || before(p) != null);
}

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

public class CircularArrayList<E> {

private E[] data;


private int size;
private int head; // Index of the first element in the circular array

public CircularArrayList(int capacity) {


this.data = (E[]) new Object[capacity];
this.size = 0;
this.head = 0;
}

// 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) - Amortized constant time (explained later)


public void addLast(E e) {
checkCapacity();
int tail = (head + size) % data.length; // Calculate tail index circularly
data[tail] = 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;
}

private void checkCapacity() {


if (size == data.length) {
throw new IllegalStateException("List is full");
}
}

private void checkIndex(int i) {


if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException("Invalid index");
}
}
}
Explanation:

• 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.

Note: While addFirst, addLast, removeFirst, and removeLast have an amortized


constant time complexity, the worst-case scenario for these operations can be O(n)
due to potential element shifting in a full list.

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

public class TextEditor {

private char[] text;


private int cursor; // Index of the cursor position
public TextEditor(int capacity) {
this.text = new char[capacity];
this.cursor = 0;
}

public String getText() {


return new String(text, 0, cursor); // Get text up to cursor position
}

public void left() {


if (cursor > 0) {
cursor--;
}
}

public void right() {


if (cursor < text.length) {
cursor++;
}
}

public void insert(char c) {


// Check if space available
if (cursor == text.length) {
throw new IllegalStateException("Text editor full");
}

// Shift elements to the right from cursor position


for (int i = text.length - 1; i > cursor; i--) {
text[i] = text[i - 1];
}

// Insert character at cursor position


text[cursor] = c;
cursor++;
}

public void delete() {


if (cursor < text.length - 1) {
// Shift elements to the left from cursor position + 1
for (int i = cursor; i < text.length - 1; i++) {
text[i] = text[i + 1];
}
}
}
}

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.

You might also like