Lecture from: 12.11.2024 | Video: Videos ETHZ
Linked Lists
Continuation from our intro to Linked Lists from last lecture…
Last time we looked at examples that demonstrate basic linked list manipulation. However, working directly with ListNode
objects can become cumbersome and error-prone, especially for more complex operations. It’s better to encapsulate the internal node structure within a LinkedList
class and provide methods to interact with the list. Before we introduce that class, let’s discuss another essential operation: outputting the elements of a linked list.
Outputting Linked List Elements
A frequent task is to print the elements of a linked list. A naive approach might lead to a common pitfall.
Incorrect Approach (Modifies the List)
This approach has a significant problem: it modifies the original list. After calling this method, the original list will be empty because the head
reference is moved through the list and eventually becomes null
.
Correct Approach (Using a Temporary Variable)
To avoid modifying the original list, we use a temporary variable (current
) for traversal:
This version preserves the original list structure because the head
reference remains unchanged. The current
variable is used for traversal, leaving the original list intact.
Now, before moving on to implementing a full LinkedList
class, let’s have a short intro on inner/nested classes, as these will be useful in our LinkedList
implementation.
The LinkedList
Class: Encapsulation and Inner Classes
As discussed, directly manipulating ListNode
objects is not ideal. We’ll create a LinkedList
class to encapsulate the nodes and provide a cleaner interface. Before that, a brief look at inner classes:
Inner/Nested Classes
Before defining the LinkedList
class, let’s introduce inner/nested classes. These are classes defined inside another class. They’re particularly useful for encapsulation and organization, especially when a helper class (like ListNode
) is only used by the enclosing class (like LinkedList
).
- Access to Outer Class Members: Inner classes have access to all members (fields and methods) of the outer class, even
private
ones. - Visibility: The visibility of the inner class itself can be controlled (e.g.,
private
,public
, etc.). Aprivate
inner class is only accessible within the outer class. OuterClass.this
: To disambiguate between members of the inner and outer class with the same name, useOuterClass.this.memberName
to refer to the outer class’s member.
Example
Important Note: Instantiation of Inner Classes
Inner class instances can only exist within an instance of the outer class. You cannot create an inner class object directly from a static
method (like main
) without first having an instance of the outer class.
LinkedList
Class (Using a Nested Inner Class)
Notice that ListNode
and front
are now private
. Clients cannot directly access or manipulate nodes; they must use the public methods of LinkedList
.
Basic LinkedList
Operations
1. size()
Note that the size can be stored as a separate attribute too to improve performance.
2. get(int index)
3. back()
This version avoids potential NullPointerException
issues.
Example of a (subtly) faulty implementation:
Adding Elements
Now, let’s implement methods to add elements to our LinkedList
.
add(int value, int index)
Inserts a new node with the given value
at the specified index
.
addBack(int value)
(Adding at the End)
Adds a new node with the given value
at the end of the list. This uses our back()
method.
Removing Elements
remove(int value)
Removes the first occurrence of a node with the given value
.
removeAt(int index)
Removes the node at the given index
.
merge(LinkedList other)
Appends the other
list to the end of the current list.
Object Invariants for LinkedList
Invariants are essential for ensuring data structure integrity. For our LinkedList
, key invariants include:
size()
returns the correct number of reachable nodes.back()
returns a reachable node (ornull
if the list is empty).- The list contains no cycles (each node can be reached at most once starting from
front
).
Sorted Linked Lists
A sorted linked list maintains its elements in sorted order (e.g., ascending). This adds an additional object invariant: the values in the list must always be in increasing (or decreasing) order.
SortedLinkedList
Class
add(int value)
for SortedLinkedList
The add()
method for a sorted linked list must ensure that the new element is inserted in the correct position to maintain the sorted order.
Recursive Methods for Linked Lists
Recursive methods can provide elegant and concise solutions when working with recursive data structures like linked lists and trees.
Recursive Methods in ListNode
We can add recursive methods directly to our inner ListNode
class. This allows the outer LinkedList
class to call them, but they remain hidden and inaccessible from external code.
Recursive Methods in BinaryTreeNode
The same principles apply to trees. Here’s a recursive visit()
method for a BinaryTreeNode
:
Java’s Built-in Lists: java.util.LinkedList
and java.util.ArrayList
Java provides implementations of common data structures in the java.util
package.
1. java.util.LinkedList
- Doubly Linked List: Java’s
LinkedList
is a doubly linked list, meaning each node has references to both the next and the previous node. This allows for efficient traversal in both directions. - Methods: Offers methods like
addFirst()
,addLast()
,removeFirst()
,removeLast()
,getFirst()
,getLast()
, etc.
Example Usage:
2. java.util.ArrayList
- Dynamic Array:
ArrayList
uses a dynamic array (a resizable array) internally. It provides fast random access (accessing elements by index) but can be less efficient for insertions or deletions, especially at the beginning of the list.
Example Usage:
Continue here: 16 Java Built-in Lists, LinkedList, ArrayList, Wrapper Classes, Inheritance Basics