Skip to lab content

EXPERIMENT 03 · DATA STRUCTURES LAB

Circular singly linked list

SOLUTION 03C173 tested sample runs

THE SYLLABUS TASK

What you need to solve

Write a program that uses functions to perform creation, insertion, deletion and traversal on a circular singly linked list.
Input format & conventions

Commands are integer tokens separated by spaces/newlines. Values: −1,000,000 to 1,000,000. Creation accepts 0–100 values. Positions are one-based. Finish with command 0. Commands print results without menu prompts so batch sample input works in the compiler.

UNDERSTAND THE IDEA

Explanation

This is a circular singly linked list: every node has one next pointer, and tail->next points back to head. There is no NULL link at the end. An empty list has head = tail = NULL.

Traversal checks for an empty list first, visits the head once, and uses do-while to stop when the pointer returns to head. Testing current != NULL would never terminate for a valid non-empty circular list.

Insertion and deletion must preserve the circle. Changing the head also changes tail->next. Deleting the only node clears both head and tail; the program never follows a link through a freed node.

Picture the links

head → [10] → [20] → [30] ← tail; tail.next points back to head

Nodes are separate allocations; arrows represent stored pointer links.

PLAN BEFORE CODING

Algorithm

  1. Start with head and tail NULL and count zero.
  2. Read one command and any arguments listed in the command table.
  3. Call createList, insertAt, deleteAt or traverse.
  4. Validate positions before allocating or dereferencing nodes.
  5. Update head, tail, count and neighbour links for empty/end cases.
  6. On command 0 or invalid input, release all remaining nodes.

CONTROL THE PROGRAM

Commands and input

A command is an operation number followed by its arguments. Whitespace separates tokens; line breaks make your input easier to read. No interactive menu prompts are printed.

InputMeaning
1 n valuesCreate a new list; n = 0 creates an empty list. Replaces the previous list.
2 position valueInsert at a one-based position from 1 to current size + 1.
3 positionDelete a one-based position from 1 to current size.
4Display from head to last node.
0Exit and free all remaining nodes.

SEE THE CONTROL FLOW

Flowcharts

Follow arrows from Start. Diamonds are decisions; Yes/No labels select a branch. Returning arrows repeat a loop. Function steps group the pointer/index updates explained in the code. Expand the operation charts for more detail. On a phone, scroll sideways within a diagram.

Program command loop

Circular singly linked list: Program command loop

Open full-size flowchart ↗

Insertion at a position
Circular singly linked list: Insertion at a position

Open full-size flowchart ↗

Deletion at a position
Circular singly linked list: Deletion at a position

Open full-size flowchart ↗

Traversal
Circular singly linked list: Traversal

Open full-size flowchart ↗

C17

Complete C program

Download .c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

/* Read a complete integer token; reject bad or oversized input safely. */
int readInt(int *value) {
    char token[64];
    int ch;
    size_t used = 0;
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;
    do {
        if (used < sizeof token - 1) token[used++] = (char)ch;
        else {
            while ((ch = getchar()) != EOF && !isspace((unsigned char)ch)) { }
            return 0;
        }
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    token[used] = '\0';
    char *end;
    errno = 0;
    long result = strtol(token, &end, 10);
    if (errno || *end != '\0' || result < -1000000 || result > 1000000) return 0;
    *value = (int)result;
    return 1;
}

typedef struct Node {
    int data;
    struct Node *next; 
} Node;
typedef struct {
    Node *head, *tail;
    int count;
} List;

void clearList(List *list) {
    Node *current = list->head;
    for (int i = 0; i < list->count; ++i) {
        Node *next = current->next;
        free(current);
        current = next;
    }
    list->head = list->tail = NULL;
    list->count = 0;
}

/* Positions are 1-based. Return 1 on success, 0 on failure. */
int insertAt(List *list, int position, int value) {
    if (position < 1 || position > list->count + 1) return 0;
    Node *node = malloc(sizeof *node);
    if (node == NULL) return 0;
    node->data = value;
    node->next = NULL; 
    if (list->count == 0) {
        list->head = list->tail = node; node->next = node;
    } else if (position == 1) {
        node->next = list->head; 
        list->head = node; list->tail->next = list->head;
    } else if (position == list->count + 1) {
        
        list->tail->next = node;
        list->tail = node; node->next = list->head;
    } else {
        Node *previous = list->head;
        for (int i = 1; i < position - 1; ++i) previous = previous->next;
        node->next = previous->next; 
        previous->next = node;
    }
    ++list->count;
    return 1;
}

int deleteAt(List *list, int position, int *removed) {
    if (position < 1 || position > list->count) return 0;
    Node *node;
    if (position == 1) {
        node = list->head;
        if (list->count == 1) list->head = list->tail = NULL;
        else {
            list->head = node->next; list->tail->next = list->head;
        }
    } else {
        Node *previous = list->head;
        for (int i = 1; i < position - 1; ++i) previous = previous->next;
        node = previous->next;
        previous->next = node->next; 
        if (node == list->tail) list->tail = previous;
    }
    *removed = node->data;
    free(node);
    --list->count;
    return 1;
}

int createList(List *list, const int values[], int n) {
    List fresh = {NULL, NULL, 0};
    for (int i = 0; i < n; ++i)
        if (!insertAt(&fresh, fresh.count + 1, values[i])) {
            clearList(&fresh);
            return 0;
        }
    clearList(list);
    *list = fresh;
    return 1;
}

void traverse(const List *list) {
    printf("List:");
    if (list->head != NULL) {
        const Node *p = list->head;
        do { printf(" %d", p->data); p = p->next; } while (p != list->head);
    }
    putchar('\n');
}

/* 1 n values: create; 2 position value: insert; 3 position: delete;
   4: display; 0: exit. */
int main(void) {
    List list = {NULL, NULL, 0};
    int command;
    while (readInt(&command)) {
        if (command == 0) { clearList(&list); return 0; }
        if (command == 1) {
            int n, values[100];
            if (!readInt(&n) || n < 0 || n > 100) goto invalid;
            for (int i = 0; i < n; ++i) if (!readInt(&values[i])) goto invalid;
            puts(createList(&list, values, n) ? "Created." : "Allocation failed; list unchanged.");
        } else if (command == 2) {
            int position, value;
            if (!readInt(&position) || !readInt(&value)) goto invalid;
            puts(insertAt(&list, position, value) ? "Inserted." : "Insertion failed: invalid position or allocation failure.");
        } else if (command == 3) {
            int position, value;
            if (!readInt(&position)) goto invalid;
            if (deleteAt(&list, position, &value)) printf("Deleted: %d\n", value);
            else puts("Invalid deletion position.");
        } else if (command == 4) traverse(&list);
        
        else goto invalid;
    }
invalid:
    puts("Invalid or incomplete input. End with command 0.");
    clearList(&list);
    return 1;
}
Open in compiler ↗

Code loads into your existing compiler. Enter the sample input there; sign-in and execution rules stay the same.

Compile and run locally
gcc -std=c17 circular-linked-list.c -o lab
./lab

On Windows, run .\lab.exe after compiling with GCC.

UNDERSTAND THE CODE

Line-by-line explanation

These steps follow the exact source above, including its input helper, operations, command handling and cleanup. Line numbers count blank lines; related statements are grouped. Start with main to follow execution, then open the functions it calls.

Start with main → · Data structure and operations →

Line 1Include terminal input and output
#include <stdio.h>

stdio.h declares printf, puts, getchar and putchar. The input helper reads characters with getchar; the main program prints operation results using the output functions.

Line 2Include conversion and memory utilities
#include <stdlib.h>

stdlib.h declares strtol for converting an integer token. It also declares malloc and free, which the linked structures and tree programs use to obtain and release node storage.

Line 3Include string and memory utilities
#include <string.h>

string.h declares utilities such as memset. It is part of the shared header list; merely including a header does not call any function or change stored values.

Lines 4–5Include character tests and conversion error reporting
#include <ctype.h>
#include <errno.h>

ctype.h supplies isspace for recognising whitespace. errno.h supplies errno so the program can detect a range error reported by strtol.

Lines 7–8Define a checked integer-input helper
/* Read a complete integer token; reject bad or oversized input safely. */
int readInt(int *value) {

The comment states the purpose of readInt. Its int *value parameter receives the address of a caller variable. The helper returns 1 after storing a valid value there, or 0 on failure; these flags are separate from main’s process exit status.

Lines 9–11Reserve a token buffer and its reading state
    char token[64];
    int ch;
    size_t used = 0;

token holds up to 63 input characters and a null terminator. ch is int so it can distinguish character input from EOF. used starts at zero and counts the stored token characters.

Lines 12–13Skip leading whitespace and handle end-of-file
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;

getchar reads one character at a time. The loop discards whitespace before the token. The unsigned-char cast supplies an allowed argument to isspace. If EOF is reached before a token starts, return 0 without changing the caller variable.

Lines 14–15Store token characters while space remains
    do {
        if (used < sizeof token - 1) token[used++] = (char)ch;

The do-while body handles the first non-whitespace character already read. The size check leaves one buffer slot for a null terminator. used++ uses the old index for storage, then advances the count.

Lines 16–19Reject a token that is too long
        else {
            while ((ch = getchar()) != EOF && !isspace((unsigned char)ch)) { }
            return 0;
        }

When the buffer has no character space left, consume the rest of that token through whitespace or EOF, then return failure. The program does not silently treat a truncated prefix as the full input number.

Lines 20–21Read the next character and decide whether the token continues
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));

Keep collecting until whitespace or EOF. The terminating whitespace has been consumed by getchar; it is not stored as part of the token.

Lines 22–24Terminate the string and prepare a checked conversion
    token[used] = '\0';
    char *end;
    errno = 0;

Write the null terminator after the stored token. end will identify where conversion stopped. Reset errno before strtol so a previous library error cannot be mistaken for this conversion’s result.

Line 25Convert the complete token as a decimal integer
    long result = strtol(token, &end, 10);

strtol reads token in base 10 and returns a long. Passing &end lets it write the address of the first unconverted character. A sign such as - is allowed as part of a valid integer token.

Line 26Reject conversion errors, leftover characters and out-of-range values
    if (errno || *end != '\0' || result < -1000000 || result > 1000000) return 0;

A nonzero errno rejects a reported conversion error. *end must be the null terminator, so input such as 12x is rejected rather than accepted as 12. The allowed value range is -1,000,000 to 1,000,000.

Lines 27–29Store the accepted integer through its address
    *value = (int)result;
    return 1;
}

After validation, cast result to int and assign it through *value. Return 1 for success. The brace ends readInt. The caller variable remains unchanged on the earlier failure returns.

Lines 31–34Define one list node
typedef struct Node {
    int data;
    struct Node *next; 
} Node;

Node is a short typedef name for the self-referential structure. data stores the integer. next identifies the following node. In this circular representation, the final node links back to head.

Lines 35–38Keep the list endpoints and node count together
typedef struct {
    Node *head, *tail;
    int count;
} List;

The List structure stores head, tail and count. Passing List * to an operation lets that operation change the caller’s endpoint pointers and count, rather than only local copies.

Lines 40–41Begin releasing the list from its head
void clearList(List *list) {
    Node *current = list->head;

clearList receives the actual list state. current initially identifies the head node; an empty list has count zero and no nodes to visit.

Lines 42–46Save the next address before freeing the current node
    for (int i = 0; i < list->count; ++i) {
        Node *next = current->next;
        free(current);
        current = next;
    }

Repeat once for each counted node. Read current->next while current is still alive, free current, then continue using the saved address. Do not read current->next after free. Counting nodes stops after one circuit; a circular list does not end at a null next link.

Lines 47–49Reset the list after cleanup
    list->head = list->tail = NULL;
    list->count = 0;
}

Set both endpoints to NULL and count to zero. The empty state prevents later operations from following a link to released storage.

Lines 51–53Define insertion with one-based positions
/* Positions are 1-based. Return 1 on success, 0 on failure. */
int insertAt(List *list, int position, int value) {
    if (position < 1 || position > list->count + 1) return 0;

The comment records the return convention. An insertion position must lie from 1 through count + 1. Position 1 means the front and count + 1 means append. Invalid positions return 0 without allocating or changing links.

Lines 54–57Allocate and initialise the new node
    Node *node = malloc(sizeof *node);
    if (node == NULL) return 0;
    node->data = value;
    node->next = NULL; 

malloc(sizeof *node) reserves space for one complete Node. If allocation fails, return 0 before changing the old list. Set data to value and initialise the link fields. next starts as NULL until the chosen branch connects it.

Lines 58–59Insert the first node into an empty list
    if (list->count == 0) {
        list->head = list->tail = node; node->next = node;

The new node becomes both head and tail. Set node->next to node itself so the one-node list already forms a circle.

Lines 60–62Insert before the current head
    } else if (position == 1) {
        node->next = list->head; 
        list->head = node; list->tail->next = list->head;

Link the new node to the old head before replacing head. The old chain stays reachable through node->next. Finally make tail->next identify the new head, preserving the circular link.

Lines 63–66Append after the current tail
    } else if (position == list->count + 1) {
        
        list->tail->next = node;
        list->tail = node; node->next = list->head;

Link the old tail to the new node, then update the tail variable. Set the new tail’s next to head to close the circle again.

Lines 67–69Find the predecessor for a middle insertion
    } else {
        Node *previous = list->head;
        for (int i = 1; i < position - 1; ++i) previous = previous->next;

For positions strictly between the endpoints, start previous at head and move until it identifies position - 1. This is the node whose next link must change.

Lines 70–72Reconnect the middle without losing the suffix
        node->next = previous->next; 
        previous->next = node;
    }

Set the new node’s next to the old successor before changing previous->next to node. This order preserves the address of the remainder of the list.

Lines 73–75Count the inserted node and report success
    ++list->count;
    return 1;
}

Increment count only after the new node has been fully connected. Return 1 to tell the caller that insertion succeeded.

Lines 77–79Validate the requested deletion position
int deleteAt(List *list, int position, int *removed) {
    if (position < 1 || position > list->count) return 0;
    Node *node;

A deletion position must be from 1 through count; an empty list has no valid deletion. node will remember the exact node that must be disconnected and freed. removed is an output pointer for returning its stored integer.

Lines 80–82Handle deletion at the head
    if (position == 1) {
        node = list->head;
        if (list->count == 1) list->head = list->tail = NULL;

Save the old head in node before changing any endpoint. If it is the only node, both head and tail become NULL.

Lines 83–86Reconnect a nonempty remainder after removing the head
        else {
            list->head = node->next; list->tail->next = list->head;
        }
    } else {

Move head to node->next when other nodes remain. Also update tail->next to the new head so the circle no longer points at the removed node. The final brace/else leads to the separate non-head case.

Lines 87–89Find and save the non-head target
        Node *previous = list->head;
        for (int i = 1; i < position - 1; ++i) previous = previous->next;
        node = previous->next;

Walk previous to the node before the requested position, then save previous->next in node. The original list stays connected until both pointers have been obtained.

Lines 90–92Bypass the target and repair tail when necessary
        previous->next = node->next; 
        if (node == list->tail) list->tail = previous;
    }

Set previous->next to node->next. If the target was the tail, previous becomes the new tail. Its next already points to head after the bypass, so the circle is preserved.

Lines 93–94Return the removed value before releasing storage
    *removed = node->data;
    free(node);

Copy node->data through the output pointer. Then free node. The integer result is independent of the released node and can safely be printed by the caller.

Lines 95–97Update the count and finish deletion
    --list->count;
    return 1;
}

Decrease count once for the successful removal, then return 1. Rejected requests returned earlier without changing the list.

Lines 99–100Build a replacement list separately
int createList(List *list, const int values[], int n) {
    List fresh = {NULL, NULL, 0};

createList receives the current list, an input array and n. fresh starts empty. Building in a separate List allows allocation failure to leave the current list unchanged.

Lines 101–105Append each input value to the temporary list
    for (int i = 0; i < n; ++i)
        if (!insertAt(&fresh, fresh.count + 1, values[i])) {
            clearList(&fresh);
            return 0;
        }

Use fresh.count + 1 as the append position. If any insertion fails, clear the partial fresh list and return 0. The old list has not been cleared yet, so it remains available.

Lines 106–109Commit the completed replacement
    clearList(list);
    *list = fresh;
    return 1;
}

After all n nodes have been created, clear the old list. Assign the whole fresh state into *list, including both endpoints and count, then return 1. This copies the small wrapper; the allocated nodes remain alive after the local fresh wrapper ends. For n = 0, the replacement is an empty list.

Lines 111–113Begin circular traversal only if a node exists
void traverse(const List *list) {
    printf("List:");
    if (list->head != NULL) {

Print the label. The head check prevents trying to print a node in an empty list; the circular loop has no null endpoint to detect emptiness on its own.

Lines 114–116Visit one complete circle
        const Node *p = list->head;
        do { printf(" %d", p->data); p = p->next; } while (p != list->head);
    }

Start p at head and use do-while to print each node before advancing. Stop when p returns to head. Testing p != NULL here would never end for a valid nonempty circular list.

Lines 117–118Finish the displayed line
    putchar('\n');
}

putchar prints a newline after traversal or after the label of an empty list. The brace ends traverse.

Lines 120–122Document the commands and start main
/* 1 n values: create; 2 position value: insert; 3 position: delete;
   4: display; 0: exit. */
int main(void) {

The comment lists the command number and any following arguments. main is the program entry function and returns an integer exit status.

Lines 123–124Create an empty list state
    List list = {NULL, NULL, 0};
    int command;

Both endpoint pointers begin as NULL and count begins at zero. command stores the next requested operation.

Lines 125–126Read commands until exit or invalid input
    while (readInt(&command)) {
        if (command == 0) { clearList(&list); return 0; }

The loop continues while readInt succeeds. Command 0 clears all nodes and returns success immediately. Running out of input without the explicit exit command is handled by the error path below.

Lines 127–130Read a create request and its array values
        if (command == 1) {
            int n, values[100];
            if (!readInt(&n) || n < 0 || n > 100) goto invalid;
            for (int i = 0; i < n; ++i) if (!readInt(&values[i])) goto invalid;

For command 1, n must be from 0 through 100. Read exactly n integers into the temporary values array. A missing or rejected integer jumps to the shared invalid label.

Line 131Create the replacement and print its status
            puts(createList(&list, values, n) ? "Created." : "Allocation failed; list unchanged.");

The conditional expression selects the success or allocation-failure message using createList’s result. The helper’s temporary-list design preserves the current list if allocation fails.

Lines 132–135Handle insertion at a requested position
        } else if (command == 2) {
            int position, value;
            if (!readInt(&position) || !readInt(&value)) goto invalid;
            puts(insertAt(&list, position, value) ? "Inserted." : "Insertion failed: invalid position or allocation failure.");

For command 2, read position and value. A token-reading failure uses the shared error path. insertAt reports whether the position and allocation were acceptable; puts then prints the corresponding message.

Lines 136–140Handle deletion and print the returned value
        } else if (command == 3) {
            int position, value;
            if (!readInt(&position)) goto invalid;
            if (deleteAt(&list, position, &value)) printf("Deleted: %d\n", value);
            else puts("Invalid deletion position.");

For command 3, read a position. Pass &value as output storage to deleteAt. Print that integer only when deletion succeeds; otherwise report an invalid position without using an unassigned value.

Line 141Display the list for command 4
        } else if (command == 4) traverse(&list);

traverse prints the nodes in their forward logical order without removing any node.

Lines 143–144Reject an unsupported command
        else goto invalid;
    }

A command not handled by the previous branches jumps to invalid. The brace ends the reading loop.

Lines 145–149Use one error path to report failure and release nodes
invalid:
    puts("Invalid or incomplete input. End with command 0.");
    clearList(&list);
    return 1;
}

The invalid label is the destination of the earlier goto statements. It is also reached if the command loop ends without command 0. Print the message, clear the list and return failure.

FOLLOW THE STATE

Dry run

StepOperation / stateResult
Create 10, 20, 30Append in order10 → 20 → 30
Insert 15 at position 2Find predecessor 10 and reconnect10 → 15 → 20 → 30
Delete position 3Remove 20 and free its node10 → 15 → 30

CHECK THE BEHAVIOUR

Sample input & output

These outputs were produced by compiling and running this exact source. Inputs are provided in the stated order; the programs do not print repeated menu prompts.

Sample 1

INPUT
1 3 10 20 30
4
2 2 15
4
3 3
4
0
OUTPUT
Created.
List: 10 20 30
Inserted.
List: 10 15 20 30
Deleted: 20
List: 10 15 30

Sample 2

INPUT
4
3 1
2 1 7
4
3 1
4
0
OUTPUT
List:
Invalid deletion position.
Inserted.
List: 7
Deleted: 7
List:

Sample 3

INPUT
1 2 -5 9
2 1 0
2 4 12
3 4
4
1 0
4
0
OUTPUT
Created.
Inserted.
Inserted.
Deleted: 12
List: 0 -5 9
Created.
List:

Common mistakes

  • Do not dereference an empty head.
  • Reconnect neighbours before freeing the node.
  • Update tail when removing the last node.
  • Stop on returning to head, not NULL.

WHY THIS GROWTH RATE?

Time and space complexity

Creation O(m + n); insertion O(1) at head/tail, O(n) in the middle; deletion O(1) at head, O(n) elsewhere; traversal O(n). Stored nodes O(n).

With maintained head and tail pointers, either-end insertion and head deletion update a fixed number of links: O(1). Finding a predecessor for a middle or tail deletion can require scanning n nodes, giving O(n). A circle does not make that predecessor directly accessible.

Traversal stops after one complete cycle, so it visits exactly n nodes: O(n). Creation appends k nodes in O(k), and freeing the previous m-node list adds O(m). Cleanup uses the stored count to visit each allocated node once.

Node storage is O(n). The operations use a few temporary pointers and a counter, giving O(1) additional working space. Allocation/free is treated as constant cost in this introductory model.

Big-O describes growth in the stated input quantity. Fixed factors and lower-order terms are omitted. Supported input bounds keep the educational examples practical; operation costs depend on the actual representation and functions shown.

PREPARE FOR YOUR LAB VIVA

Related viva questions & answers

6 focused questions

Try answering aloud, then expand the answer to check your reasoning. These questions focus on the concepts used in this program.

What link identifies a nonempty list as circular in this program?

Tail->next points back to head instead of NULL. This closes the ring, so following next repeatedly would revisit the first node rather than naturally stop.

Why does circular-list traversal use a do-while loop with a return-to-head test?

A nonempty traversal must print head once before checking whether it has come back to head. The loop stops after one full circuit, avoiding both skipping the first node and looping forever.

What should the next pointer contain when a circular list has exactly one node?

The node's next points to itself, and both head and tail identify that node. A self-link is the correct one-node ring rather than an error.

Why does inserting a new circular head also require updating tail->next?

Tail closes the ring by pointing to the first node. Once head changes, tail->next must point to the new head or the new node will not be included in the complete circular path.

What distinguishes deleting the sole circular node from deleting the head of a larger ring?

For a sole node, the ring becomes empty and both endpoints are cleared. In a larger ring, head advances to its successor and tail->next is redirected to the surviving head before the old node is released.

Why does cleanup use a known node count instead of waiting for NULL?

A valid ring never reaches NULL. Visiting exactly count nodes bounds cleanup to one circuit; each next pointer is saved before freeing its node, and the loop never dereferences the freed original head.