القوائم المترابطة (Linked lists)
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Arrays keep items in one block, so reaching item i is instant but inserting in the middle means shifting. A linked list makes the opposite trade. Its items are scattered, each one pointing to the next, so rewiring a few references inserts or removes without shifting anything, but finding item i means walking there.
The picture is a treasure hunt. You are given the first clue (the head). Each clue holds a prize and tells you where the next clue is. The last clue says "no more". To get to clue 7, you must follow clues 1 to 6.
By the end you will build a small singly linked list, with addFirst, addLast, contains, and remove, and you will know the cost of each operation.
Why this matters: linked structures are the idea behind many other data structures, and reading and rewiring references is exactly the skill that trees and graphs need later. Linked lists are also a classic interview topic.
This lesson builds on generic classes (BEG-031), references and aliasing (BEG-038), and Lessons 1 and 2 of this module.
Learning Objectives
By the end of this lesson you will be able to:
- Describe a singly linked list as nodes holding a value and a
nextreference. - Explain the roles of
head,tail, and the finalnext == null. - Implement
addFirst,addLast,contains, andremoveon a generic list. - Rewire references to remove a node without losing the head or the tail.
- State the cost of each operation, with and without a tail reference.
- Explain why there is no O(1) access by index.
Written Explanation
Nodes
static class Node<T> {
T value;
Node<T> next;
Node(T value) { this.value = value; }
}A node holds one value and a reference to the next node. Node<T> refers to Node<T>: the class mentions itself, which is what makes a chain possible. The list keeps references to the ends of the chain:
head tail
| |
[A|*]-->[B|*]-->[C|*]-->[D|null]headis the first node. Without it you lose the whole list, because nothing else points at the first node.tailis the last node. Keeping it is optional but makes adding at the end fast.- The last node's
nextisnull. That is how a walk knows to stop. - An empty list has
head == null.
Nodes are separate objects, so they can sit anywhere on the heap. You cannot compute where node 5 is. You must follow references.
The classic walk
Every operation that looks at the list follows the same pattern:
for (Node<T> cur = head; cur != null; cur = cur.next) {
// look at cur.value
}Start at head, stop when cur becomes null, step with cur = cur.next. The cost is O(n) because in the worst case it visits every node.
addFirst: O(1)
To put a new node at the front, point it at the old head, then make it the new head. The order matters:
void addFirst(T v) {
Node<T> n = new Node<>(v);
n.next = head; // 1. new node points at the old first node
head = n; // 2. the list now starts at the new node
if (tail == null) tail = n; // first item: also the last
size++;
}If you did step 2 first, you would lose the old chain. Nothing is shifted and nothing is copied, so this is O(1).
addLast: O(1) with a tail, O(n) without
With a tail reference you attach to the end directly:
void addLast(T v) {
Node<T> n = new Node<>(v);
if (tail == null) { head = n; tail = n; }
else { tail.next = n; tail = n; }
size++;
}Without tail you would walk from head to the last node first: O(n). So we keep both ends. Keeping tail correct is a responsibility, which leads to the main mistake of this module.
contains: O(n)
Walk the list and compare each value with equals (not ==, which compares references, as BEG-026 taught). If you reach null, the value is not there. The cost is O(n), the same as scanning an array. There is no way to skip ahead, because there is no index. Likewise get(i) walks i steps, so it is O(n).
remove: find the previous node
To remove a node, make the node before it point past it. That means you need the previous node, so the walk carries two references, prev and cur:
boolean remove(T v) {
Node<T> prev = null;
Node<T> cur = head;
while (cur != null && !cur.value.equals(v)) {
prev = cur;
cur = cur.next;
}
if (cur == null) return false; // not found
if (prev == null) head = cur.next; // removing the head
else prev.next = cur.next; // rewire around cur
if (cur == tail) tail = prev; // do not lose the tail
size--;
return true;
}Three cases, and each needs care:
- Not found:
curreachednull; change nothing. - Removing the head (
prev == null): the head moves to the next node. - Removing any other node:
prev.next = cur.nextskips it. Ifcurwas the tail, the tail becomesprev. Forgetting this leavestailpointing at a node that is no longer in the list, and the nextaddLastquietly adds to a detached node. This is "losing the list's tail on removal".
Finding the node is O(n); the rewiring itself is O(1). So remove(value) is O(n). If you already hold the previous node, the removal is O(1). That is the real strength of linked lists: cheap changes at a known position.
The skipped node still points at the next one, but nothing in the list points at it, so the garbage collector can reclaim it (as in the memory module, BEG-038).
Costs
| Operation (singly linked, n items) | Cost |
|---|---|
addFirst | O(1) |
addLast with tail | O(1) |
addLast without tail | O(n) |
get(i), contains, remove(value) | O(n) |
| Remove the first node | O(1) |
| Remove the last node | O(n): you must reach the node before the tail |
Last one surprises people. In a singly linked list, nodes know only their next node. To remove the tail you must find its predecessor, so you walk from the head. A doubly linked list stores both next and prev, which fixes that, at the cost of one more reference per node. The JDK's LinkedList is a doubly linked list, according to its documentation.
Arrays versus linked nodes
| Array-based | Linked | |
|---|---|---|
| Get by index | O(1) | O(n) |
| Insert/remove at a known position | O(n) shifting | O(1) rewiring |
| Memory | Items packed together | Node object plus one reference per item |
| Growth | Resize and copy | Add one node |
Lesson 6 turns this into a rule for choosing.
Examples
Example 1: building a list and looking inside (simple)
MyLinkedList<String> list = new MyLinkedList<>();
list.addLast("B");
list.addLast("C");
list.addFirst("A");
System.out.println(list);
System.out.println(list.contains("C") + " " + list.contains("Z"));toString walks the list and prints the chain. Real output:
A -> B -> C -> null
true falseThe calls ran in the order B, C, then A at the front, yet the list reads A, B, C. Order depends on where you add, not on when.
Example 2: removing middle, tail, and head (practical)
list.remove("B");
System.out.println(list); // A -> C -> null
list.remove("C"); // C was the tail
list.addLast("D"); // must work: tail was updated
System.out.println(list);
System.out.println(list.remove("A") + " " + list);
System.out.println(list.remove("Q"));Real output:
A -> C -> null
A -> D -> null
true D -> null
falseRemoving C changed the tail. Because remove set tail = prev, the later addLast("D") attached to A, and the list printed A -> D. If the tail line were missing, D would have been attached to the removed C and would never appear in the printed list. Removing A (the head) moved head to D, and Q was not found so remove returned false.
Example 3: a browsing-history list on the heap (realistic)
A page records the last visited URLs, always newest first, and drops a URL if the user clears it. Newest-first means addFirst, which is O(1) no matter how long the history grows:
history.addFirst("/orders");
history.addFirst("/orders/42");
history.remove("/orders"); // O(n) search, O(1) rewiringThe history is short, so O(n) removal is no problem. The point of the example is the choice: if the access pattern is "add at the front often, rarely search", a linked list fits; if it is "get the 500th item", an array does. A real application would more likely use ArrayDeque, as Lesson 6 explains.
Backend Insight
You will rarely hand-write a linked list in a backend project, because the JDK provides LinkedList and ArrayDeque. The skills you practiced here are used constantly:
- Following and rewiring references: the same moves appear in trees, graphs, caches such as an LRU cache, and object graphs in your domain model.
- Cost reasoning: knowing that
get(i)on a linked list is a walk helps you spot slow loops such asfor (i...) list.get(i)over aLinkedList, which becomes O(n²). - Memory: every node is a separate object plus a reference, so a linked list uses more memory per item than an array of the same size. The exact overhead depends on the JVM, so measure if it matters.
Common Mistakes
Mistake 1: Losing the tail (or head) when removing
- Incorrect: removing the last node and leaving
tailpointing at it. - Correct: after removing, if the removed node was the tail, set
tail = prev. If it was the head, sethead = cur.next. If the list becomes empty, both end upnull(the code above gives this:head = null, thentail = prevwhich isnull).
Mistake 2: Rewiring in the wrong order
In addFirst, assign n.next = head before head = n. Doing it the other way round loses the old chain.
Mistake 3: Comparing values with ==
cur.value == v compares references. Use equals, and think about null values: cur.value.equals(v) throws a NullPointerException if a stored value is null. This lesson's version assumes no null items.
Mistake 4: Assuming get(i) is fast
A linked list has no index arithmetic. get(i) walks i nodes.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. A list is X -> Y -> Z -> null. What are head, tail, and the next of Z?
أظهر الحل A1
A1. head is the node holding X; tail is the node holding Z; the next of Z is null.
A2. Why does addFirst cost O(1) but get(i) cost O(n)?
أظهر الحل A2
A2. addFirst rewires two references and needs no walk. get(i) must follow i links from the head, because nothing can compute where node i is.
A3. Draw A -> B -> C -> D -> null before and after removing D. Which two fields must change?
أظهر الحل A3
A3. Before: A -> B -> C -> D -> null. After: A -> B -> C -> null. C.next becomes null and the list's tail becomes C. To find C you must walk from the head.
Level B: Application
B1. Write static <T> int length(Node<T> head) that counts the nodes. What is its cost?
أظهر الحل B1
B1.
static <T> int length(Node<T> head) {
int count = 0;
for (Node<T> cur = head; cur != null; cur = cur.next) count++;
return count;
}O(n). (Keeping a size field makes size() O(1).)
B2. Write static <T> T get(Node<T> head, int index) that returns the value at index and throws IndexOutOfBoundsException if there is no such node.
أظهر الحل B2
B2.
static <T> T get(Node<T> head, int index) {
Node<T> cur = head;
for (int i = 0; i < index && cur != null; i++) cur = cur.next;
if (cur == null) throw new IndexOutOfBoundsException("index " + index);
return cur.value;
}For A -> B -> C, get(head, 2) returns C, and get(head, 3) throws (real run).
B3. For a list A -> B -> C written with plain nodes a, b, c, write the one line that removes B from the list (given a), and say what the node b still points to afterwards.
أظهر الحل B3
B3. a.next = b.next; Afterwards b still points to c, but nothing in the list points to b, so the list is A -> C.
Level C: Challenge
C1. Write static <T> Node<T> reverse(Node<T> head) that reverses the list in place and returns the new head. Why must you save cur.next before changing it?
أظهر الحل C1
C1.
static <T> Node<T> reverse(Node<T> head) {
Node<T> prev = null;
Node<T> cur = head;
while (cur != null) {
Node<T> next = cur.next; // save the rest of the chain
cur.next = prev; // point backwards
prev = cur;
cur = next;
}
return prev;
}Without saving next, the assignment cur.next = prev would cut off the rest of the list. Reversing A -> B -> C printed C -> B -> A -> null (real run). It is O(n).
C2. A list has a tail reference. Which of these are O(1): addFirst, addLast, remove the first node, remove the last node? Which would become O(1) in a doubly linked list?
أظهر الحل C2
C2. O(1): addFirst, addLast, remove the first node. Remove the last node is O(n) because you must find the node before the tail. In a doubly linked list the tail knows its prev, so removing the last node becomes O(1).
Mini Challenge
Extend MyLinkedList<T> with int size() (already tracked by the field), T getFirst(), T removeFirst() (throw NoSuchElementException when empty), and boolean isEmpty(). Write a main that:
- Adds three items, removes the first twice, and prints the list.
- Removes the last remaining item, then adds a new item with
addLast. - Prints the list again.
Check your work: after removing the only node, both head and tail must be null, and the later addLast must produce a list with one item.
Summary & Next Lesson
Key takeaways
- A singly linked list is nodes with a value and
next;headstarts the chain and the lastnextisnull. addFirstand (withtail)addLastare O(1);get(i),contains, andremove(value)are O(n).- To remove a node, point the previous node's
nextpast it, and updateheadortailwhen they were the removed node. - In a singly linked list, removing the last node is O(n) because you need its predecessor.
- Linked lists give cheap changes at a known position but no index arithmetic.
Next lesson
Next is "Stacks". A stack restricts where items are added and removed (only the top), and that restriction is what makes it useful and fast.
Self-check
- What does a node contain?
- Why must
addFirstsetn.next = headbeforehead = n? - What is the cost of
get(i)on a linked list, and why? - What must you update when the removed node was the tail?
- Why is removing the last node O(n) in a singly linked list?
أظهر الإجابات
- A value and a reference to the next node.
- Otherwise the old chain is lost because
headwould no longer point at it. - O(n): you walk
inodes from the head. tailmust become the previous node (ornullif the list became empty).- You need the node before the tail, and only walking from the head can find it.
References
- Oracle, Java SE 21 API, java.util.LinkedList. Supports:
LinkedListis a doubly linked list, indexing operations traverse from the nearer end, andnullelements are permitted. - Oracle, Java SE 21 API, java.util.ArrayList. Supports:
geton an array-backed list is constant time (the contrast made in the table). - The code and every printed output were compiled and run on JDK 21. The claim that a linked list uses more memory per item than an array is general reasoning, not measured here.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.