Making a sublist
A sublist is a second list built out of part of a first one. Before you write a line, answer one question, because it decides everything else: does the original keep those values?
If the original keeps them, you are copying values, so you build new nodes and the original is untouched at the end. If the original gives them up, you are moving nodes, so you build nothing and you rewrite a small number of arrows. Getting this backward is not a small bug. Copy when you were meant to move and the values are in both lists at once, so a later change to one shows up in the other. Move when you were meant to copy and the original is short values it was supposed to keep.
Moving nodes: the cut
Cutting a list at a position means the front part stays where it is and the back part becomes a new list. No node is created and no node is destroyed. Two arrows change, and 2 counts.
before: head -> [a] -> [b] -> [c] -> [d] -> null size 4
splitAfter("b"):
after: head -> [a] -> [b] -> null size 2
other.head -> [c] -> [d] -> null size 2
Work backward from the after picture. The node holding b used to point at c and now points at null. Something in the new list now points at c. Both sizes changed. That is 4 named changes and 4 lines of code.
The method below finds the cut point by value. splitAfter(Object target) finds the first node whose data equals target. Every node after that one moves into a new list, which the method returns. When no node holds target, or target is in the last node, it returns an empty list. In both of those cases this list keeps its size, and the returned list has size 0.
public LinkedList splitAfter(Object target)
{
LinkedList other = new LinkedList();
Node cur = this.head;
int kept = 1; // nodes this list keeps, cur included
while (cur != null && !cur.data.equals(target))
{
cur = cur.next;
kept++;
}
if (cur == null)
{
return other; // no match, so nothing moves
}
other.head = cur.next; // copy the pointer first
cur.next = null; // then sever
other.size = this.size - kept;
this.size = kept;
return other;
}
Four things in there are worth naming.
Build the list you will return at the very top. Then every path out of the method hands back a real object, and the cases where it comes back empty need no extra thought. A method that returns null to mean nothing moved hands the caller a landmine, because the caller then prints it or asks it for its size and gets a NullPointerException instead of an empty list.
Copy the pointer before you sever. other.head = cur.next has to run before cur.next = null, because after the sever there is nothing left that remembers where the second half was. Aim before you break, the same rule as every other rewiring on this site.
The sever is not optional. Skip it and the front list's last kept node still points at the first node you gave away, so both lists walk through the same nodes. Printing the first list walks straight into the second one and prints values it no longer owns, and the size on the first line disagrees with the lines under it.
A missing target needs its own branch. When no node matches, the walk ends with cur at null. There is no node to sever after, so the method returns the empty list before it reads cur.next. The head of this list never moves, because the node that matches always stays in this list.
A target in the last node needs no branch at all, which is worth walking through because it looks like it should. The walk stops on the last node, whose next is already null. The copy puts null into the new list's head, and the sever writes null over a null. The new size comes out 0, and the old size comes out the size it already was. That is the right answer, produced by the ordinary path.
Copying values: the sublist by criterion
Now the rule is not a position, it is a question you ask each value. Every value over 100. Every name that starts with a letter. Every record whose field matches. Walk the original, ask the question, and when the answer is yes, put a copy of the value into the new list. The original is never touched, so its size never moves.
The only hard part is putting values into the new list in order. Reaching for addFirst is the trap, because it pushes each value on the front, so the new list comes out reversed. Walking the new list to its end for every value is the other trap, because then a list of n values costs a full walk each time.
Keep a second cursor on the end of the list you are building. One attach and one step per value, and nothing is ever re-walked.
public LinkedList over(int limit)
{
LinkedList out = new LinkedList();
Node tail = null; // the last node of the new list
Node cur = this.head;
while (cur != null)
{
if (((Integer) cur.data).intValue() > limit)
{
Node fresh = new Node(cur.data, null);
if (tail == null)
{
out.head = fresh; // the first one that matched
}
else
{
tail.next = fresh;
}
tail = fresh;
out.size++;
}
cur = cur.next;
}
return out;
}
The tail == null branch is the front case again, wearing different clothes: the first matching value has no node in front of it in the new list, so it has to become the head. Give the new list a sentinel and that branch goes away, which is one more place the extra node earns its keep.
Notice what the new nodes hold. new Node(cur.data, null) copies the reference to the value, not the value itself, so both lists point at the same objects. For strings and numbers that never matters, because nothing can change them. For an object with fields somebody can set, it matters a lot, and if the specification says the 2 lists have to be independent you have to copy the object as well as the reference.
Copying a whole list
A copy of a linked list either shares the original's nodes or has nodes of its own.
| Kind of copy | New objects | Shared with the original |
|---|---|---|
| 2 lists that point at the same chain of nodes (a shallow copy) | 1 list object, with its own head and size | every node |
| a list with a node of its own for every value (a deep copy of the chain) | 1 list object, plus 1 new node per value | the data objects the nodes point at |
A shallow copy takes 2 lines:
other.head = this.head;
other.size = this.size;
After those 2 lines, both lists start at the same first node. The table shows what a test sees on the list a b c after each kind of copy, and after the original unlinks its second node.
| Test | Shallow copy | Deep copy |
|---|---|---|
copy.head == original.head | true | false |
copy.head.data == original.head.data | true | true |
the original unlinks b; the original prints | List size is: 2, a c | List size is: 2, a c |
| then the copy prints | List size is: 3, a c | List size is: 3, a b c |
The shallow copy lost b through a change it never made, and its size no longer matches its printout. A deep copy walks the original once and builds 1 new node per value, in the same order, linked only to each other. The data objects stay shared, as in the copying sublist above, because new Node(cur.data, null) copies the reference to the value.
In the practice APE
The cutList problem in Practice exam 2 and the clone problem in Practice exam 1 use this lesson. Each file's header comment is the specification.
Trace the ordinary case of splitAfter. The list holds first second third four five six seven eight nine ten and the call is splitAfter("four").
| Move | Cursor is on | Why |
|---|---|---|
| start | first | the cursor starts at head, and kept is 1 |
| step 1 | second | first does not match, so kept becomes 2 |
| step 2 | third | second does not match, so kept becomes 3 |
| step 3 | four | third does not match, so kept becomes 4 |
| stop | four | four matches, so the walk ends on the last node this list keeps |
| copy | four | the new list's head becomes five |
| sever | four | four's next becomes null, and the lists are separate |
| count | done | the new size is 10 minus 4, and this size is 4 |
What the list you keep prints:
List size is: 4
first
second
third
four
And the list you hand back:
List size is: 6
five
six
seven
eight
nine
ten
Both head and size are private, and the method still reaches into the other list's fields. That is legal, because in Java private means private to the class, not to the object. Your code is inside LinkedList, so it can reach the fields of any LinkedList it is holding, including one it just built. The nested Node class is visible to you for the same reason.
Not knowing this is what sends people to addFirst instead, and addFirst pushes on the front, so copying the tail forward hands it back in reverse. Getting the order right that way costs a second pass. Move the pointers.
Readiness check
A list with no sentinel holds a b c, so its size is 3. Use the splitAfter method above, and start each question from a b c.
splitAfter("a"). What does each list read, and what are the 2 sizes?splitAfter("c"). Same question.splitAfter("z"). Same question.- Someone deletes the
if (cur == null)branch. What happens onsplitAfter("z")?
Answers
1. The original reads a at size 1, and the returned list reads b c at size 2. The head of the original did not move.
2. The original still reads a b c at size 3, and the returned list is empty at size 0. Nothing moved, and no branch was needed to make that happen.
3. No node matches, so the walk ends with cur at null. The branch returns the empty list at size 0, and the original still reads a b c at size 3.
4. The line other.head = cur.next reads through null and throws a NullPointerException.
A list holds a b c d and someone writes splitAfter without the line cur.next = null.
- After
splitAfter("b"), what does the original list print, first line included? - What does the returned list print?
- How many nodes now have 2 lists reaching them?
- Why does nothing throw?
Answers
1. The header says size 2, and then it prints a b c d, because the node holding b still points at c. The count and the values disagree, which is the signature of a missing sever.
2. c d at size 2, which happens to be right.
3. Two, the nodes holding c and d.
4. Because nothing is null that should not be. Every arrow points at a real node, so the walk is perfectly happy. The list is wrong in the way that costs you the most, quietly.
The original holds the integers 5 200 7 300 and you build a sublist of every value over 100.
- Using the
tailversion above, what does the new list read, and what is its size? - What does the original read afterward, and what is its size?
- Someone replaces the wiring with a call to
addFirston the new list. What does the new list read now? - On which pass is the
tail == nullbranch taken?
Answers
1. 200 300, size 2.
2. 5 200 7 300, size 4. Copying values never changes the original.
3. 300 200. Each value is pushed on the front, so the result is reversed.
4. On the pass that matches 200, which is the first match. After that tail is a real node, so every later match attaches to it.
Further reading
- List (Java SE 19 & JDK 19).
subList(int, int)is the library's version of this idea, and its documentation is worth reading closely, because the sublist it returns is a view backed by the original rather than a copy. - LinkedList (Java SE 19 & JDK 19). The class whose fields you are imitating, and the place to check what the library promises about the range it accepts.
- The List Interface (The Java Tutorials). Oracle's own section on range view operations, including the idiom that clears a range through a sublist.
- AbstractList (Java SE 19 & JDK 19). Where the library's sublist view is actually implemented, if you want to see the machinery.
- Filter (higher-order function) (Wikipedia). The general name for selecting the values that satisfy a rule, across languages.
- Linked list (Wikipedia). Splitting and joining chains, and why both cost so little on this structure.
- SLList: A Singly-Linked List, from Pat Morin's Open Data Structures. A full implementation whose walks match the ones on this page.
The one thing to keep
Decide first whether the original keeps its values. Moving nodes means copying the pointer before you sever, severing so the 2 lists cannot see each other, and fixing both sizes. Copying values means building new nodes and leaving the original alone, with a tail cursor so each match is one attach and one step rather than a fresh walk. A missing target is the branch, and a target in the last node needs no branch at all.