0% found this document useful (0 votes)
0 views

Data Structures. Mod_3 .Docx

A linked list in C is a dynamic data structure consisting of nodes, each containing data and a pointer to the next node, allowing for efficient memory management and operations like insertion and deletion. Unlike arrays, linked lists can grow and shrink in size, require linear time for access, and do not waste memory due to fixed sizes. There are various types of linked lists, including singly, doubly, and circular linked lists, each with distinct characteristics and operations.

Uploaded by

2301109157cse
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
0 views

Data Structures. Mod_3 .Docx

A linked list in C is a dynamic data structure consisting of nodes, each containing data and a pointer to the next node, allowing for efficient memory management and operations like insertion and deletion. Unlike arrays, linked lists can grow and shrink in size, require linear time for access, and do not waste memory due to fixed sizes. There are various types of linked lists, including singly, doubly, and circular linked lists, each with distinct characteristics and operations.

Uploaded by

2301109157cse
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 15

What is a Linked List?

A linked list in C is a data structure that consists of a sequence of nodes, where each node
contains data and a pointer to the next node in the sequence. This structure allows for dynamic
memory allocation and efficient insertion and deletion of elements.

Differences Between Linked Lists and Arrays:

●​ Memory Allocation:​

○​ Arrays: Have a fixed size and allocate memory in a contiguous block. The size
must be defined at compile time.
○​ Linked Lists: Can grow and shrink dynamically, as nodes are allocated
individually using functions like malloc().
●​ Access Time:​

○​ Arrays: Allow for constant time access (O(1)) to elements using an index.
○​ Linked Lists: Require linear time (O(n)) to access elements, as you must
traverse from the head node to the desired node.
●​ Insertion/Deletion:​

○​ Arrays: Inserting or deleting elements can be costly, as it may require shifting


elements.
○​ Linked Lists: Insertion and deletion are more efficient, as they only require
updating the pointers of the nodes.

Structure of a Node in a Linked List:

In C, a node can be defined using a struct as follows:

1.​ struct Node {


2.​ int data; // The value stored in the node
3.​ struct Node* next; // Pointer to the next node
4.​ };

Key Components of a Linked List:


1.​ Head: A pointer to the first node in the linked list, which serves as the entry point.​

struct Node* head = NULL; // Initially, the list is empty.
2.​ Nodes: Each element in the linked list, containing data and a pointer to the next
node.​
3.​ Tail: The last node, which points to NULL, indicating the end of the list.

Advantages of Linked Lists over Arrays:

1.​ Dynamic Size:​

○​ Linked Lists: Can grow and shrink in size dynamically, allowing for efficient use
of memory.
○​ Arrays: Have a fixed size, which can lead to wasted memory or overflow if the
size is exceeded.
2.​ Efficient Insertions and Deletions:​

○​ Linked Lists: Inserting or deleting nodes can be done in constant time (O(1)) if
the pointer to the node is known, as it only requires updating pointers.
○​ Arrays: Inserting or deleting elements often requires shifting other elements,
leading to linear time complexity (O(n)).
3.​ No Memory Wastage:​

○​ Linked Lists: Allocate memory for each node as needed, minimizing wasted
space.
○​ Arrays: May allocate more memory than necessary, especially if the maximum
size is set too high.

Scenarios to Prefer a Linked List over an Array:

●​ Frequent Insertions/Deletions: When the application requires frequent insertions and


deletions of elements, linked lists are more efficient.
●​ Unknown Size: When the size of the data structure is not known in advance, linked lists
provide flexibility.
●​ Memory Management: In situations where memory fragmentation is a concern, linked
lists can help manage memory more effectively.

Limitations of Arrays that Linked Lists Can Overcome:

1.​ Fixed Size: Arrays require a predetermined size, which can lead to overflow or
underutilization of memory.
2.​ Costly Insertions/Deletions: Arrays require shifting elements during insertions and
deletions, which can be inefficient for large datasets.
3.​ Memory Contiguity: Arrays require contiguous memory allocation, which can be
problematic in fragmented memory environments. Linked lists can utilize scattered
memory locations.
Representation of Linked Lists in Memory:

●​ Nodes: Each node in a linked list is represented in memory as a separate block. Each
node contains:​

○​ Data: The value stored in the node.


○​ Pointer: A reference to the next node in the list.
●​ Memory Allocation: Nodes are typically allocated dynamically using functions like
malloc() in C. This means that each node can be located anywhere in memory, and
they are linked together through pointers.​

Concept of Pointers in Linked Lists:

●​ Pointers: A pointer is a variable that stores the memory address of another variable. In
the context of linked lists:
○​ Each node contains a pointer that points to the next node in the list.
○​ This allows for the traversal of the list by following the pointers from one node to
the next.

For example, in C, a node structure might look like this:

4.​ struct Node {


5.​ int data; // The value stored in the node
6.​ struct Node* next; // Pointer to the next node
7.​ };

Significance of the Head Pointer in a Linked List:

●​ Entry Point: The head pointer is crucial as it serves as the entry point to the linked list. It
points to the first node in the list.
●​ Traversal: To access or traverse the linked list, you start from the head pointer and
follow the next pointers until you reach the end (where the next pointer is NULL).
●​ List Operations: Operations such as insertion, deletion, and searching often rely on the
head pointer to locate the nodes.

Main Types of Linked Lists:

1.​ Singly Linked List


2.​ Doubly Linked List
3.​ Circular Linked List
Characteristics of a Singly Linked List:

●​ Structure: Each node contains two components:


○​ Data: The value stored in the node.
○​ Next Pointer: A pointer to the next node in the list.
●​ Traversal: Can only be traversed in one direction (from head to tail).
●​ Memory Efficiency: Requires less memory per node compared to doubly linked lists, as
it only stores one pointer.
●​ Insertion/Deletion: Efficient for adding or removing nodes at the beginning or end of the
list.

Differences Between a Doubly Linked List and a Singly Linked List:

●​ Structure:
○​ Doubly Linked List: Each node contains three components:
■​ Data: The value stored in the node.
■​ Next Pointer: A pointer to the next node.
■​ Previous Pointer: A pointer to the previous node.
●​ Traversal: Can be traversed in both directions (forward and backward).
●​ Memory Usage: Requires more memory per node due to the additional pointer.
●​ Insertion/Deletion: More flexible for operations, as you can easily access both the
previous and next nodes.

Circular Linked List:

●​ Definition: In a circular linked list, the last node points back to the first node instead of
pointing to NULL. This creates a circular structure.
●​ Types:
○​ Circular Singly Linked List: Each node points to the next node, and the last
node points back to the head.
○​ Circular Doubly Linked List: Each node has pointers to both the next and
previous nodes, with the last node pointing back to the head and the head
pointing to the last node.
●​ Functionality:
○​ Allows for continuous traversal of the list without reaching a null reference.
○​ Useful for applications that require a circular iteration, such as round-robin
scheduling.

Common Operations on Linked Lists:

1.​ Insertion: Adding a new node to the list.


2.​ Deletion: Removing a node from the list.
3.​ Traversal: Accessing each node in the list.
4.​ Searching: Finding a node with a specific value.
5.​ Updating: Modifying the data in a node.
6.​ Merging: Combining two linked lists into one.

Insertion in a Linked List:

The process of insertion can vary based on where you want to insert the node (beginning, end,
or middle).

1.​ At the Beginning:​

○​ Create a new node.


○​ Set the new node's next pointer to the current head.
○​ Update the head pointer to the new node.
1.​ void insertAtBeginning(struct Node** head, int newData) {
2.​ struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
3.​ newNode->data = newData;
4.​ newNode->next = *head;
5.​ *head = newNode;
6.​ }
2.​
3.​ At the End:​

○​ Create a new node.


○​ Traverse to the last node.
○​ Set the last node's next pointer to the new node.
7.​ void insertAtEnd(struct Node** head, int newData) {
8.​ struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
9.​ struct Node* last = *head;
10.​ newNode->data = newData;
11.​ newNode->next = NULL;
12.​
13.​ if (*head == NULL) {
14.​ *head = newNode;
15.​ return;
16.​ }
17.​
18.​ while (last->next != NULL) {
19.​ last = last->next;
20.​ }
21.​ last->next = newNode;
22.​}
4.​
5.​ In the Middle:​

○​ Traverse to the desired position.


○​ Adjust pointers to insert the new node.

Deleting a Node from a Linked List:

1.​ Identify the Node: Traverse the list to find the node to delete.
2.​ Adjust Pointers: Update the previous node's next pointer to skip the node to be deleted.
3.​ Free Memory: Deallocate the memory of the node.
23.​void deleteNode(struct Node** head, int key) {
24.​ struct Node* temp = *head, *prev = NULL;
25.​
26.​ if (temp != NULL && temp->data == key) {
27.​ *head = temp->next; // Change head
28.​ free(temp); // Free old head
29.​ return;
30.​ }
31.​
32.​ while (temp != NULL && temp->data != key) {
33.​ prev = temp;
34.​ temp = temp->next;
35.​ }
36.​
37.​ if (temp == NULL) return; // Key not found
38.​
39.​ prev->next = temp->next; // Unlink the node
40.​ free(temp); // Free memory
41.​}

Merging Two Linked Lists:

1.​ Create a New List: Initialize a new head pointer.


2.​ Compare Nodes: Traverse both lists, comparing nodes and adding the smaller node to
the new list.
3.​ Attach Remaining Nodes: If one list is exhausted, attach the remaining nodes of the
other list.
42.​struct Node* mergeLists(struct Node* list1, struct Node* list2) {
43.​ struct Node* merged = NULL;
44.​
45.​ if (list1 == NULL) return list2;
46.​ if (list2 == NULL) return list1;
47.​
48.​ if (list1->data < list2->data) {
49.​ merged = list1;
50.​ merged->next = mergeLists(list1->next, list2);
51.​ } else {
52.​ merged = list2;
53.​ merged->next = mergeLists(list1, list2->next);
54.​ }
55.​ return merged;
56.​}

Updating a Node in a Linked List:

1.​ Search for the Node: Traverse the list to find the node with the specified value.
2.​ Modify the Data: Update the data field of the node.
57.​void updateNode(struct Node* head, int oldData, int newData) {
58.​ struct Node* current = head;
59.​
60.​ while (current != NULL) {
61.​ if (current->data == oldData) {
62.​ current->data = newData; // Update the data
63.​ return;
64.​ }
65.​ current = current->next;
66.​ }
67.​}

Stack Representation Using a Linked List:

●​ Structure: A stack can be represented using a singly linked list where:​

○​ The top of the stack corresponds to the head of the linked list.
○​ Each node contains the data and a pointer to the next node.
●​ Operations:​

○​ Push: To add an element, create a new node, set its next pointer to the current
head, and update the head to the new node.
○​ Pop: To remove an element, check if the stack is empty. If not, store the head
node, update the head to the next node, and free the stored node.
1.​ struct Node {
2.​ int data;
3.​ struct Node* next;
4.​ };
5.​
6.​ void push(struct Node** top, int newData) {
7.​ struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
8.​ newNode->data = newData;
9.​ newNode->next = *top;
10.​ *top = newNode;
11.​}
12.​
13.​int pop(struct Node** top) {
14.​ if (*top == NULL) return -1; // Stack is empty
15.​ struct Node* temp = *top;
16.​ int poppedData = temp->data;
17.​ *top = (*top)->next;
18.​ free(temp);
19.​ return poppedData;
20.​}

Queue Representation Using a Linked List:

●​ Structure: A queue can be represented using a singly linked list where:​

○​ The front of the queue corresponds to the head of the linked list.
○​ The rear of the queue corresponds to the tail of the linked list.
●​ Operations:​

○​ Enqueue: To add an element, create a new node. If the queue is empty, set both
front and rear to the new node. Otherwise, set the current rear's next pointer to
the new node and update the rear pointer.
○​ Dequeue: To remove an element, check if the queue is empty. If not, store the
front node, update the front to the next node, and free the stored node.
21.​struct Node {
22.​ int data;
23.​ struct Node* next;
24.​};
25.​
26.​void enqueue(struct Node** front, struct Node** rear, int newData) {
27.​ struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
28.​ newNode->data = newData;
29.​ newNode->next = NULL;
30.​
31.​ if (*rear == NULL) {
32.​ *front = *rear = newNode; // Queue is empty
33.​ return;
34.​ }
35.​
36.​ (*rear)->next = newNode;
37.​ *rear = newNode;
38.​}
39.​
40.​int dequeue(struct Node** front) {
41.​ if (*front == NULL) return -1; // Queue is empty
42.​ struct Node* temp = *front;
43.​ int dequeuedData = temp->data;
44.​ *front = (*front)->next;
45.​ free(temp);
46.​ return dequeuedData;
47.​}

Advantages of Using Linked Lists for Stack and Queue Implementations:

●​ Dynamic Size: Linked lists can grow and shrink in size dynamically, allowing for efficient
memory usage without a predefined limit.
●​ No Wasted Space: Unlike arrays, linked lists do not require a fixed size, so there is no
wasted space when the stack or queue is not full.
●​ Efficient Insertions/Deletions: Both stacks and queues can perform push/pop or
enqueue/dequeue operations in constant time (O(1)) without the need for shifting
elements, as would be necessary in an array-based implementation.

Key Operations on Single Linked Lists:

1.​ Insertion:​

○​ At the beginning
○​ At the end
○​ At a specific position
2.​ Deletion:​

○​ From the beginning


○​ From the end
○​ A specific node by value
3.​ Traversal: Accessing each node in the list to read or display data.​
4.​ Searching: Finding a node with a specific value.​

5.​ Updating: Modifying the data in a node.​

6.​ Reversing: Changing the order of nodes in the list.​

7.​ Finding the Length: Counting the number of nodes in the list.​

Algorithm for Reversing a Single Linked List:

To reverse a single linked list, you can use an iterative approach. The idea is to change the next
pointers of the nodes so that they point to the previous node instead of the next one.

Algorithm:

1.​ Initialize three pointers: prev (set to NULL), current (set to head), and next (to store
the next node).
2.​ Traverse the list:
○​ Store the next node: next = current->next
○​ Reverse the current node's pointer: current->next = prev
○​ Move the prev and current pointers one step forward: prev = current,
current = next
3.​ After the loop, set the head to prev.

Code Example:

1.​ void reverseList(struct Node** head) {


2.​ struct Node* prev = NULL;
3.​ struct Node* current = *head;
4.​ struct Node* next = NULL;
5.​
6.​ while (current != NULL) {
7.​ next = current->next; // Store next node
8.​ current->next = prev; // Reverse the pointer
9.​ prev = current; // Move prev and current one step forward
10.​ current = next;
11.​ }
12.​ *head = prev; // Update head to the new front
13.​}

Finding the Middle Element of a Single Linked List:


To find the middle element, you can use the two-pointer technique (also known as the slow
and fast pointer approach).

Algorithm:

1.​ Initialize two pointers: slow and fast, both pointing to the head of the list.
2.​ Move slow one step at a time and fast two steps at a time.
3.​ When fast reaches the end of the list, slow will be at the middle.

Code Example:

14.​struct Node* findMiddle(struct Node* head) {


15.​ struct Node* slow = head;
16.​ struct Node* fast = head;
17.​
18.​ while (fast != NULL && fast->next != NULL) {
19.​ slow = slow->next; // Move slow by one
20.​ fast = fast->next->next; // Move fast by two
21.​ }
22.​ return slow; // Slow is now at the middle
23.​}

Operations on Double Linked Lists:

1.​ Insertion:​

○​ At the beginning
○​ At the end
○​ At a specific position
2.​ Deletion:​

○​ From the beginning


○​ From the end
○​ A specific node by value
3.​ Traversal:​

○​ Forward (from head to tail)


○​ Backward (from tail to head)
4.​ Searching: Finding a node with a specific value.​

5.​ Updating: Modifying the data in a node.​


6.​ Reversing: Changing the order of nodes in the list.​

7.​ Finding the Length: Counting the number of nodes in the list.​

Traversing a Double Linked List:

●​ Forward Traversal:​

○​ Start from the head node.


○​ Use a pointer to move to the next node until you reach the end (where the next
pointer is NULL).
●​ Backward Traversal:​

○​ Start from the tail node (the last node).


○​ Use a pointer to move to the previous node until you reach the head (where the
previous pointer is NULL).

Code Example for Traversal:

1.​ // Forward Traversal


2.​ void traverseForward(struct Node* head) {
3.​ struct Node* current = head;
4.​ while (current != NULL) {
5.​ printf("%d ", current->data);
6.​ current = current->next;
7.​ }
8.​ }
9.​
10.​// Backward Traversal
11.​void traverseBackward(struct Node* tail) {
12.​ struct Node* current = tail;
13.​ while (current != NULL) {
14.​ printf("%d ", current->data);
15.​ current = current->prev;
16.​ }
17.​}

Algorithm for Deleting a Node in a Double Linked List:

To delete a node, you need to adjust the pointers of the neighboring nodes.

Algorithm:
1.​ If the list is empty, return.
2.​ If the node to be deleted is the head:
○​ Update the head to the next node.
○​ Set the new head's previous pointer to NULL.
3.​ If the node to be deleted is the tail:
○​ Update the tail to the previous node.
○​ Set the new tail's next pointer to NULL.
4.​ If the node is in the middle:
○​ Adjust the previous node's next pointer to the next node.
○​ Adjust the next node's previous pointer to the previous node.
5.​ Free the memory of the deleted node.

Code Example:

18.​void deleteNode(struct Node** head, struct Node* delNode) {


19.​ if (*head == NULL || delNode == NULL) return;
20.​
21.​ // If the node to be deleted is the head
22.​ if (*head == delNode) {
23.​ *head = delNode->next; // Update head
24.​ if (*head != NULL) {
25.​ (*head)->prev = NULL; // Update previous pointer
26.​ }
27.​ } else {
28.​ // Adjust pointers for the node to be deleted
29.​ if (delNode->next != NULL) {
30.​ delNode->next->prev = delNode->prev;
31.​ }
32.​ if (delNode->prev != NULL) {
33.​ delNode->prev->next = delNode->next;
34.​ }
35.​ }
36.​ free(delNode); // Free the memory
37.​}

Unique Features of Circular Linked Lists:

1.​ Circular Structure: Unlike traditional linked lists, the last node in a circular linked list
points back to the first node, forming a circle. This means there is no NULL at the end.​

2.​ No End: You can traverse the list indefinitely without reaching a NULL pointer, making it
suitable for applications that require continuous looping through the list.​
3.​ Single or Double Circular: Circular linked lists can be singly linked (each node points to
the next) or doubly linked (each node points to both the next and previous nodes).​

4.​ Efficient Traversal: You can easily traverse the list from any node, making it useful for
applications like round-robin scheduling.​

Implementing Insertion in a Circular Linked List:

To insert a new node, you can choose to insert it at the beginning, end, or a specific position.
Here’s how to insert at the end:

Algorithm:

1.​ Create a new node.


2.​ If the list is empty, set the new node's next pointer to itself and update the head.
3.​ If the list is not empty:
○​ Traverse to the last node.
○​ Set the last node's next pointer to the new node.
○​ Set the new node's next pointer to the head.

Code Example:

1.​ void insertEnd(struct Node** head, int newData) {


2.​ struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
3.​ struct Node* last = *head;
4.​
5.​ newNode->data = newData;
6.​ newNode->next = *head; // Link new node to head
7.​
8.​ // If the list is empty
9.​ if (*head == NULL) {
10.​ *head = newNode;
11.​ newNode->next = newNode; // Point to itself
12.​ return;
13.​ }
14.​
15.​ // Traverse to the last node
16.​ while (last->next != *head) {
17.​ last = last->next;
18.​ }
19.​ last->next = newNode; // Link last node to new node
20.​}
Algorithm for Detecting a Loop in a Circular Linked List:

To detect a loop, you can use the Floyd’s Cycle-Finding Algorithm (also known as the
Tortoise and Hare algorithm).

Algorithm:

1.​ Initialize two pointers, slow and fast. Set both to the head of the list.
2.​ Move slow one step at a time and fast two steps at a time.
3.​ If there is a loop, the fast pointer will eventually meet the slow pointer.
4.​ If fast reaches NULL, there is no loop.

Code Example:

21.​bool detectLoop(struct Node* head) {


22.​ struct Node* slow = head;
23.​ struct Node* fast = head;
24.​
25.​ while (fast != NULL && fast->next != NULL) {
26.​ slow = slow->next; // Move slow by one
27.​ fast = fast->next->next; // Move fast by two
28.​
29.​ if (slow == fast) {
30.​ return true; // Loop detected
31.​ }
32.​ }
33.​ return false; // No loop
34.​}

You might also like