Collection

**Collection** is a group of items that we want to treat as conceptual unit. Collections can be
homogeneous when all items in the collection must be of the same type, or heterogeneous when items can be of different types. For example, lists are heterogeneous in Python.

linear collection
grocery lists, queue
hierarchical collection
upside down tree, something similiar to this file
graph collection

Each data item can have many predecessors and many successors
Unordered collection
self explanatory

Free Space List: a linked list that acts as resource and recycle of nodes for data structures (linked list, stack, queue, BST)

Linked-list insertion and deletion from the beginning

For WA2, you need to understand a singly linked list.

A linked list is a chain of nodes:

head
 ↓
[A | •] → [B | •] → [C | None]

Each node stores:

[data | next]
  • data is the value inside the node.
  • next refers to the next node.
  • head refers to the first node.
  • None means there is no next node.

Your notes require insertion and deletion by position or value, and the revision papers test inserting at the fourth position and deleting the first node containing a target value. 8.DataStructures.pdfPDF WA2 Revision Paper 1 soln.pdfPDF WA2 Revision Paper 2 soln.pdfPDF


1. The basic classes

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

Creating one node:

node1 = Node("A")

produces:

node1
  ↓
[A | None]

A linked-list class stores the head:

class LinkedList:
    def __init__(self):
        self.head = None

Initially:

self.head → None

This means the linked list is empty.


2. Four variables you must understand

VariableMeaning
self.headFirst node in the list
currThe node we are currently looking at
prevThe node immediately before curr
new_nodeA node we want to insert

Example:

head → [A] → [B] → [C] → None
        ↑     ↑
       prev  curr

Here:

prev.data = "A"
curr.data = "B"
curr.next refers to C

3. The central idea

Insertion

Insertion means:

Change the arrows so the new node becomes part of the chain.

Before:

[A] → [B] → [C]

Insert X after B:

[A] → [B] → [X] → [C]

Deletion

Deletion means:

Change an arrow so that it skips the node being deleted.

Before:

[A] → [B] → [C]

Delete B:

[A] ─────→ [C]

The most important difference is:

Insertion: add an arrow through the new node
Deletion: bypass an existing node

Part A: Insertion

4. Insert at the front

Before:

head → [B] → [C] → None

new_node → [A] → None

We want:

head → [A] → [B] → [C] → None

Code

new_node.next = self.head
self.head = new_node

Line 1

new_node.next = self.head

self.head currently refers to B.

Therefore:

A.next = B

Now:

new_node → [A] → [B] → [C] → None

But head still points to B.

Line 2

self.head = new_node

Now head points to A:

head → [A] → [B] → [C] → None

Complete method

def insert_front(self, data):
    new_node = Node(data)

    new_node.next = self.head
    self.head = new_node

Usage:

my_list.insert_front("A")

Memory sentence

Connect the new node to the old head, then make it the new head.


5. Insert after a particular node

Before:

[A] → [B] → [C] → None
       ↑
      curr

We want to insert X after B.

[A] → [B] → [X] → [C] → None

Code

new_node.next = curr.next
curr.next = new_node

Line 1

new_node.next = curr.next

curr points to B.

curr.next points to C.

Therefore:

X.next = C

Line 2

curr.next = new_node

This means:

B.next = X

Final list:

[A] → [B] → [X] → [C] → None

Why must the lines be in this order?

Correct:

new_node.next = curr.next
curr.next = new_node

First, make X remember where C is.

Then connect B to X.

If you replaced B.next too early, you could lose the reference to C.

Memory sentence

New points forward first; previous node points to new second.


6. Insert at a given position

We will number positions starting from 1:

Position    1       2       3       4
head →     [A] →   [B] →   [C] →   [D] → None

Suppose we want to insert X at position 4:

Position    1       2       3       4       5
head →     [A] →   [B] →   [C] →   [X] →   [D] → None

To insert at position 4, we must stop at position 3.

Why?

Because position 3 is the node before the insertion position.

[C].next must change from D to X

Moving to the predecessor

curr = self.head

for count in range(position - 2):
    curr = curr.next

For position 4:

position - 2 = 2

Start at position 1 and move twice:

A → B → C

Now curr points to C.

Then insert:

new_node.next = curr.next
curr.next = new_node

This is the same pointer pattern used in your revision paper’s fourth-position insertion solution. WA2 Revision Paper 2 soln.pdfPDF

Complete method

def insert_at_position(self, data, position):
    if position < 1:
        print("Invalid position")
        return
 
    new_node = Node(data)
 
    # Special case: insert at the front
    if position == 1:
        new_node.next = self.head
        self.head = new_node
        return
 
    # Move to the node before the insertion position
    curr = self.head
 
    for count in range(position - 2):
        if curr is None:
            print("Invalid position")
            return
 
        curr = curr.next
 
    # If curr is None, the position is beyond the list
    if curr is None:
        print("Invalid position")
        return
 
    new_node.next = curr.next
    curr.next = new_node

Example

Starting list:

head → [A] → [B] → [C] → None

Call:

my_list.insert_at_position("X", 3)

The method stops at position 2, which is B.

Then:

new_node.next = curr.next

means:

X.next = C

and:

curr.next = new_node

means:

B.next = X

Final list:

head → [A] → [B] → [X] → [C] → None

7. Insert after a given value

Suppose:

head → [A] → [B] → [C] → None

We want to insert X after the node containing B.

First, search for B.

curr = self.head

while curr is not None and curr.data != target:
    curr = curr.next

When the loop ends:

  • curr points to the target node, or
  • curr is None if the target was not found.

Complete method

def insert_after_value(self, target, data):
    curr = self.head
 
    # Search for the target
    while curr is not None and curr.data != target:
        curr = curr.next
 
    if curr is None:
        print("Target not found")
        return
 
    new_node = Node(data)
 
    new_node.next = curr.next
    curr.next = new_node

Usage:

my_list.insert_after_value("B", "X")

Result:

[A] → [B] → [X] → [C] → None

8. Insert in alphabetical or numerical order

Your past paper describes a spellbook stored in alphabetical order. The question itself tests deletion, but you should understand how sorted insertion works. WA2 Revision Paper 1 soln.pdfPDF

Suppose:

Apple → Mango → Pear

We want to insert Banana.

It belongs between Apple and Mango.

Code

def insert_sorted(self, data):
    new_node = Node(data)
 
    # Insert at the front if the list is empty
    # or the new value comes before the first value
    if self.head is None or data < self.head.data:
        new_node.next = self.head
        self.head = new_node
        return
 
    curr = self.head
 
    # Stop when the next value is greater than the new value
    while curr.next is not None and curr.next.data < data:
        curr = curr.next
 
    new_node.next = curr.next
    curr.next = new_node

Result:

Apple → Banana → Mango → Pear

Do not memorise this before you understand ordinary insertion. The two crucial pointer lines are still:

new_node.next = curr.next
curr.next = new_node

Part B: Deletion

9. Delete the first node

Before:

head → [A] → [B] → [C] → None

To delete A, move head forward:

self.head = self.head.next

self.head.next refers to B.

After:

head → [B] → [C] → None

Complete method

def delete_front(self):
    if self.head is None:
        print("Linked list is empty")
        return
 
    self.head = self.head.next

Memory sentence

Deleting the first node means moving head forward once.


10. Delete a middle node

Before:

[A] → [B] → [C] → None
 ↑     ↑
prev  curr

We want to delete B.

prev points to A.

curr points to B.

curr.next points to C.

Code

prev.next = curr.next

This means:

A.next = C

After:

[A] ─────→ [C] → None

B has been bypassed.

Memory sentence

Make the previous node point to the node after the deleted node.


11. Delete by value

Suppose:

head → [A] → [B] → [C] → None

We want to delete the first node containing B.

There are three cases.

Case 1: The list is empty

head → None

There is nothing to delete.

if self.head is None:
    print("Linked list is empty")

Case 2: The target is at the head

head → [B] → [C] → None

Delete it using:

self.head = self.head.next

Case 3: The target is later in the list

Use two variables:

prev = self.head
curr = self.head.next

Then search:

while curr is not None and curr.data != target:
    prev = curr
    curr = curr.next

When the target is found:

prev.next = curr.next

Complete method

def remove(self, target):
    # Case 1: empty list
    if self.head is None:
        print("Linked list is empty")
        return
 
    # Case 2: target is at the head
    if self.head.data == target:
        self.head = self.head.next
        return
 
    # Case 3: target is later in the list
    prev = self.head
    curr = self.head.next
 
    while curr is not None and curr.data != target:
        prev = curr
        curr = curr.next
 
    if curr is None:
        print("Target not found")
    else:
        prev.next = curr.next

This is the main WA2-style deletion method from your first revision paper. WA2 Revision Paper 1 soln.pdfPDF

Example trace

List:

[A] → [B] → [C] → [D] → None

Target:

C

Initially:

prev → A
curr → B

B is not C, so move both:

prev → B
curr → C

Now the target is found.

Run:

prev.next = curr.next

This means:

B.next = D

Final list:

[A] → [B] → [D] → None

12. Why the while condition is written in this order

Use:

while curr is not None and curr.data != target:

Do not use:

while curr.data != target and curr is not None:

Why?

If curr is None, this is illegal:

curr.data

None has no data attribute.

Therefore, always check:

curr is not None

before accessing:

curr.data

13. Delete by position

Positions start from 1:

Position    1       2       3       4
head →     [A] →   [B] →   [C] →   [D] → None

Suppose we want to delete position 3, containing C.

We need to stop at position 2, containing B.

Then:

prev.next = prev.next.next

prev.next is C.

prev.next.next is D.

Therefore:

B.next = D

Complete method

def delete_at_position(self, position):
    if position < 1:
        print("Invalid position")
        return
 
    if self.head is None:
        print("Linked list is empty")
        return
 
    # Special case: delete the first node
    if position == 1:
        self.head = self.head.next
        return
 
    # Move to the node before the deletion position
    prev = self.head
 
    for count in range(position - 2):
        if prev is None:
            print("Invalid position")
            return
 
        prev = prev.next
 
    # The node to delete must exist
    if prev is None or prev.next is None:
        print("Invalid position")
        return
 
    prev.next = prev.next.next

Example

head → [A] → [B] → [C] → [D] → None

Call:

my_list.delete_at_position(3)

Move to position 2:

prev → B

Then:

prev.next = prev.next.next

becomes:

B.next = D

Final list:

head → [A] → [B] → [D] → None

14. Deleting the last node

You do not need a completely different algorithm.

Example:

[A] → [B] → [C] → None

To delete C:

prev → B
curr → C

Then:

prev.next = curr.next

Since:

curr.next = None

this becomes:

B.next = None

Final list:

[A] → [B] → None

The same deletion pattern works for middle and last nodes.


15. Complete beginner-friendly class

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None
 
 
class LinkedList:
    def __init__(self):
        self.head = None
 
    def display(self):
        curr = self.head
 
        while curr is not None:
            print(curr.data)
            curr = curr.next
 
    def insert_front(self, data):
        new_node = Node(data)
 
        new_node.next = self.head
        self.head = new_node
 
    def insert_at_position(self, data, position):
        if position < 1:
            print("Invalid position")
            return
 
        new_node = Node(data)
 
        if position == 1:
            new_node.next = self.head
            self.head = new_node
            return
 
        curr = self.head
 
        for count in range(position - 2):
            if curr is None:
                print("Invalid position")
                return
 
            curr = curr.next
 
        if curr is None:
            print("Invalid position")
            return
 
        new_node.next = curr.next
        curr.next = new_node
 
    def remove(self, target):
        if self.head is None:
            print("Linked list is empty")
            return
 
        if self.head.data == target:
            self.head = self.head.next
            return
 
        prev = self.head
        curr = self.head.next
 
        while curr is not None and curr.data != target:
            prev = curr
            curr = curr.next
 
        if curr is None:
            print("Target not found")
        else:
            prev.next = curr.next
 
    def delete_at_position(self, position):
        if position < 1:
            print("Invalid position")
            return
 
        if self.head is None:
            print("Linked list is empty")
            return
 
        if position == 1:
            self.head = self.head.next
            return
 
        prev = self.head
 
        for count in range(position - 2):
            if prev is None:
                print("Invalid position")
                return
 
            prev = prev.next
 
        if prev is None or prev.next is None:
            print("Invalid position")
            return
 
        prev.next = prev.next.next

Using it

my_list = LinkedList()
 
my_list.insert_front("C")
my_list.insert_front("B")
my_list.insert_front("A")

List:

A → B → C

Display:

my_list.display()

Output:

A
B
C

Insert X at position 3:

my_list.insert_at_position("X", 3)

List:

A → B → X → C

Delete B:

my_list.remove("B")

List:

A → X → C

Delete position 2:

my_list.delete_at_position(2)

List:

A → C

16. Pseudocode versions for exams

Insert at position i

IF i = 1 THEN
    new.next ← head
    head ← new
ELSE
    curr ← head
 
    FOR count ← 1 TO i - 2
        curr ← curr.next
    ENDFOR
 
    new.next ← curr.next
    curr.next ← new
ENDIF

Your notes describe the same two insertion cases: insertion at the front updates head, while insertion at position i updates the link of the node at position i - 1. 8.DataStructures.pdfPDF

Delete by value

IF head = NULL THEN
    OUTPUT "Linked list is empty"
 
ELSE IF head.data = target THEN
    head ← head.next
 
ELSE
    prev ← head
    curr ← head.next
 
    WHILE curr <> NULL AND curr.data <> target
        prev ← curr
        curr ← curr.next
    ENDWHILE
 
    IF curr = NULL THEN
        OUTPUT "Target not found"
    ELSE
        prev.next ← curr.next
    ENDIF
ENDIF

Part C: Array implementation and free-space list

Your teacher also highlighted:

  • Linked list implemented using arrays
  • Free-space list

Tutorial 8A explicitly includes linked lists stored using arrays and managing free space when adding or deleting items. 8.DataStructures.pdfPDF

17. Parallel-array linked list

Instead of storing actual Python node objects, we can use two arrays:

Data[index]
Next[index]

Example:

IndexDataNext
0B3
1-1
2A0
3C-1

Suppose:

Start = 2

Follow the indexes:

Start = 2
Data[2] = A
Next[2] = 0
 
Data[0] = B
Next[0] = 3
 
Data[3] = C
Next[3] = -1

Logical list:

A → B → C

-1 plays the same role as None.


18. What is the free-space list?

Unused array positions are also linked together.

Suppose positions 1 and 4 are unused:

Free = 1

and:

Next[1] = 4
Next[4] = -1

Free-space chain:

Free → index 1 → index 4 → -1

This means positions 1 and 4 are available for new nodes.

The slides require this concept; the following algorithms are the standard Start, Free, Data and Next implementation consistent with those exercises.


19. Taking a space during insertion

Suppose:

Free = 1
Next[1] = 4

Use the first free position:

new_index = Free
Free = Next[Free]

Afterwards:

new_index = 1
Free = 4

Index 1 has been removed from the free-space list.

Store the new data:

Data[new_index] = new_item

Insert at the front

Next[new_index] = Start
Start = new_index

This is the array equivalent of:

new_node.next = head
head = new_node

Comparison:

Object versionArray version
new_node.next = headNext[new_index] = Start
head = new_nodeStart = new_index

20. Returning a space during deletion

Suppose we delete the node at delete_index.

After removing it from the main list, return it to the front of the free-space list:

Next[delete_index] = Free
Free = delete_index

This is similar to inserting the deleted index at the front of the free-space chain.

You may also clear its data:

Data[delete_index] = None

21. Array insertion and deletion templates

Insert at front

if Free == -1:
    print("No free space")
else:
    new_index = Free
    Free = Next[Free]
 
    Data[new_index] = new_item
 
    Next[new_index] = Start
    Start = new_index

Delete front

if Start == -1:
    print("List is empty")
else:
    delete_index = Start
    Start = Next[Start]
 
    Data[delete_index] = None
    Next[delete_index] = Free
    Free = delete_index

Insert after index curr

new_index = Free
Free = Next[Free]

Data[new_index] = new_item

Next[new_index] = Next[curr]
Next[curr] = new_index

Delete the node after index prev

delete_index = Next[prev]

Next[prev] = Next[delete_index]

Data[delete_index] = None
Next[delete_index] = Free
Free = delete_index

The six code patterns to memorise

# 1. Move forward
curr = curr.next
# 2. Insert at front
new_node.next = self.head
self.head = new_node
# 3. Insert after curr
new_node.next = curr.next
curr.next = new_node
# 4. Delete front
self.head = self.head.next
# 5. Delete curr
prev.next = curr.next
# 6. Search
while curr is not None and curr.data != target:
    curr = curr.next

Final memory picture

INSERT AFTER curr
 
Before:
curr → [B] → [C]
 
Step 1:
new → [X] → [C]
 
Step 2:
curr → [B] → [X] → [C]
 
 
DELETE curr
 
Before:
prev → [A] → [B] → [C]

             curr
 
Change:
prev.next = curr.next
 
After:
[A] ─────────→ [C]

The two most important lines for WA2 are:

new_node.next = curr.next
curr.next = new_node

and:

prev.next = curr.next