Insert at End (Circular Linked List)
In a singly circular linked list (CLL) with a tail pointer, inserting at the end is O(1). Keep the ring intact by making the new node point to the current first node (tail->next), connect old tail to the new node, then move tail to the new node.
C program (return updated tail)
The function returns the tail pointer so main can write tail = insert_end(tail, value);. For non-empty lists, both pointer links and the tail update happen in constant time.
- Empty (
tail == NULL): one new node;newNode->next = newNode; return it astail. - Non-empty:
newNode->next = tail->next;,tail->next = newNode;, thentail = newNode;.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node *next;
};
struct Node *insert_end(struct Node *tail, int data)
{
struct Node *newNode = malloc(sizeof(struct Node));
if (newNode == NULL)
return tail;
newNode->data = data;
if (tail == NULL) {
newNode->next = newNode;
return newNode;
}
newNode->next = tail->next;
tail->next = newNode;
return newNode;
}
void print_list(struct Node *tail)
{
if (tail == NULL) {
printf("\n");
return;
}
struct Node *head = tail->next;
struct Node *cur = head;
do {
printf("%d ", cur->data);
cur = cur->next;
} while (cur != head);
printf("\n");
}
void free_list(struct Node *tail)
{
if (tail == NULL)
return;
struct Node *head = tail->next;
tail->next = NULL; /* break ring, then free linearly */
struct Node *cur = head;
while (cur != NULL) {
struct Node *next = cur->next;
free(cur);
cur = next;
}
}
int main(void)
{
struct Node *tail = NULL;
tail = insert_end(tail, 10);
tail = insert_end(tail, 20);
tail = insert_end(tail, 30);
print_list(tail); /* 10 20 30 */
free_list(tail);
return 0;
}
Program output
Traversal path for print_list()
Even after end insertion, traversal still starts at head = tail->next and completes one full loop.
Start at head (tail→next) = [10]
↓
Print 10 → move to next → [20]
↓
Print 20 → move to next → [30]
↓
Print 30 → move to next → [10]
↓
Stop (back to head)
Output: 10 20 30
Overall program flow
| Step | Operation | tail |
tail->next (first) |
Print order |
|---|---|---|---|---|
| 1 | tail = NULL |
NULL |
— | Empty |
| 2 | tail = insert_end(tail, 10) |
Node 10 |
Self (10) |
10 |
| 3 | tail = insert_end(tail, 20) |
Moves to node 20 |
Still node 10 |
10 20 |
| 4 | tail = insert_end(tail, 30) |
Moves to node 30 |
Still node 10 |
10 20 30 |
| 5 | print_list(tail) |
Same as step 4 | Same | Output line: 10 20 30 |
| 6 | free_list(tail) |
All nodes freed | — | List destroyed |
Each insert_end call in detail
Call 1: insert_end(NULL, 10)
| Step | Operation | Variable | Value |
|---|---|---|---|
| 1 | Allocate new node | newNode | Address: 0x1000 |
| 2 | Set newNode->data | newNode->data | 10 |
| 3 | tail == NULL branch | newNode->next | newNode (self-loop) |
| 4 | Return newNode | return value | 0x1000 (only node = first = last) |
After call 1 (one-node ring):
tail → [10 | ──┐
└────┘ (next points to self)
Call 2: insert_end(0x1000, 20)
| Step | Operation | Variable | Value |
|---|---|---|---|
| 1 | Allocate new node | newNode | Address: 0x2000 |
| 2 | Set newNode->data | newNode->data | 20 |
| 3 | newNode->next = tail->next | newNode->next | 0x1000 (first node) |
| 4 | tail->next = newNode | old tail next | 0x2000 |
| 5 | Return newNode | new tail | 0x2000 |
After call 2: ring 10 → 20 → 10; tail moves to 20.
tail (0x2000) ──→ [20 | 0x1000] ──→ [10 | 0x2000] ──→ …
last first (tail->next)
Call 3: insert_end(0x2000, 30)
| Step | Operation | Variable | Value |
|---|---|---|---|
| 1 | Allocate new node | newNode | Address: 0x3000 |
| 2 | Set newNode->data | newNode->data | 30 |
| 3 | newNode->next = tail->next | newNode->next | 0x1000 (first node 10) |
| 4 | tail->next = newNode | old tail next | 0x3000 |
| 5 | Return newNode | new tail | 0x3000 |
After call 3: ring 10 → 20 → 30 → 10.
tail->next (0x1000) = first
10 → 20 → 30 ─┘
▲ │
└──────┘ tail (0x3000) points to last (30)
Memory layout (example addresses)
Final ring in heap
| Address | Node | data |
next |
Points to |
|---|---|---|---|---|
0x1000 |
n1 (first insert, first) |
10 | 0x2000 |
n2 |
0x2000 |
n2 |
20 | 0x3000 |
n3 |
0x3000 |
n3 (newest insert; tail) |
30 | 0x1000 |
n1 (closes ring) |
Detailed step-by-step: call 3 (insert 30)
Inside insert_end when tail is 0x2000 (node 20) and data is 30:
| Line | Code | Before | After |
|---|---|---|---|
| 1 | malloc(sizeof(struct Node)) |
— | newNode = 0x3000 |
| 2 | newNode->data = data; |
— | newNode->data = 30 |
| 3 | newNode->next = tail->next; |
tail->next was 0x1000 |
newNode->next = 0x1000 |
| 4 | tail->next = newNode; |
old tail = node 20 |
node 20 now points to 30 |
| 5 | return newNode; |
— | main updates tail = 0x3000 |
Complexity summary
| Operation | Time | Extra space |
|---|---|---|
insert_end |
O(1) |
O(1) for the one new node |
print_list |
O(n) |
O(1) |
free_list |
O(n) |
O(1) |
| Whole program (build + print + free) | O(n) |
O(n) nodes in the ring |
With a tail pointer, end insertion in CLL is naturally O(1) and keeps tail->next as the first node. This is one of the main reasons CLL implementations prefer storing tail instead of only head.
See also Insert at Beginning (CLL) and CLL vs SLL, tail pointer.