Lecture 6: Singly Linked List Operations¶
Lecture 5 covered insertion and deletion at the front of a linked list — the cheapest
possible case. Real programs need the full toolkit: inserting or deleting at the end, at
an arbitrary position, searching for a value, updating a node, counting the list, and
reversing it entirely. This lecture builds all of them into one complete
SinglyLinkedList class, compiled and run as one program.
In This Lecture¶
- Every core singly linked list operation, built as methods on one reusable class
- Insertion and deletion at the beginning, end, and a specific position
- Deleting by value instead of by position
- Searching, updating, and counting nodes
- Reversing a linked list in place
- Step-by-step diagrams for insertion, deletion, searching, and reversal
- The complexity of every operation covered
The Singly Linked List Class¶
A singly linked list is exactly what Lecture 5 introduced: each node points only
forward, to the next node — there is no way to go backward from a node. Wrapping the
node-pointer logic inside a class (rather than free functions passing head around, like
Lecture 5 did) is how real code organizes this: the class keeps track of its own head,
and every operation becomes a method on it.
#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node(int value) : data(value), next(nullptr) {}
};
class SinglyLinkedList {
private:
Node* head;
public:
SinglyLinkedList() : head(nullptr) {}
void insertAtBeginning(int value) {
Node* newNode = new Node(value);
newNode->next = head;
head = newNode;
}
void insertAtEnd(int value) {
Node* newNode = new Node(value);
if (head == nullptr) {
head = newNode;
return;
}
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->next = newNode;
}
// position is 0-based; inserting at position == count() appends at the end.
void insertAtPosition(int value, int position) {
if (position == 0) { insertAtBeginning(value); return; }
Node* current = head;
for (int i = 0; i < position - 1 && current != nullptr; i++) {
current = current->next;
}
if (current == nullptr) return; // position out of range
Node* newNode = new Node(value);
newNode->next = current->next;
current->next = newNode;
}
void deleteFromBeginning() {
if (head == nullptr) return;
Node* oldHead = head;
head = head->next;
delete oldHead;
}
void deleteFromEnd() {
if (head == nullptr) return;
if (head->next == nullptr) { delete head; head = nullptr; return; }
Node* current = head;
while (current->next->next != nullptr) {
current = current->next;
}
delete current->next;
current->next = nullptr;
}
void deleteAtPosition(int position) {
if (position == 0) { deleteFromBeginning(); return; }
Node* current = head;
for (int i = 0; i < position - 1 && current != nullptr; i++) {
current = current->next;
}
if (current == nullptr || current->next == nullptr) return;
Node* toDelete = current->next;
current->next = toDelete->next;
delete toDelete;
}
// Deletes the FIRST node whose data equals `value`. Returns true if a
// node was found and removed, false if `value` was never in the list.
bool deleteByValue(int value) {
if (head == nullptr) return false;
if (head->data == value) {
Node* oldHead = head;
head = head->next;
delete oldHead;
return true;
}
Node* current = head;
while (current->next != nullptr && current->next->data != value) {
current = current->next;
}
if (current->next == nullptr) return false; // reached the end: not found
Node* toDelete = current->next;
current->next = toDelete->next;
delete toDelete;
return true;
}
// Returns the 0-based index of `value`, or -1 if not found.
int search(int value) const {
Node* current = head;
int index = 0;
while (current != nullptr) {
if (current->data == value) return index;
current = current->next;
index++;
}
return -1;
}
bool updateAt(int position, int newValue) {
Node* current = head;
for (int i = 0; i < position && current != nullptr; i++) {
current = current->next;
}
if (current == nullptr) return false;
current->data = newValue;
return true;
}
int countNodes() const {
int count = 0;
Node* current = head;
while (current != nullptr) {
count++;
current = current->next;
}
return count;
}
// Reverses the list in place by walking it once, re-pointing each node's `next`
// backward instead of forward.
void reverse() {
Node* previous = nullptr;
Node* current = head;
while (current != nullptr) {
Node* nextNode = current->next; // save it before we overwrite `next`
current->next = previous; // reverse this node's pointer
previous = current; // advance previous
current = nextNode; // advance current
}
head = previous; // previous is now the new head (the old tail)
}
void display() const {
Node* current = head;
while (current != nullptr) {
cout << current->data;
if (current->next != nullptr) cout << " -> ";
current = current->next;
}
cout << endl;
}
};
int main() {
SinglyLinkedList list;
list.insertAtEnd(10);
list.insertAtEnd(20);
list.insertAtEnd(30);
cout << "After inserting 10, 20, 30 at end: ";
list.display();
list.insertAtBeginning(5);
cout << "After inserting 5 at beginning: ";
list.display();
list.insertAtPosition(15, 2);
cout << "After inserting 15 at position 2: ";
list.display();
cout << "Search for 20: index " << list.search(20) << endl;
cout << "Search for 99: index " << list.search(99) << endl;
list.updateAt(1, 100);
cout << "After updating position 1 to 100: ";
list.display();
cout << "Node count: " << list.countNodes() << endl;
list.deleteAtPosition(2);
cout << "After deleting position 2: ";
list.display();
bool removed = list.deleteByValue(100);
cout << "deleteByValue(100) returned " << boolalpha << removed << ", list: ";
list.display();
bool notFound = list.deleteByValue(9999);
cout << "deleteByValue(9999) returned " << boolalpha << notFound << ", list: ";
list.display();
list.deleteFromEnd();
cout << "After deleting from end: ";
list.display();
list.reverse();
cout << "After reversing: ";
list.display();
return 0;
}
$ g++ -std=c++17 -o singly_linked_list singly_linked_list.cpp
$ ./singly_linked_list
After inserting 10, 20, 30 at end: 10 -> 20 -> 30
After inserting 5 at beginning: 5 -> 10 -> 20 -> 30
After inserting 15 at position 2: 5 -> 10 -> 15 -> 20 -> 30
Search for 20: index 3
Search for 99: index -1
After updating position 1 to 100: 5 -> 100 -> 15 -> 20 -> 30
Node count: 5
After deleting position 2: 5 -> 100 -> 20 -> 30
deleteByValue(100) returned true, list: 5 -> 20 -> 30
deleteByValue(9999) returned false, list: 5 -> 20 -> 30
After deleting from end: 5 -> 20
After reversing: 20 -> 5
Why reverse() only needs one pass
reverse() never allocates a new node or copies any data — it walks the list exactly
once, and at each node it flips next to point backward instead of forward, using
two helper pointers (previous and nextNode) so it never loses track of the rest of
the list. This is the standard pattern for in-place linked list reversal and is worth
tracing on paper, node by node, until it clicks.
Visualizing the Operations, Step by Step¶
Code alone hides why each operation has the complexity it does — the diagrams below
make the pointer movement (and, for insertAtEnd and deleteByValue, the O(n) walk
that precedes it) explicit for the operations that don't reduce directly to the
front-insertion/front-deletion cases Lecture 5 already diagrammed.
insertAtEnd: Walk, Then Attach¶
insertAtEnd has no tail pointer to shortcut with (Lecture 7's doubly linked list adds
one) — it must walk every node until current->next is nullptr, which is the entire
reason this operation is O(n) instead of O(1).
flowchart LR
Head(["head"]) --> N1["10"] --> N2["20"] --> N3["30<br/>(current: next is ✕)"]
N3 -.->|"current->next = newNode"| New["40<br/>(new node, next: ✕)"]
insertAtPosition: Splice In the Middle¶
Once current (the node just before the target position) is found by the walk, inserting
in the middle is exactly Lecture 5's arbitrary-position insertion: point the new node at
what current used to point to, then repoint current itself.
flowchart LR
N1["10<br/>(current)"] --> N2["20"]
New["15<br/>(new node)"] -.->|"1. newNode->next =<br/>current->next"| N2
N1 -.->|"2. current->next =<br/>newNode"| New
deleteByValue: Search, Then Splice Out¶
deleteByValue combines two ideas already covered separately: it walks like search
(comparing current->next->data against the target, since it needs to stop one node
early to keep a reference to the node being removed), then splices exactly like
deleteAtPosition once a match is found.
flowchart LR
N1["10<br/>(current)"] --> Del["100<br/>(matches --<br/>current->next)"]
Del --> N2["20"]
N1 -.->|"current->next =<br/>toDelete->next"| N2
search: Comparing Node by Node¶
There is no shortcut — search must compare data against every node starting from
head, in order, until it finds a match or runs out of nodes. This is precisely the
"no random access" limitation Lecture 5 introduced, made concrete.
flowchart LR
Head(["head"]) --> N1["index 0: 10<br/>10 == 20? no"] --> N2["index 1: 20<br/>20 == 20? found!"] --> N3["index 2: 30<br/>(never reached)"]
reverse: Every next Pointer Flips Direction¶
reverse()'s single pass leaves every node holding the same data, but every next
pointer now points the opposite way — and head moves from the old first node to the old
last node.
flowchart LR
subgraph Before["Before reverse()"]
direction LR
H1(["head"]) --> B1["10"] --> B2["20"] --> B3["30"] --> BX["nullptr"]
end
flowchart LR
subgraph After["After reverse()"]
direction LR
H2(["head"]) --> A1["30"] --> A2["20"] --> A3["10"] --> AX["nullptr"]
end
Every arrow reversed direction, and the node that used to be the tail (30) is now the
head — exactly what head = previous; sets up at the end of the loop, since previous
finishes the walk sitting on the old last node.
Complexity of Singly Linked List Operations¶
| Operation | Complexity | Why |
|---|---|---|
| Insert at beginning | O(1) | Only the new node's next and head change |
| Insert at end | O(n) | Must walk the whole list to find the current last node |
Insert at position k |
O(k) | Must walk k nodes in from the head |
| Delete from beginning | O(1) | Only head and one pointer change |
| Delete from end | O(n) | Must walk to the second-to-last node |
Delete at position k |
O(k) | Must walk k nodes in from the head |
| Delete by value | O(n) | Must search for the value first, in the worst case all the way to the last node |
| Search | O(n) | No way to skip ahead — must check nodes one by one |
Update at position k |
O(k) | Same walk as insert/delete at a position |
| Count nodes | O(n) | Must visit every node once |
| Reverse | O(n) | Visits every node exactly once |
Compare this table to Lecture 4's array complexity table: a linked list flips the array's trade-off almost exactly — cheap insert/delete at the front instead of the end, expensive access by position instead of cheap.
| Operation | Array | Singly Linked List |
|---|---|---|
| Access by index | O(1) | O(k) — must walk from head |
| Insert/delete at front | O(n) (shift everything) | O(1) |
| Insert/delete at end | O(1) (if there's room) | O(n) — no tail pointer here |
Insert/delete at position k |
O(n) (shift from k onward) |
O(k) — walk, then splice |
| Search by value | O(n) | O(n) |
Every row tells the same story from a different angle: an array pays its cost up front
(shifting elements to keep them contiguous) so that later access is instant; a singly
linked list pays nothing to stay non-contiguous, but every access has to be earned with a
walk from head.
Common pitfalls when writing these operations by hand
A handful of mistakes account for nearly every bug students write in this unit — worth checking your own code against, every time:
- Losing the rest of the list. Overwriting a
nextpointer before saving what it used to point to (exactly the bug the diagrams above are built to prevent) silently detaches every node after that point — they still exist in memory, but nothing reachable fromheadpoints to them anymore, a memory leak with no crash to warn you. - Off-by-one position indices.
insertAtPosition's loop walks toposition - 1, notposition— because it needs the node before the target, not the target itself. Writingpositioninstead ofposition - 1is the single most common typo in this kind of code, and it silently inserts one slot too late instead of crashing. - Forgetting the empty-list check. Every method above starts by asking "is
head(or the walk it's about to do) actually valid?" Skip that check and the very first call on a freshly constructed, emptySinglyLinkedListdereferences anullptr. - Dereferencing after
delete. Covered in depth in Lecture 5 — reading any field of a node afterdelete-ing it is undefined behavior, not merely "risky." - Not checking
current == nullptrafter a bounds-limited walk.deleteAtPositionandinsertAtPosition's loops stop early if they run off the end — but the code after the loop still needs to check whethercurrent(orcurrent->next) came backnullptrbefore dereferencing it, or an out-of-range position crashes instead of failing gracefully.
Try It Yourself¶
- Compile and run
singly_linked_list.cppyourself, then add a call toinsertAtPosition(999, 0)and confirm from the output that it behaves identically toinsertAtBeginning— trace through the code to explain why theposition == 0check makes that guaranteed, not a coincidence. - Add a method
int sum() constthat returns the sum of every node's data, and a methodbool isEmpty() const. Test both by calling them before and after emptying the list with repeateddeleteFromBeginning()calls. deleteByValueonly removes the first matching node. Write a methodint deleteAllByValue(int value)that removes every node holdingvalueand returns how many were removed. Test it on a list with several duplicates (for example, build10 -> 5 -> 20 -> 5 -> 5 -> 30and confirm all three5s are gone afterward).- Draw the "Before reverse()" and "After reverse()" diagrams above yourself, but for a
4-node list of your choosing, then trace
reverse()'s loop by hand, writing down the values ofprevious,current, andnextNodeafter each iteration, before checking your answer by actually running the code.
Key Takeaways¶
- Wrapping node-pointer logic inside a class turns loose functions into a reusable,
self-contained ADT — the caller never touches
Nodeorheaddirectly. - Insert/delete at the beginning are O(1); insert/delete at the end or a specific position are O(n) or O(k), because reaching that point requires walking the list — there is no shortcut the way array indexing provides one.
- Deleting by value combines a
search-style walk with adeleteAtPosition-style splice — it's O(n) because, unlike deleting by position, the position isn't known ahead of time. - Reversing a singly linked list is a classic O(n), single-pass algorithm using three
pointers (
previous,current,nextNode) to flip eachnextpointer without losing the rest of the list — every arrow in the list ends up pointing the opposite way, and the old tail becomes the new head. - Every operation's complexity comes down to one fact: a singly linked list can only be walked forward, one node at a time, from the head.