THE SYLLABUS TASK
What you need to solve
Write a program that uses functions to perform creation, insertion, deletion and traversal on a doubly linked list.
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
Each node has both next and prev pointers. Moving next goes toward the tail; moving prev goes toward the head. head->prev and tail->next are NULL for a non-empty list.
Insertion connects the new node on both sides: predecessor.next and successor.prev must agree. Deletion reconnects both neighbours before freeing the node. A separate reverse-traversal function demonstrates why prev is useful.
The program locates a middle position by walking from the head. Having a prev pointer does not automatically make finding a numbered position constant time. Head/tail/count are maintained after every operation.
NULL ← [10] ⇄ [20] ⇄ [30] → NULL; head = 10, tail = 30
Nodes are separate allocations; arrows represent stored pointer links.
PLAN BEFORE CODING
Algorithm
- Start with head and tail NULL and count zero.
- Read one command and any arguments listed in the command table.
- Call createList, insertAt, deleteAt or traverse.
- Validate positions before allocating or dereferencing nodes.
- Update head, tail, count and neighbour links for empty/end cases.
- 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.
| Input | Meaning |
|---|---|
1 n values | Create a new list; n = 0 creates an empty list. Replaces the previous list. |
2 position value | Insert at a one-based position from 1 to current size + 1. |
3 position | Delete a one-based position from 1 to current size. |
4 | Display from head to last node. |
5 | Display backward from tail to head. |
0 | Exit 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
#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; struct Node *prev;
} 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; node->prev = NULL;
if (list->count == 0) {
list->head = list->tail = node;
} else if (position == 1) {
node->next = list->head; list->head->prev = node;
list->head = node;
} else if (position == list->count + 1) {
node->prev = list->tail;
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; node->prev = previous; node->next->prev = node;
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->head->prev = NULL;
}
} else {
Node *previous;
if (position == list->count) previous = list->tail->prev;
else {
previous = list->head;
for (int i = 1; i < position - 1; ++i) previous = previous->next;
}
node = previous->next;
previous->next = node->next; if (node->next != NULL) node->next->prev = previous;
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');
}
void traverseReverse(const List *list) {
printf("Reverse:");
for (const Node *p = list->tail; p != NULL; p = p->prev) printf(" %d", p->data);
putchar('\n');
}
/* 1 n values: create; 2 position value: insert; 3 position: delete;
4: display; 5: reverse 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 if (command == 5) traverseReverse(&list);
else goto invalid;
}
invalid:
puts("Invalid or incomplete input. End with command 0.");
clearList(&list);
return 1;
}
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 doubly-linked-list.c -o lab
./labOn 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; struct Node *prev;
} Node;Node is a short typedef name for the self-referential structure. data stores the integer. next identifies the following node. prev also identifies the preceding node, enabling backward traversal.
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; node->prev = 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. Both next and prev start as NULL.
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->prev = node;
list->head = node; Link the new node to the old head before replacing head. The old head’s prev must also point back to the new node.
Lines 63–66Append after the current tail
} else if (position == list->count + 1) {
node->prev = list->tail;
list->tail->next = node;
list->tail = node; Link the old tail to the new node, then update the tail variable. Set the new node’s prev to the old tail so the backward link is preserved. 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; node->prev = previous; node->next->prev = node;
previous->next = node;
}Set the new node’s next to the old successor before changing previous->next to node. Also set node->prev to previous and the old successor’s prev to node. Both traversal directions must agree.
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 deletion and reserve a target pointer
int deleteAt(List *list, int position, int *removed) {
if (position < 1 || position > list->count) return 0;
Node *node;Reject positions outside 1 through count. node will remember the removed node so its data can be returned and its storage freed after reconnection.
Lines 80–85Delete the head and repair its successor’s backward link
if (position == 1) {
node = list->head;
if (list->count == 1) list->head = list->tail = NULL;
else {
list->head = node->next; list->head->prev = NULL;
}Save the old head. A one-node deletion clears both endpoints. Otherwise move head to the next node and set its prev to NULL, because a linear list’s first node has no predecessor.
Lines 86–92Locate the predecessor for a non-head deletion
} else {
Node *previous;
if (position == list->count) previous = list->tail->prev;
else {
previous = list->head;
for (int i = 1; i < position - 1; ++i) previous = previous->next;
}When deleting the tail, tail->prev provides the predecessor directly. For other non-head positions, walk forward from head to position - 1. The brace ends this search branch.
Lines 93–96Bypass the removed node in both directions
node = previous->next;
previous->next = node->next; if (node->next != NULL) node->next->prev = previous;
if (node == list->tail) list->tail = previous;
}Save previous->next in node, then link previous to node->next. If a successor exists, set its prev to previous. If node was tail, update tail to previous. These repairs keep forward and reverse traversal consistent.
Lines 97–101Save the data, free the node and reduce the count
*removed = node->data;
free(node);
--list->count;
return 1;
}Assign node->data through removed before freeing node. Decrement count, return success and close deleteAt. The caller receives a copied integer, not an address into freed storage.
Lines 103–104Build 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 105–109Append 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 110–113Commit 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 115–119Display from head to tail
void traverse(const List *list) {
printf("List:");
for (const Node *p = list->head; p != NULL; p = p->next) printf(" %d", p->data);
putchar('\n');
}Follow next links from head until NULL, printing each integer. The const Node pointer prevents modification through that traversal access. Finish with a newline.
Lines 120–124Display from tail to head
void traverseReverse(const List *list) {
printf("Reverse:");
for (const Node *p = list->tail; p != NULL; p = p->prev) printf(" %d", p->data);
putchar('\n');
}Start at tail and follow prev links until NULL. This produces the reverse order without changing the list or allocating a reverse copy.
Lines 125–127Document the commands and start main
/* 1 n values: create; 2 position value: insert; 3 position: delete;
4: display; 5: reverse 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 128–129Create 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 130–131Read 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 132–135Read 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 136Create 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 137–140Handle 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 141–145Handle 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 146Display 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 147–149Handle reverse display or an unsupported command
else if (command == 5) traverseReverse(&list);
else goto invalid;
}Command 5 calls traverseReverse. Any remaining command jumps to invalid. The closing brace ends the command loop.
Lines 150–154Report malformed or incomplete input and clean up
invalid:
puts("Invalid or incomplete input. End with command 0.");
clearList(&list);
return 1;
}invalid is a label, not another function. All goto invalid branches arrive here. Print the message, release every remaining list node, then return failure from main.
FOLLOW THE STATE
Dry run
| Step | Operation / state | Result |
|---|---|---|
| Create 10, 20, 30 | Append in order | 10 → 20 → 30 |
| Insert 15 at position 2 | Find predecessor 10 and reconnect | 10 → 15 → 20 → 30 |
| Delete position 3 | Remove 20 and free its node | 10 → 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
1 3 10 20 30
4
2 2 15
4
5
3 3
4
0
Created.
List: 10 20 30
Inserted.
List: 10 15 20 30
Reverse: 30 20 15 10
Deleted: 20
List: 10 15 30
Sample 2
4
3 1
2 1 7
4
3 1
4
0
List:
Invalid deletion position.
Inserted.
List: 7
Deleted: 7
List:
Sample 3
1 2 -5 9
2 5
1 0
2 4 12
3 4
4
5
1 0
4
0
Created.
Insertion failed: invalid position or allocation failure.
Common mistakes
- Do not dereference an empty head.
- Reconnect neighbours before freeing the node.
- Update tail when removing the last node.
- Update both next and prev pointers.
WHY THIS GROWTH RATE?
Time and space complexity
Creation O(m + n); insertion and deletion O(1) at either end, O(n) at middle positions; forward/reverse traversal O(n). Stored nodes O(n).
Head and tail pointers give direct access to both ends. Tail deletion can use tail->prev directly, so end deletion is O(1), unlike the singly linked version. Updating the neighbouring next and prev links takes a fixed number of assignments.
A middle insertion/deletion still scans from the head to its one-based position, costing O(n) in the worst case. Forward and reverse traversals visit n nodes and therefore each take O(n). Creating k new nodes and freeing m old nodes costs O(m + k).
Each node stores two pointers rather than one, but the number of nodes remains proportional to n, so node storage is O(n). The algorithms are iterative and need O(1) additional scalar working space. Allocation/free is treated as constant cost here.
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
Try answering aloud, then expand the answer to check your reasoning. These questions focus on the concepts used in this program.
Which two link directions must agree after a middle insertion?
The predecessor's next and the successor's prev must both reference the new node. The new node also stores those two neighbors; updating only one direction would leave inconsistent forward and reverse paths.
Why can deleting the tail of this doubly linked list take constant time?
Tail->prev directly identifies the preceding node. The program can move tail there and clear its next link without searching from head, unlike the singly linked implementation.
Why must the new head's prev be NULL after deleting the old head?
The new head has no preceding node. Clearing prev removes the reference to the freed old head and correctly marks the beginning for reverse-link operations.
How does reverse traversal work without reversing the list itself?
It starts at tail and repeatedly follows prev until NULL. Only the visiting direction changes; all next and prev links remain as stored.
Does having prev pointers make deletion by any numbered position O(1)?
No. Once a node is known, unlinking it takes constant link updates, but this program still locates an arbitrary middle position by walking from head. That locating step can take O(n).
What extra storage does a doubly linked node need compared with a singly linked node?
It stores a prev pointer in addition to data and next. That backward link supports reverse movement and direct tail deletion, at the cost of another pointer per node and more link maintenance.