LMU ☀️ CMSI 2120
DATA STRUCTURES
HOMEWORK #3 PARTIAL ANSWERS
SimpleLinkedList.java
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

/**
 * A simple linked list class, implemented completely from scratch, using doubly
 * linked nodes and a cool "header" node to greatly simplify insertion and
 * deletions on the end of the list.
 */
public class SimpleLinkedList {

    /**
     * Doubly-linked node class, completely private to the List class, as clients
     * don't care about the implementation of the list. This is a regular class and
     * not a record because nodes are mutable.
     */
    private class Node {
        Object item;
        Node next;
        Node previous;

        Node(Object item, Node next, Node previous) {
            this.item = item;
            this.next = next;
            this.previous = previous;
        }
    }

    /**
     * The list itself maintains only a reference to its "header" node. The header
     * is a node that does not store any data. Its 'next' field points to the first
     * item in the list and its 'previous' field points to the last item. This makes
     * all insertions and deletions uniform, even at the beginning and the end of
     * the list!
     */
    private Node header;

    /**
     * The number of items in the list, stored to make size() O(1).
     */
    private int size = 0;

    public SimpleLinkedList() {
        // The empty list is made by pointing the header to itself.
        header = new Node(null, null, null);
        header.next = header.previous = header;
    }

    public int size() {
        // The size stored as a field for easy lookup
        return size;
    }

    public void addFirst(Object item) {
        // The first node is the one right after the header.
        addBefore(item, header.next);
    }

    public void addLast(Object item) {
        // The last node is the one right before the header.
        addBefore(item, header);
    }

    public void add(int index, Object item) {
        // This one is trickier, as there is a special case. If the index is equal
        // to the size, we are just adding the new element at the end. Otherwise
        // we use the special nodeAt() utility to point to the existing node at the
        // desired position. Adding before this node does the trick.
        addBefore(item, (index == size ? header : nodeAt(index)));
    }

    public void remove(int index) {
        // The utility methods do all the work here.
        remove(nodeAt(index));
    }

    public Object get(int index) {
        // Many thanks to the nodeAt() utility!
        return nodeAt(index).item;
    }

    public void set(int index, Object item) {
        // nodeAt() is sure cool, innit?
        nodeAt(index).item = item;
    }

    public int indexOf(Object item) {
        // Ah the old "linear search" pattern: a for-loop with a return in
        // the middle. Note that to iterate the actual nodes, we start at
        // header.next, since header does not hold a list item. The loop
        // terminates when we get back to the header.
        var index = 0;
        for (var node = header.next; node != header; node = node.next) {
            if (node.item.equals(item)) {
                return index;
            }
            index++;
        }
        return -1;
    }

    public void forEach(Consumer<Object> consumer) {
        // Iterate from the first node (the one after the header).
        for (var node = header.next; node != header; node = node.next) {
            consumer.accept(node.item);
        }
    }

    public void take(int n) {
        if (n < 0 || n > size) {
            throw new IllegalArgumentException("Cannot take that many");
        }
        if (n == 0) {
            // Unhook them all
            header.next = header.previous = header;
        } else {
            // Make the nth element the new last one
            var newLastNode = nodeAt(n - 1);
            newLastNode.next = header;
            header.previous = newLastNode;
        }
        size = n;
    }

    public void drop(int n) {
        if (n < 0 || n > size) {
            throw new IllegalArgumentException("Cannot drop that many");
        }
        if (n == size) {
            // Unhook them all
            header.next = header.previous = header;
        } else {
            // Make the nth element the new last one
            var newFirstNode = nodeAt(n);
            newFirstNode.previous = header;
            header.next = newFirstNode;
        }
        size -= n;
    }

    public void reverse() {
        var node = header;
        // Flip the next and previous links in every node including the header.
        // It's hard to get the terminating condition of the loop exactly right
        // so hey just count!
        for (var i = 0; i <= size(); i++) {
            var oldNext = node.next;
            node.next = node.previous;
            node = node.previous = oldNext;
        }
    }

    public void append(SimpleLinkedList other) {
        if (this == other) {
            throw new IllegalArgumentException("Cannot append to self");
        }
        other.forEach(this::addLast);
    }

    public void map(UnaryOperator<Object> f) {
        // Straight iteration through list nodes
        for (var node = header.next; node != header; node = node.next) {
            node.item = f.apply(node.item);
        }
    }

    public void filter(Predicate<Object> p) {
        // This method is destructive so we can make use of the remove utility.
        for (var node = header.next; node != header; node = node.next) {
            if (!p.test(node.item)) {
                remove(node);
            }
        }
    }

    public Object last() {
        if (size == 0) {
            throw new NoSuchElementException("No last element");
        }
        return header.previous.item;
    }

    public boolean every(Predicate<Object> p) {
        for (var node = header.next; node != header; node = node.next) {
            if (!p.test(node.item)) {
                return false;
            }
        }
        return true;
    }

    public boolean some(Predicate<Object> p) {
        for (var node = header.next; node != header; node = node.next) {
            if (p.test(node.item)) {
                return true;
            }
        }
        return false;
    }

    public static SimpleLinkedList of(Object... items) {
        var list = new SimpleLinkedList();
        for (var item : items) {
            list.addLast(item);
        }
        return list;
    }

    /**
     * Get the node at a given index.
     */
    private Node nodeAt(int index) {
        // The only legal indexes are 0, 1, ... size-1.
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException(index + " for size " + size);
        }
        // Only way to get by index in a simple linked list is to walk and
        // count. There are other kinds of lists that give quicker access by
        // position, but those structures are for another time.
        var node = header;
        for (var i = 0; i <= index; i++) {
            node = node.next;
        }
        return node;
    }

    private void addBefore(Object item, Node node) {
        var newNode = new Node(item, node, node.previous);
        newNode.previous.next = newNode;
        newNode.next.previous = newNode;
        size++;
    }

    /**
     * Removes the referenced node by making its neighbors point around it.
     */
    private void remove(Node node) {
        // Just in case
        if (node == header) {
            throw new NoSuchElementException();
        }
        node.previous.next = node.next;
        node.next.previous = node.previous;
        size--;
    }
}

SimpleImmutableList.java
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

/**
 * A simple immutable linked list, implement completely from scratch as a
 * singly-linked structure. There aren't too many operations, as additional
 * will be left for homework.
 * 
 * The implementation uses a sum type because I have major null-phobia.
 * Not going to pay the billion-dollar mistake, not with this one.
 */
public sealed interface SimpleImmutableList permits EmptyList, ListNode {
    int size();

    Object head();

    SimpleImmutableList tail();

    static SimpleImmutableList of(Object... items) {
        SimpleImmutableList list = new EmptyList();
        for (var i = items.length - 1; i >= 0; i--) {
            list = new ListNode(items[i], list);
        }
        return list;
    }

    static SimpleImmutableList from(Object head, SimpleImmutableList tail) {
        return new ListNode(head, tail);
    }

    Object at(int index);

    void forEach(Consumer<Object> consumer);

    SimpleImmutableList take(int n);

    SimpleImmutableList drop(int n);

    SimpleImmutableList reversed();

    SimpleImmutableList append(SimpleImmutableList other);

    SimpleImmutableList map(UnaryOperator<Object> f);

    SimpleImmutableList filter(Predicate<Object> p);

    Object last();

    boolean every(Predicate<Object> p);

    boolean some(Predicate<Object> p);
}

final record EmptyList() implements SimpleImmutableList {
    public int size() {
        return 0;
    }

    public Object head() {
        throw new NoSuchElementException();
    }

    public SimpleImmutableList tail() {
        throw new NoSuchElementException();
    }

    public Object at(int index) {
        throw new NoSuchElementException();
    }

    public void forEach(Consumer<Object> consumer) {
        // Intentionally empty: nothing to iterate
    }

    public SimpleImmutableList take(int n) {
        if (n != 0) {
            throw new IllegalArgumentException("Cannot take that many");
        }
        return this;
    }

    public SimpleImmutableList drop(int n) {
        if (n != 0) {
            throw new IllegalArgumentException("Cannot drop that many");
        }
        return this;
    }

    public SimpleImmutableList reversed() {
        return this;
    }

    public SimpleImmutableList append(SimpleImmutableList other) {
        return other;
    }

    public SimpleImmutableList map(UnaryOperator<Object> f) {
        return this;
    }

    public SimpleImmutableList filter(Predicate<Object> p) {
        return this;
    }

    public Object last() {
        throw new NoSuchElementException();
    }

    public boolean every(Predicate<Object> p) {
        return true;
    }

    public boolean some(Predicate<Object> p) {
        return false;
    }
}

final record ListNode(Object head, SimpleImmutableList tail) implements SimpleImmutableList {
    public int size() {
        return 1 + tail.size();
    }

    public Object at(int index) {
        return index == 0 ? head : tail.at(index - 1);
    }

    public void forEach(Consumer<Object> consumer) {
        consumer.accept(head);
        tail.forEach(consumer);
    }

    public SimpleImmutableList take(int n) {
        if (n < 0) {
            throw new IllegalArgumentException();
        }
        if (n == 0) {
            return new EmptyList();
        }
        return new ListNode(head, tail.take(n - 1));
    }

    public SimpleImmutableList drop(int n) {
        if (n < 0) {
            throw new IllegalArgumentException();
        }
        if (n == 0) {
            return this;
        }
        return tail.drop(n - 1);
    }

    public SimpleImmutableList reversed() {
        return tail.reversed().append(new ListNode(head, new EmptyList()));
    }

    public SimpleImmutableList append(SimpleImmutableList other) {
        return new ListNode(head, tail.append(other));
    }

    public SimpleImmutableList map(UnaryOperator<Object> f) {
        return new ListNode(f.apply(head), tail.map(f));
    }

    public SimpleImmutableList filter(Predicate<Object> p) {
        return p.test(head) ? new ListNode(head, tail.filter(p)) : tail.filter(p);
    }

    public Object last() {
        return tail instanceof EmptyList ? head : tail.last();
    }

    public boolean every(Predicate<Object> p) {
        return p.test(head) && tail.every(p);
    }

    public boolean some(Predicate<Object> p) {
        return p.test(head) || tail.some(p);
    }
}