CIS 3130
Data Structures
Lecture 08
Linked Implementations

Motivation

However, lists provide insertions/deletions inside the sequence, seemingly requiring shifts. This can be avoided by switching from the contiguous storage scheme of the array to a linked representation.

Implicit vs Explicit Addressing

Arrays use implicit addressing: This is in contrast with explicit addressing … stored with each element of the sequence is the location of the next element.

Nodes: The Atoms of Linked Implementations

In a linked implementation, the elements are maintained in a sequence where the order is maintained by explicit addresses, usually called links or pointers. Each element is stored in a discrete entity known as a node or cell containing:

Nodes in Java

class Node<E> {
	Node(E data) {this(data, null);}
	Node(E data, Node next) {
		this.data = data;
		this.next = next;
	}
	E data;
	Node next;
}
There are a couple of operations that can be coded using nothing more than the notion of a node (i.e., before moving on to lists of nodes).

Operations Involving a Specified Node

insertAfter

Insert an element after a particular node (pointed to by the node parameter)

void insertAfter(Node node, E data) {
	if (node == null) throw new Exception("...");
	Node newNode = new Node(data, node.next);
	node.next = newNode;
}
removeAfter

Remove the element after a particular node (pointed to by node) in a list

E removeAfter(Node node) {
	if (node == null) throw new Exception("null sent as argument to removeAfter");
	if (node.next == null) throw new Exception("Attempt to remove node after last node of list");
	Node hold = node.next;
	node.next = hold.next;
	return hold.data;
}
Notes

Lists of Nodes — Linked Lists

Advantages of Linked Lists

Disadvantages of Linked Lists

  • Linked list wikipedia entry

    Linked Lists in Java

    class LinkedList {
    	Node head = null;
    }
    

    Some Tips on Working with Linked Lists

    A Progression of Linked Lists

    Singly-linked Linear (SL1NSingly-linked / Linear / 1 pointer / No header

    Provides direct access to first node on list

    Notes
    public class SL1N_Deque<E> implements Deque<E>{
    	static class Node<E> {
    		public Node(E data, Node<E> next) {
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data) {this(data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> next;
    	}
    
    	public void addFirst(E data) {
    		var node = new Node<>(data, head);
    		head = node;
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = head;
    		head = head.next;
    		return node.data;
    	}
    
    	public void addLast(E data) {
    		if (isEmpty()) 
    			addFirst(data);
    		else {
    			Node<E> p = head;
    			while (p.next != null) {
    				p = p.next;
    			}
    			p.next = new Node<E>(data);
    		}
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		Node<E> p = head;
    		while (p != null) {
    			count++;
    			p = p.next;
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return head == null;}
    
    	public String toString() {
    		String result = "{";
    		var p = head;
    		while (p != null) {
    			result += p.data + (p.next != null ? ", " : "");
    			p = p.next;
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> head = null;
    }
    
    Notes
    initial state
    Efficient operations
    Requires traversal
    'Random access' of nodes
    Good for implementing
    One could either use composition and leverage the addFirst and removeFirst methods (renaming them push and pop, or alternatively code the Stack directly (i.e., independently).
    Stack implementation Using a Singly Linked Linear List
    push
    Inserts an element at the front of the linked list
    void push(int data) {		// same as an addFront method
    	Node  node = new Node(data, head); 
    	head = node;
    }
    
    More succinctly:
    void push(Node *&head, const EType &data) {
    	head = new Node(data, head);
    }
    
    pop
    Remove and return the element from the front of the linked list
    int pop() {									// same as a removeFront method
    	if (isEmpty()) throw new Exception("....");
    	Node hold = head;
    	head = head.next;
    	return hold.data;
    }
    

    Other Comments

    Header (Dummy) Nodes

    For 'Meta' Data (Historical)
    There is a technique to add an additional 'meta'-node to the beginning of the list that contains meta-information about the list; e.g. number of nodes (to avoid traversal when implementing a 'length' method), or maximum value in the list, etc.
    To Eliminate Special Logic (Current)
    Having a header node always present on the list avoids the special logic of updating 'head' during the empty → non-empty and non-empty → empty transitions.

    Singly-linked Linear With Header Node (SL1HSingly-linked / Linear / 1 pointer / Header

    Simplifies insertion/removal logic, by eliminating the special cases of empty → non-empty and non-empty → empty transitions.

    public class SL1H_Deque<E> implements Deque<E>{
    	static class Node<E> {
    		public Node(E data, Node<E> next) {
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data) {this(data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> next;
    	}
    
    	SL1H_Deque() {
    		header = new Node<>();
    	}
    
    	public void addFirst(E data) {
    		var node = new Node<>(data, header.next);
    		header.next = node;
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = header.next;
    		header.next = node.next;
    		return node.data;
    	}
    
    	p ublic void addLast(E data) {
    		Node<E> p = header;
    		while (p.next != null) {
    			p = p.next;
    		}
    		p.next = new Node<E>(data);
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		Node<E> p = header.next<.span>;<.span>
    		while (p != null) {
    			count++;
    			p = p.next;
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return header.next == null;}
    
    	public String toString() {
    		String result = "{";
    		var p = header.next;
    		while (p != null) {
    			result += p.data + (p.next != null ? ", " : "");
    			p = p.next;
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> header = null;
    }
    
    Notes
    initial state
    Efficient operations
    Requires traversal
    Good for implementing

    Singly-linked Linear With Head and Tail Pointers (SL2NSingly-linked / Linear / 2 pointers / No header

    Provides direct access to first and last node on list

    mublic class SL2N_Deque<E> implements Deque<E>{
    	static class Node<E> {
    		public Node(E data, Node<E> next) {
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data) {this(data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> next;
    	}
    
    	public void addFirst(E data) {
    		var node = new Node<>(data, head);
    		if (head == null) tail = node;
    		head = node;
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = head;
    		head = head.next;
    		if (head == null) tail = null;
    		return node.data;
    	}
    
    	public void addLast(E data) {
    		if (isEmpty()) 
    			addFirst(data);
    		else {
    			Node<E> node = new Node<E>(data);
    			tail.next = node;
    			tail = node; 
    		}
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		Node<E> p = head, q = null;
    		while (p != null) {
    			count++;
    			q = p;
    			p = p.next;
    		}
    		if (q != tail) {
    			System.out.println("*** Deque integrity error: tail is not pointing at last node");
    			System.exit(1);
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return head == null && tail == null;}
    
    	public String toString() {
    		String result = "{";
    		Node<E> p = head, q = null;
    		while (p != null) {
    			result += p.data + (p.next != null ? ", " : "");
    			q = p;
    			p = p.next;
    		}
    		if (q != tail) {
    			System.out.println("*** Deque integrity error: tail is not pointing at last node");
    			System.exit(1);
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> 
    		head = null, 
    		tail = null;
    }
    
    Notes
    initial state
    Efficient operations
    Requires traversal
    As discussed above, this implementation requires special-case logic in order to synch the two pointers wrt empty → non-empty and non-empty → empty transitions
    Good for implementing
    FIFO Queue Implementation Using Singly Linked Linear List with Head and Tail Pointers
    Again, we could leverage this implementation to create a FIFO queue class, or directly implement the methods (as follows)
    add (to rear)
    void add(E data) {
    	Node node = new Node(data);
    	if (tail != null)
    		tail.next = node;
    	else
    		head = node;
    	tail =  node;	
    }
    
    remove (from front)
    E remove() {
    	if (isEmpty()) throw new Exception("…");
    	E data = head.data;
    	head = head.next;
    	if (head == null) tail = null;
    	return data;;
    }
    

    Singly-linked Circular with Single Pointer to 'Last' Node on List (SC1NSingly-linked / Circular / 1 pointer / No header

    Provides direct access to last and first node on list using a single tail pointer

    public class SC1N_Deque<E> implements Deque<E>{
    	static class Node<E> {
    		public Node(E data, Node<E> next) {
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data) {this(data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> next;
    	}
    
    	public void addFirst(E data) {
    		var node = new Node<>(data);
    		if (tail == null) {
    			node.next = node;
    			tail = node;
    		}
    		else {
    			node.next = tail.next;
    			tail.next = node;
    		}
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = tail.next;
    		if (tail.next == tail) 
    			tail = null;
    		else
    			tail.next = node.next;
    		return node.data;
    	}
    
    	public void addLast(E data) {
    		addFirst(data);
    		tail = tail.next;
    		
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		if (tail != null) {
    			Node<E> p = tail.next;
    			do {
    				count++;
    				p = p.next;
    			} while (p != tail.next);
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return tail == null;}
    
    	public String toString() {
    		String result = "{";
    		if (tail != null) {
    			var p = tail.next;
    			do {
    				result += p.data + (p.next != tail.next ? ", " : "");
    				p = p.next;
    			} while (p != tail.next);
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> tail = null;
    }
    
    Notes
    initial state
    Efficient operations
    Requires traversal
    Good for implementing

    Doubly-linked Nodes in Java

    class Node<E> {
    	Node(E data, Node prev, Node next) {
    		this.data = data;
    		this.prev = prev;
    		this.next = next;
    	}
    	Node(E data) {this(data, null, null);}
    	E data;
    	Node next, prev; 	// often left, right
    }
    

    Doubly-linked Linear (DL1NDoubly-linked / Linear / 1 pointer / No header

    Provides direct access to first node on list; allows two way motion

    public class DL1N_Deque<E> implements Deque<E> {
    	static class Node<E> {
    		public Node(Node<E> prev, E data, Node<E> next) {
    			this.prev = prev;
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data, Node<E> next) {this(null, data, next);}
    		Node(Node<E> prev, E data) {this(prev, data, null);}
    		Node(E data) {this(null, data, null);}
    		Node( ) {this(null);}
    
    		E data;
    		Node<E> prev, next;
    	}
    
    	public void addFirst(E data) {
    		Node<E> node = new Node<>(data, head);
    		if (head != null) 
    			head.prev = node;
    		head = node;
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = head;
    		if (node.next != null) node.next.prev = null;
    		head = node.next;
    		return node.data;
    	}
    
    	public void addLast(E data) {
    		if (isEmpty()) 
    			addFirst(data);
    		else {
    			Node<E> p = head;
    			while (p.next != null)
    				p = p.next;
    			Node<E> node = new Node<E>(p, data);
    			p.next = node;
    		}
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		Node<E> p = head;
    		while (p != null) {
    			count++;
    			p = p.next;
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return head == null;}
    
    	public String toString() {
    		String result = "{";
    		Node<E> p = head;
    		while (p != null) {
    			result += p.data + (p.next != null ? ", " : "");
    			p = p.next;
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> head = null;
    }
    
    Notes
    initial state
    Efficient operations
    Requires traversal
    Good for implementing
    Requires special-case logic for processing nodes at end of list; i.e., nodes with null left or right (or both) fields

    A List Class Using Doubly-linked Implementation

    At this point, however, we do have a reasonable implementation of a List container.
    class List<E> {
    	static class Node<E> {
    		public Node(Node<E> prev, E data, Node<E> next) {
    			this.prev = prev;
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data, Node<E> next) {this(null, data, next);}
    		Node(Node<E> prev, E data) {this(prev, data, null);}
    		Node(E data) {this(null, data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> prev, next;
    	}
    	E get(int pos) {return getNode(pos).data;}
    
    	void set(int pos, E val) {getNode(pos).data = val;} 
    
    	void add(int pos, E val) {
    		Node<E> atPrev, atNext;
    		if (pos == 0) {
    			atPrev = null;
    			atNext = head;
    		}
    		else {
    			atPrev = getNode(pos-1);
    			atNext = atPrev.next;
    		}
    		Node<E> node = new Node<E>(atPrev, val, atNext);
    		if (atNext != null) atNext.prev = node;
    		if (atPrev != null) atPrev.next = node;
    		if (pos == 0) head = node;
    		size++;
    	}
    
    	E remove(int pos) {
    		Node<E> node = getNode(pos);
    		E data = node.data;
    		Node<E> atNext = node.next;
    		Node<E> atPrev = node.prev;
    		if (atPrev != null) atPrev.next = atNext;
    		if (atNext != null) atNext.prev = atPrev;
    		size--;
    		return data;
    	}
    
    	int size() {return size;}
    	
    	boolean isEmpty() {return size == 0;}
    
    	private Node<E> getNode(int pos) {
    		int count = 0;
    		Node<E> p = head;
    		while (p != null && count < pos) {
    			p = p.next();
    			count++;	 
    		}
    		if (p == null) throw new ListException("illegal position " + pos);
    		return p;
    	}
    
    	public String toString() {
    		Node<E> p = head;
    		String result = "";
    		result += "[";
    		while (p != null) {
    			result += p.data + (p.next != null ? ", " : "");
    			p = p.next;
    		}
    		result += "]";
    		return result;
    	}
    
    	Node<E> head = null;
    	int size = 0;
    }
    
    Notes

    Doubly-linked Linear With Head and Tail Pointers (DL2NDoubly-linked / Linear / 2 pointer / No header

    public class DL2N_Deque<E> implements Deque<E> {
    	static class Node<E> {
    		public Node(Node<E> prev, E data, Node<E> next) {
    			this.prev = prev;
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data, Node<E> next) {this(null, data, next);}
    		Node(Node<E> prev, E data) {this(prev, data, null);}
    		Node(E data) {this(null, data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> prev, next;
    	}
    
    	public void addFirst(E data) {
    		Node<E> node = new Node<>(data, head);
    		if (isEmpty()) 
    			tail = node;
    		else	
    			head.prev = node;
    		head = node;
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = head;
    		if (node.next != null) 
    			node.next.prev = null;
    		head = node.next;;
    		return node.data;
    	}
    
    	public void addLast(E data) {
    		if (isEmpty()) 
    			addFirst(data);
    		else {
    			Node<E> node = new Node<>(tail, data);
    			tail.next = node;
    			tail = node;
    		}
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		Node<E> p = head;
    		while (p != null) {
    			count++;
    			p = p.next;
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return head == null;}
    
    	public String toString() {
    		String result = "{";
    		Node<E> p = head;
    		while (p != null) {
    			result += p.data + (p.next != null ? ", " : "");
    			p = p.next;
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> head = null, tail = null;
    }
    
    Notes

    Doubly-linked Circular (DC1NDoubly-linked / Circular / 1 pointer / No header

    Provides direct access to 'last' and 'first' node on list; eliminates null next/prev fields at end

    public class DC1N_Deque<E> implements Deque<E> {
    	static class Node<E> {
    		public Node(Node<E> prev, E data, Node<E> next) {
    			this.prev = prev;
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data) {this(null, data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> prev, next;
    	}
    
    	public void addFirst(E data) {
    		Node<E> node = new Node<>(data);
    		if (head == null) {
    			node.next = node.prev = node;
    		}
    		else {
    			node.prev = head.prev;
    			head.prev.next = node;
    			node.next = head;
    			head.prev = node;
    		}
    		head = node;
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = head;
    		if (head.next == head) 
    			head = null;
    		else {
    			node.next.prev = node.prev;
    			node.prev.next = node.next;;
    			head = node.next;
    		}
    		return node.data;
    	}
    
    	public void addLast(E data) {
    		if (head == null) 
    			addFirst(data);
    		else {
    			Node<E> node = new Node<>(head.prev, data, head);
    			node.prev = head.prev;
    			head.prev.next = node;
    			node.next = head;
    			head.prev = node;
    		}
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		if (head != null) {
    			Node<E> p = head;
    			do {
    				count++;
    				p = p.next;
    			} while (p != head);
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return head == null;}
    
    	public String toString() {
    		String result = "{";
    		if (head != null) {
    			Node<E> p = head;
    			do {
    				result += p.data + (p.next != head ? ", " : "");
    				p = p.next;
    			} while (p != head);
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> head = null;
    }
    
    Notes
    initial state
    Efficient operations
    Good for implementing
    Requires special-case logic for updating 'head' during empty → non-empty and non-empty → empty operations

    Doubly-linked Linear Circular With Header Node (DC1HDoubly-linked / Circular / 1 pointer / Header

    Sounds the most complicated, but in fact, eliminates all special logic.

    public class DC1H_Deque<E> implements Deque<E> {
    	static class Node<E> {
    		public Node(Node<E> prev, E data, Node<E> next) {
    			this.prev = prev;
    			this.data = data;
    			this.next = next;
    		}
    		Node(E data) {this(null, data, null);}
    		Node() {this(null);}
    
    		E data;
    		Node<E> prev, next;
    	}
    
    	DC1H_Deque() {
    		 header = new Node<>();
    		 header.next = header.prev = header;
    	}
    
    	public void addFirst(E data) {
    		Node<E> node = new Node<>(header, data, header.next);
    		header.next.prev = node;
    		header.next = node;
    	}
    
    	public E removeFirst() {
    		if (isEmpty()) throw new CollectionException("Deque", "removeFirst", "empty container"); 
    		var node = header.next;
    		node.next.prev = header;
    		header.next = node.next;;
    		return node.data;
    	}
    
    	public void addLast(E data) {
    		Node<E> node = new Node<>(header.prev, data, header);
    		header.prev.next = node;
    		header.prev = node;
    	}
    
    	public E removeLast();			// you are to implement this method
    		
    	public int size() {
    		int count = 0;
    		Node<E> p = header.next;
    		while (p != header) {
    			count++;
    			p = p.next;
    		}
    		return count;
    	}
    
    	public boolean isEmpty() {return header.next == header && header == header.prev;}
    
    	public String toString() {
    		String result = "{";
    		Node<E> p = header.next;
    		while (p != header) {
    			result += p.data + (p.next != header ? ", " : "");
    			p = p.next;
    		}
    		result += "}";
    		return result;
    	}
    
    	public int capacity() {return -1;}
    		
    	private Node<E> header;
    }
    
    Notes
    initial state
    Efficient operations
    Good for implementing

    Basic Insertion/Removal Operations for Lists of Nodes

    As we've seen above, different linked list implementations support different sets of efficient insertion/removal methods: insertFront, removeFront, insertLast, removeLast, insertAfter, removeAfter.

    If we are willing to traverse the list, we can also consider:

    Note that this version of remove really belongs to the associative world, as it it value-based. We are presenting it here to introduce a standard technique in linked list processing.

    remove

  • remove: remove the first element equal to some specified value (content-based remove)
    boolean remove(E data) {
    	Node p = head, q = null; 
    
    	while (p != null && data != p.data) {
    		q = p;		// this is the piggyback action
    		p = p.next;
    	}
    	if (p == null) 
    		return false;
    	else if (q == null)
    		removeFront();
    	else
    		removeAfter(q);
    }
    			

    Traversive Operations on Lists of Nodes

    These operations work on the list as a whole

    Iterative Versions of the Traversive Operations

    Recursion and Lists of Nodes

    Recursive Versions of the Traversive Operations

    Recursion in the Presence of Classes