انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-040-L03

القوائم المترابطة (Linked lists)

  • الدرس 3 من 7
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 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 next reference.
  • Explain the roles of head, tail, and the final next == null.
  • Implement addFirst, addLast, contains, and remove on 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

java
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:

text
head                              tail
 |                                 |
[A|*]-->[B|*]-->[C|*]-->[D|null]
  • head is the first node. Without it you lose the whole list, because nothing else points at the first node.
  • tail is the last node. Keeping it is optional but makes adding at the end fast.
  • The last node's next is null. 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:

java
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:

java
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:

java
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:

java
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:

  1. Not found: cur reached null; change nothing.
  2. Removing the head (prev == null): the head moves to the next node.
  3. Removing any other node: prev.next = cur.next skips it. If cur was the tail, the tail becomes prev. Forgetting this leaves tail pointing at a node that is no longer in the list, and the next addLast quietly 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
addFirstO(1)
addLast with tailO(1)
addLast without tailO(n)
get(i), contains, remove(value)O(n)
Remove the first nodeO(1)
Remove the last nodeO(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-basedLinked
Get by indexO(1)O(n)
Insert/remove at a known positionO(n) shiftingO(1) rewiring
MemoryItems packed togetherNode object plus one reference per item
GrowthResize and copyAdd one node

Lesson 6 turns this into a rule for choosing.

Examples

Example 1: building a list and looking inside (simple)

java
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:

text
A -> B -> C -> null
true false

The 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)

java
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:

text
A -> C -> null
A -> D -> null
true D -> null
false

Removing 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:

java
history.addFirst("/orders");
history.addFirst("/orders/42");
history.remove("/orders");     // O(n) search, O(1) rewiring

The 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 as for (i...) list.get(i) over a LinkedList, 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 tail pointing at it.
  • Correct: after removing, if the removed node was the tail, set tail = prev. If it was the head, set head = cur.next. If the list becomes empty, both end up null (the code above gives this: head = null, then tail = prev which is null).

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.

java
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.

java
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.

java
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:

  1. Adds three items, removes the first twice, and prints the list.
  2. Removes the last remaining item, then adds a new item with addLast.
  3. 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; head starts the chain and the last next is null.
  • addFirst and (with tail) addLast are O(1); get(i), contains, and remove(value) are O(n).
  • To remove a node, point the previous node's next past it, and update head or tail when 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

  1. What does a node contain?
  2. Why must addFirst set n.next = head before head = n?
  3. What is the cost of get(i) on a linked list, and why?
  4. What must you update when the removed node was the tail?
  5. Why is removing the last node O(n) in a singly linked list?
أظهر الإجابات
  1. A value and a reference to the next node.
  2. Otherwise the old chain is lost because head would no longer point at it.
  3. O(n): you walk i nodes from the head.
  4. tail must become the previous node (or null if the list became empty).
  5. 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: LinkedList is a doubly linked list, indexing operations traverse from the nearer end, and null elements are permitted.
  • Oracle, Java SE 21 API, java.util.ArrayList. Supports: get on 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.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.