Creating a Singly Linked List from Scratch in DSA Go - Complexity Walkthrough
When we create a singly linked list by adding nodes one by one, it is important to understand how the time needed grows as we add more nodes.
We want to know how the work changes when the list gets bigger.
Analyze the time complexity of the following code snippet.
package main
import "fmt"
type Node struct {
value int
next *Node
}
type LinkedList struct {
head *Node
}
func (l *LinkedList) Append(val int) {
newNode := &Node{value: val}
if l.head == nil {
l.head = newNode
return
}
current := l.head
for current.next != nil {
current = current.next
}
current.next = newNode
}
func main() {
list := LinkedList{}
for i := 1; i <= 5; i++ {
list.Append(i)
}
current := list.head
for current != nil {
fmt.Print(current.value, " -> ")
current = current.next
}
fmt.Println("nil")
}
This code creates a singly linked list by adding new nodes at the end one by one.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The loop inside the Append method that moves through the list to find the last node.
- How many times: For each new node added, this loop runs zero times for the first node (since the list is empty), once for the second, twice for the third, and so on, increasing with the list size.
Explain the growth pattern intuitively.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 45 steps (0+1+...+9) |
| 100 | About 4950 steps (sum of 0 to 99) |
| 1000 | About 499,500 steps (sum of 0 to 999) |
Pattern observation: The total steps grow roughly like the square of the number of nodes added, because each new node requires walking through the existing list.
Time Complexity: O(n²)
This means that as the list grows, the time to add all nodes grows very quickly, roughly like the square of the number of nodes.
[X] Wrong: "Appending a node always takes the same amount of time, so adding n nodes is O(n)."
[OK] Correct: Each append must find the end by walking through the list, which takes longer as the list grows, so the total time adds up faster than just n.
Understanding this helps you explain why adding nodes to a linked list can be slow if you don't keep track of the end, and shows your grasp of how data structure design affects performance.
"What if we kept a pointer to the last node (tail) and updated it on each append? How would the time complexity change?"
Practice
Solution
Step 1: Understand singly linked list structure
A singly linked list stores elements in nodes where each node points to the next node, forming a chain.Step 2: Compare with other data structures
Unlike trees or arrays, singly linked lists have a linear sequence with pointers only to the next element.Final Answer:
To store elements in a linear order where each element points to the next -> Option DQuick Check:
Singly linked list = linear chain with next pointers [OK]
- Confusing singly linked list with trees or arrays
- Thinking nodes have pointers to previous nodes
- Assuming fixed size storage like arrays
Solution
Step 1: Identify correct pointer usage
In Go, the next node must be a pointer to Node, so *Node is correct.Step 2: Check struct fields
Field 'value' stores data, 'next' points to the next node. 'prev' is for doubly linked lists, and 'interface{}' is too generic.Final Answer:
type Node struct { value int; next *Node } -> Option AQuick Check:
Node next pointer = *Node [OK]
- Using value type instead of pointer for next
- Confusing singly linked list with doubly linked list (prev pointer)
- Using interface{} instead of *Node for next
package main
import "fmt"
type Node struct {
value int
next *Node
}
func main() {
head := &Node{value: 1}
second := &Node{value: 2}
third := &Node{value: 3}
head.next = second
second.next = third
current := head
for current != nil {
fmt.Print(current.value)
if current.next != nil {
fmt.Print(" -> ")
}
current = current.next
}
fmt.Println(" -> null")
}Solution
Step 1: Trace node creation and linking
Nodes with values 1, 2, 3 are created and linked in order: head.next = second, second.next = third.Step 2: Trace printing loop
Loop prints current.value, then ' -> ' if next exists, moving from head to third, then prints ' -> null'.Final Answer:
1 -> 2 -> 3 -> null -> Option CQuick Check:
Print nodes in order 1,2,3 with arrows [OK]
- Printing nodes in reverse order
- Missing last node in output
- Confusing pointer assignments causing nil errors
func appendNode(head *Node, val int) {
newNode := &Node{value: val}
current := head
for current.next != nil {
current = current.next
}
current = newNode
}Solution
Step 1: Analyze the loop and pointer update
The loop correctly finds the last node where current.next is nil.Step 2: Check how new node is linked
Assigning current = newNode only changes the local variable, not the last node's next pointer. It should be current.next = newNode.Final Answer:
The new node is not linked to the list; current pointer is reassigned incorrectly -> Option BQuick Check:
Link new node by current.next = newNode [OK]
- Reassigning current instead of current.next
- Wrong loop condition causing nil pointer dereference
- Not returning updated head when needed
func reverseList(head *Node) *Node {
var prev *Node = nil
current := head
for current != nil {
nextTemp := current.next
current.next = prev
prev = current
current = nextTemp
}
return prev
}
What will be the output if the original list is 1 -> 2 -> 3 -> null and you print the reversed list?Solution
Step 1: Understand reversal logic
The function iterates through the list, reversing each node's next pointer to point to the previous node.Step 2: Trace reversal on list 1 -> 2 -> 3
After reversal, the list becomes 3 -> 2 -> 1 -> null, and prev points to new head.Final Answer:
3 -> 2 -> 1 -> null -> Option AQuick Check:
Reversed list head = prev = 3 [OK]
- Not updating current.next correctly
- Losing track of next node causing infinite loop
- Returning old head instead of new head
Ready to prove it?
Pass the test and this pattern moves unknown
