Linked List - Nodes
""" LINKED LIST - NODES
-----------------------
A Node is a small container that holds:
- data (the actual value)
- nextNode (a reference to the next node in memory)
Important:
- nodes are NOT stored next to each other in memory
- each node only knows where the NEXT node is
- we must follow the links step by step
Real-life analogy:
A treasure hunt clue that tells you:
- the message
- where the next clue is
-------------------------
"""
class Node:
def __init__(self, data):
self.data = data # The value stored in the node
self.next = None # Reference to the next node
# Example: Manual Node Linking
# ----------------------------
node1 = Node("Go to the oak tree")
node2 = Node("Look under the bench")
node3 = Node("Check the fountain")
node1.next = node2
node2.next = node3
print(node1.data, '->', node1.next.data)
print(node2.data, '->', node2.next.data)
# ------------------------------------------
# Go to the oak tree -> Look under the bench
# Look under the bench -> Check the fountain
Linked Lists - Operations
""" LINKED LIST - OPERATIONS
----------------------------
Concept:
- Keep track ONLY of the first node
- Traverses nodes by following next references
- Has no direct access by index (unlike arrays)
Time Complexity:
- Read (by index): O(n)
- Insert at beggining: O(1)
- Insert elsewhere: O(n)
Below:
- Linked list that stores a reference to the first node.
--------------------------------------------------------
"""
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self, first):
self.first = first
# --------------------------
# READ value at index / O(n)
# --------------------------
def read(self, index):
current = self.first
i = 0
while i < index:
current = current.next
i += 1
return current
# ----------------------------
# INSERT value at index / O(n)
# ----------------------------
def insert(self, index, value):
node = Node(value)
# Insert at the beginning / O(1)
if index == 0:
node.next = self.first
self.first = node
return
# Insert elsewhere / O(n)
current = self.first
i = 0
# Stop at node BEFORE insertion point
while i < index - 1:
current = current.next
i += 1
node.next = current.next
current.next = node
# ---------------------
# Example Usage & Tests
# ---------------------
# Create nodes
n1 = Node("Start")
n2 = Node("Forest")
n3 = Node("River")
n4 = Node("Treasure")
# Link nodes
n1.next = n2
n2.next = n3
n3.next = n4
# Create linked list
hunt = LinkedList(n1)
# Insert new nodes
hunt.insert(0, "Map")
hunt.insert(3, "Cave")
# Read nodes
print(hunt.read(0).data) # Map
print(hunt.read(1).data) # Start
print(hunt.read(2).data) # Forest
print(hunt.read(3).data) # Cave
print(hunt.read(4).data) # River
print(hunt.read(5).data) # Treasure
Linked List - Insert Tail
""" LINKED LIST - INSERT AT TAIL
--------------------------------
Concept:
- We only store a reference to the FIRST node (head)
- Nodes know ONLY about the next node
- To insert at the tail, we must traverse the list
Key Insights:
- Assigning `current = self.head` does NOT copy the node
- `current` and `self.head point` to the SAME object
- Moving `current` does not move `self.head`
- Modifying the `current.next` MUTTATES the list itself (!)
Time Complexity:
- Insert at tail: O(n)
-----------------------
"""
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self, head=None):
self.head = head
def insert_at_tail(self, value):
new_node = Node(value)
# -------------------------------------
# Case 1: Empty list
# The new node becames the first (head)
# -------------------------------------
if self.head is None:
self.head = new_node
# -----------------------------
# Case 2: Non-empty list
# Start traversal from the head
# -----------------------------
current = self.head
# ---------------------------------------------
# Move `current` until it reaches the last node
# (the node whose `next` is None)
# ---------------------------------------------
while current.next is not None:
current = current.next
# ------------------------------------------------
# We mutate the last node, extending the list that
# `self.head` already points to.
# ------------------------------------------------
current.next = new_node
# ---------------------
# Example Usage & Tests
# ---------------------
# Create nodes
n1 = Node("Start")
n2 = Node("Forest")
n3 = Node("River")
# Link nodes
n1.next = n2
n2.next = n3
# Create linked list
hunt = LinkedList(n1)
# Insert at tail
hunt.insert_at_tail("Cave")
hunt.insert_at_tail("Treasure")
# Traverse and print
current = hunt.head
while current:
print(current.data)
current = current.next
# --------------------
# Start
# Forest
# River
# Cave
# Treasure