Skip to lab content

EXPERIMENT 01 · DATA STRUCTURES LAB

Singly linked list

SOLUTION 01C173 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 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

A linked list stores values in separate nodes. Each node holds data and a next pointer. head points to the first node; tail points to the last; the last next pointer is NULL. The count field makes size and position validation immediate.

Creation appends each supplied value through a function. Insertion changes links to include a newly allocated node. Deletion first reconnects the remaining nodes, then frees the removed node. Traversal follows next until NULL.

For example, inserting 15 at position 2 changes 10 → 20 → 30 into 10 → 15 → 20 → 30. This program deletes by position, not by matching a value. An empty list and a one-node list are handled separately.

Picture the links

head → [10 | next] → [20 | next] → [30 | NULL] ← tail

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

Singly linked list: Program command loop

Open full-size flowchart ↗

Insertion at a position
Singly linked list: Insertion at a position

Open full-size flowchart ↗

Deletion at a position
Singly linked list: Deletion at a position

Open full-size flowchart ↗

Traversal
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; 
    } else if (position == 1) {
        node->next = list->head; 
        list->head = node; 
    } else if (position == list->count + 1) {
        
        list->tail->next = node;
        list->tail = node; 
    } 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; 
        }
    } 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:");
    for (const Node *p = list->head; p != NULL; p = p->next) printf(" %d", p->data);
    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 singly-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. The final node’s next is NULL.

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. The loop releases each node exactly once.

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; 

The new node becomes both head and tail. There is no old endpoint to reconnect.

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

Link the new node to the old head before replacing head. The old chain stays reachable through node->next.

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

Link the old tail to the new node, then update the tail variable. The new tail’s next remains NULL.

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; 
        }
    } else {

Move head to node->next when other nodes remain. The old head stays available in node until its value is saved and it is freed. 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. The new tail’s next is NULL when the removed node was last.

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–115Traverse the linear list without modifying it
void traverse(const List *list) {
    printf("List:");
    for (const Node *p = list->head; p != NULL; p = p->next) printf(" %d", p->data);
    putchar('\n');
}

Start a read-only traversal pointer at head. Print the label, then each data value while p is not NULL, following p->next after each visit. A null next marks the end. Print a newline after the final value.

Lines 117–119Document 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 120–121Create 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 122–123Read 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 124–127Read 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 128Create 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 129–132Handle 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 133–137Handle 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 138Display 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 140–141Reject an unsupported command
        else goto invalid;
    }

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

Lines 142–146Use 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.
  • Do not lose the remaining list when changing head.

WHY THIS GROWTH RATE?

Time and space complexity

Creation O(m + n); insertion O(1) at either end and O(n) at a middle position; deletion O(1) at the head and O(n) elsewhere; traversal O(n). Stored nodes O(n).

Let n be the current list size. A middle operation walks through up to n links to find the predecessor; changing two links after that is O(1). The tail and count fields allow insertion at the end without a scan. Deleting the tail still requires finding its predecessor, so it is O(n).

Head insertion and deletion touch only a fixed number of links, giving O(1) structural work. Traversal visits all n nodes once: O(n). Creating k new nodes uses k constant-time tail appends; replacing an old m-node list also frees m nodes, so creation is O(m + k).

The list occupies O(n) node storage. Traversal and individual modifications use O(1) extra scalar space, apart from the inserted node. During replacement, the old and new lists coexist until allocation succeeds. This introductory analysis treats allocation/free as constant cost; allocator internals can vary.

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.

Why does deleting the last node of a singly linked list require finding its predecessor?

A node stores only its next link, so tail cannot directly move backward. The program walks from head to find the node whose next points to tail, then makes that predecessor the new tail.

How is insertion at position 1 different from insertion in the middle?

At position 1, the new node points to the old head and becomes head. A middle insertion first locates the predecessor, then links the new node between that predecessor and its old successor.

Why can appending use the stored tail pointer without traversing the list?

Tail already identifies the final node. The program connects tail->next to the new node and updates tail, avoiding a scan from head when appending.

What must head, tail and count become after deleting the only node?

Both head and tail must become NULL, and count must become zero. Leaving tail pointing to the freed node would create a dangling pointer even though head correctly indicates an empty list.

Why does createList build a fresh list before replacing the existing one?

If allocation fails halfway through creation, the fresh partial list can be freed while the old list remains usable. Only after successful construction does the program free the old list and install the new one.

Does traversing a singly linked list allow direct access to its fifth node?

No. Unlike an array index, a linked position is reached by following links from a known node. Reaching position 5 from head requires four next-link steps, and general position access is O(n).