Unit 1 and 2 Data Structures
Unit 1 and 2 Data Structures
Stack ADT have operations like push,pop but we don’t know how those operations are
implemented.
A linked list is a linear data structure in which all the elements, i.e., nodes, are stored in a
sequence but not in contiguous memory locations. Each node of a linked list contains two
parts:
A singly linked list refers to a kind of linked list in which each node points to the following
node in the sequence, and the last node points to NULL, indicating the end of the list.
Singly Linked Lists are more commonly used in data structures due to their dynamic
allocation properties, which aid in better memory usage, node insertion, and deletion
compared to arrays.
A singly linked list in C is a list of nodes connected together sequentially, where each node
stores:
The singly linked list starts from the first node, known as the head pointer. If the list is empty,
the head is assigned NULL.
Singly linked lists offer flexibility and dynamic memory utilisation. Unlike arrays, their size is
not fixed, and nodes can be added or removed without requiring memory reallocation or
data shifting.
The working mechanism of a singly linked list revolves around creating nodes, linking them,
and managing their connections dynamically. Here’s how it typically works:
Node Creation: Each node is created dynamically using memory allocation functions.
Sequential Linking: Nodes are linked together by assigning the next pointer of one node to
the address of the next node.
Head Management: The head pointer maintains a reference to the first node, ensuring
accessibility to the list.
Termination: The last node's pointer (next) is set to null, signaling that this is the end of the
list.
Traversing a singly linked list thus first starts from the head pointer and follows the chain of
the next pointers until the very end is reached.
The core strength of a singly linked list lies in its ability to handle dynamic data efficiently.
The primary operations it supports are insertion, deletion, and traversal. Let’s discuss each in
detail.
Insertion in the singly linked list is simply adding a new node in specific positions. There are
three major types of insertions:
At the beginning
At the end
At a specific position
This operation involves putting a new node in front of the current head. Then the new node
thus becomes the first node, and the head pointer changes to point to it. This is a very
efficient operation, as it requires a constant amount of time no matter what the length of
the list might be.
Code Example:
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
newNode->next = *head; // Make the new node point to the current head
head = head->next;
int main() {
insertAtBeginning(&head, 10);
insertAtBeginning(&head, 20);
insertAtBeginning(&head, 30);
return 0;
Output:
To add the node at the end, we can traverse the list until we reach the last node. The new
node is attached to the last node's pointer with the address of the new node.
Code Example:
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
};
newNode->data = val;
newNode->next = NULL;
// If the linked list is empty, the new node becomes the head
if (*head == NULL) {
*head = newNode;
} else {
lastNode = lastNode->next;
lastNode->next = newNode;
printf("%d -> ", temp->data); // Print the data in the current node
int main() {
addLast(&head, 10);
addLast(&head, 20);
addLast(&head, 30);
printList(head);
return 0;
Output
This operation involves navigating to the desired position in the list and adjusting the
pointers of the surrounding nodes to include the new node in the sequence.
Code Example:
#include <stdio.h>
#include <stdlib.h>
// Node structure
struct Node {
int data;
};
if (!newNode) {
newNode->data = x;
newNode->next = NULL;
return newNode;
curr = curr->next;
printf("NULL\n");
if (pos < 1) {
printf("Invalid position.\n");
return head;
if (pos == 1) {
newNode->next = head;
return newNode;
curr = curr->next;
if (curr == NULL) {
return head;
newNode->next = curr->next;
curr->next = newNode;
return head;
// Main function
int main() {
head->next = createNode(5);
head->next->next = createNode(8);
head->next->next->next = createNode(10);
// Print original list
printf("Original list:\n");
printList(head);
printList(head);
head = head->next;
free(temp);
return 0;
Output
original list:
Deletion removes a node from the list while maintaining the structural integrity of the
remaining nodes. Similar to insertion, deletion can occur:
At the beginning
At the end
At a specific position
To delete the first node, the head pointer must be updated so that it points to the second
node. The memory allocated for the first node is then deallocated.
#include <stdio.h>
#include <stdlib.h>
// Node structure
struct Node {
int data;
};
if (!newNode) {
exit(1);
newNode->data = new_data;
newNode->next = NULL;
return newNode;
}
// Function to delete the head node and return the new head
if (head == NULL) {
return NULL;
head = head->next;
free(temp);
return head;
curr = curr->next;
printf("NULL\n");
}
// Main function
int main() {
head->next = createNode(12);
head->next->next = createNode(15);
head->next->next->next = createNode(18);
printf("Original list:\n");
printList(head);
head = deleteHead(head);
printList(head);
head = head->next;
free(temp);
}
return 0;
Output:
Original list:
This entails traversing to the second-latest node and terminating at the last one connected
by accessing the next pointer via NULL.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
};
if (!newNode) {
exit(1);
newNode->data = data;
newNode->next = NULL;
return newNode;
if (head == NULL) {
return NULL;
// If the list has only one node, delete it and return NULL
if (head->next == NULL) {
free(head);
return NULL;
secondLast = secondLast->next;
free(secondLast->next);
return head;
head = head->next;
printf("NULL\n");
// Driver code
int main() {
head->next = createNode(2);
head->next->next = createNode(3);
head->next->next->next = createNode(4);
head->next->next->next->next = createNode(5);
printf("Original list:\n");
printList(head);
printList(head);
head = head->next;
free(temp);
return 0;
Output:
list after removing last node : 1 -> 2 -> 3 -> 4 -> NULL
Here, the pointers of the surrounding nodes are adjusted to bypass the node to be deleted.
The memory allocated for the node is subsequently freed.
#include <stdio.h>
#include <stdlib.h>
int data;
};
node->data = data;
node->next = NULL;
return node;
if (head == NULL) {
return head;
if (position == 1) {
head = head->next;
free(temp);
return head;
}
// Traverse to the node just before the one to delete
prev = temp;
temp = temp->next;
if (temp == NULL) {
return head;
prev->next = temp->next;
free(temp);
return head;
head = head->next;
printf("NULL\n");
}
int main() {
head->next = newNode(3);
head->next->next = newNode(4);
head->next->next->next = newNode(5);
printList(head);
int position = 2;
printList(head);
head = head->next;
free(temp);
return 0;
Output:
Traversal refers to visiting each node in the list sequentially to perform operations like
displaying, processing, or searching for data. Starting from the head pointer, the traversal
follows the chain of the next pointers until it reaches the end (NULL).
Traversal is particularly useful for debugging and understanding the current structure of the
list.
Code Example:
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
};
return newNode;
int main() {
head->next = createNode(20);
head->next->next = createNode(30);
head->next->next->next = createNode(40);
traverseList(head);
temp = head;
head = head->next;
free(temp);
return 0;
Output:
10 20 30 40
The core strength of a singly linked list lies in its ability to handle dynamic data efficiently.
The primary operations it supports are insertion, deletion, and traversal. Let’s discuss each in
detail.
Insertion in the singly linked list is simply adding a new node in specific positions. There are
three major types of insertions:
At the beginning
At the end
At a specific position
This operation involves putting a new node in front of the current head. Then the new node
thus becomes the first node, and the head pointer changes to point to it. This is a very
efficient operation, as it requires a constant amount of time no matter what the length of
the list might be.
Code Example:
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
};
newNode->next = *head; // Make the new node point to the current head
head = head->next;
int main() {
insertAtBeginning(&head, 10);
insertAtBeginning(&head, 20);
insertAtBeginning(&head, 30);
printList(head);
return 0;
Output:
To add the node at the end, we can traverse the list until we reach the last node. The new
node is attached to the last node's pointer with the address of the new node.
Code Example:
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
};
newNode->data = val;
newNode->next = NULL;
// If the linked list is empty, the new node becomes the head
if (*head == NULL) {
*head = newNode;
} else {
lastNode->next = newNode;
printf("%d -> ", temp->data); // Print the data in the current node
int main() {
addLast(&head, 10);
addLast(&head, 20);
addLast(&head, 30);
printList(head);
return 0;
}
Output
This operation involves navigating to the desired position in the list and adjusting the
pointers of the surrounding nodes to include the new node in the sequence.
Code Example:
#include <stdio.h>
#include <stdlib.h>
// Node structure
struct Node {
int data;
};
if (!newNode) {
exit(1);
newNode->data = x;
newNode->next = NULL;
return newNode;
}
// Function to print the linked list
curr = curr->next;
printf("NULL\n");
if (pos < 1) {
printf("Invalid position.\n");
return head;
if (pos == 1) {
newNode->next = head;
return newNode;
if (curr == NULL) {
return head;
newNode->next = curr->next;
curr->next = newNode;
return head;
// Main function
int main() {
head->next = createNode(5);
head->next->next = createNode(8);
head->next->next->next = createNode(10);
printf("Original list:\n");
printList(head);
printList(head);
head = head->next;
free(temp);
return 0;
Output
original list:
Deletion removes a node from the list while maintaining the structural integrity of the
remaining nodes. Similar to insertion, deletion can occur:
At the beginning
At the end
At a specific position
Code Example for Deleting at the Beginning Operation:
To delete the first node, the head pointer must be updated so that it points to the second
node. The memory allocated for the first node is then deallocated.
#include <stdio.h>
#include <stdlib.h>
// Node structure
struct Node {
int data;
};
if (!newNode) {
exit(1);
newNode->data = new_data;
newNode->next = NULL;
return newNode;
// Function to delete the head node and return the new head
if (head == NULL) {
printf("The list is already empty.\n");
return NULL;
head = head->next;
free(temp);
return head;
curr = curr->next;
printf("NULL\n");
// Main function
int main() {
head->next = createNode(12);
head->next->next = createNode(15);
head->next->next->next = createNode(18);
printf("Original list:\n");
printList(head);
head = deleteHead(head);
printList(head);
head = head->next;
free(temp);
return 0;
Output:
Original list:
This entails traversing to the second-latest node and terminating at the last one connected
by accessing the next pointer via NULL.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
};
if (!newNode) {
exit(1);
newNode->data = data;
newNode->next = NULL;
return newNode;
if (head == NULL) {
return NULL;
// If the list has only one node, delete it and return NULL
if (head->next == NULL) {
free(head);
return NULL;
secondLast = secondLast->next;
free(secondLast->next);
secondLast->next = NULL;
return head;
}
// Function to print the linked list
head = head->next;
printf("NULL\n");
// Driver code
int main() {
head->next = createNode(2);
head->next->next = createNode(3);
head->next->next->next = createNode(4);
head->next->next->next->next = createNode(5);
printf("Original list:\n");
printList(head);
head = removeLastNode(head);
printList(head);
// Free remaining nodes to avoid memory leaks
head = head->next;
free(temp);
return 0;
Output:
list after removing last node : 1 -> 2 -> 3 -> 4 -> NULL
Here, the pointers of the surrounding nodes are adjusted to bypass the node to be deleted.
The memory allocated for the node is subsequently freed.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
};
node->data = data;
node->next = NULL;
return node;
if (head == NULL) {
return head;
if (position == 1) {
head = head->next;
free(temp);
return head;
prev = temp;
temp = temp->next;
}
if (temp == NULL) {
return head;
prev->next = temp->next;
free(temp);
return head;
head = head->next;
printf("NULL\n");
int main() {
head->next = newNode(3);
head->next->next = newNode(4);
head->next->next->next = newNode(5);
printList(head);
int position = 2;
printList(head);
head = head->next;
free(temp);
return 0;
Output:
Traversal refers to visiting each node in the list sequentially to perform operations like
displaying, processing, or searching for data. Starting from the head pointer, the traversal
follows the chain of the next pointers until it reaches the end (NULL).
Traversal is particularly useful for debugging and understanding the current structure of the
list.
Code Example:
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
};
return newNode;
printf("%d -> ", head->data); // Print the data of the current node
int main() {
head->next = createNode(20);
head->next->next = createNode(30);
head->next->next->next = createNode(40);
traverseList(head);
temp = head;
head = head->next;
free(temp);
return 0;
Output:
10 20 30 40
Struct node
struct node*prev;
int data;
struct *next;
};
new->prev=NULL;
else
{
tail->next=new;
new->prev=tail;
tail=new;
DISPLAY:
1000 20 3000
{ 2000
printf(“%d”,temp->data)
Insertion at beginning:
scanf(“%d”,&value);
new->data=value;
new->prev=null;
new->next=head;
head->prev=new;
head=new;
Insertion at ending:
scanf(“%d”,&value);
new->data=value;
new->next=null;
new->prev=tail;
tail->next=new;
tail=new;
DELETION
Beginning:
temp=head;
head=head->next;
temp->next=NULL;
head->prev=NULL;
Ending:
temp=tail;
tail=tail->prev;
temp->prev=NULL;
tail->next=NULL;
scanf(“%d”,&pos);
temp=head;
for(i=0;i<pos-1;i++)
temp=temp->next;
temp->next=temp->next->next;
temp->next->prev=temp;
Circular linked list
CREATE:
scanf(“%d”,&value);
new->data=value;
new->next=NULL;
if(head==NULL)
head=new;
tail=new;
else
tail->next=new;
tail=new;
tail->next=head;
DISPLAY:
while(temp->next!=head)
printf(“%d”,temp->data);
temp=temp->next;
printf(“%d”,temp->data)
Insertion
Beginning:
new->data=value;
new->next=head;
tail->next=new;
Ending:
scanf(%d”,&value)
new->data=value;
tail->next=new;
new->next=head;
tail=new;
DELETION:
Beginning:
temp=head;
head=head->next;
tail->next=head;
temp->next=NULL;
ENDING:
temp=head;
while(temp->next!=tail)
temp=temp->next;
tail->next=NULL;
temp->next=head;
tail=temp;
TREES:
A Tree Abstract Data Type (ADT) is a non-linear data structure that organizes data in a
hierarchical manner.
Tree Terminology:
Child – A node directly connected to another node when moving away from the Root.
Descendant – Any successor node on the path from root to that node.
Ancestor – Any predecessor node on the path from root to that node
Level – The level of a node is defined by 1 + (the number of connections between the node
and the root).
Height of node – The height of a node is the number of edges on the longest downward
path between that node and a leaf.
Height of tree – The height of a tree is the height of its root node.
Depth – The depth of a node is the number of edges from the node to the tree's root node.
A Binary Tree is a type of tree data structure where each node can have a maximum of two
child nodes, a left child node and a right child node.
This restriction, that a node can have a maximum of two child nodes.
If each node of binary tree has either two children or no child at all.
A perfect binary tree in which all leaf nodes are at the same level.
COMPLETE BINARY TREE:
A complete binary tree is a binary tree in which every level except possibly the last node is
completely filled from left to right leaving no gaps.
If a tree which is dominated by left child node or right child node is said to be a skewed
binary tree.
Each node of a binary tree has the following 3 parts:
Data
Binary tree traversal refers to the process of visiting each node in the tree exactly once in a
systematic way. There are four main types of binary tree traversals: in-order, pre-order, post-
order, and level-order. Each traversal method visits nodes in a different order.
In in-order traversal, the nodes are recursively visited in this order: left subtree, root node,
and then the right subtree.
Example:
In-order Traversal: 4, 2, 5, 1, 3
Steps:
In pre-order traversal, the nodes are recursively visited in this order: root node, left subtree,
and then the right subtree
Example:
Pre-order Traversal: 1, 2, 4, 5, 3
Steps:
In post-order traversal, the nodes are recursively visited in this order: left subtree, right
subtree, and then the root node.
Example:
Post-order Traversal: 4, 5, 2, 3, 1
Steps:
In level-order traversal, the nodes are visited level by level from left to right. This traversal
uses a queue data structure to keep track of nodes.
Example:
Level-order Traversal: 1, 2, 3, 4, 5
Steps:
Using Arrays
i=0,2i+2=2
i=0,2i+1=1
C
B
i=1,2i+1=3 i=1,2i+2=4
D E
i=3,2i+1=7 i=4,2i+1=9
F
G
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
A B C D E - - F - G
A binary search tree (BST) is a type of data structure used in computer science to store and
manage data. In a BST, each node is a part of the tree that holds a value. Each node can have
up to two children nodes. The node at the top is called the root.
All the values in the left child and its descendants are smaller than the node's value.
All the values in the right child and its descendants are greater than the node's value.
This structure helps to quickly find, add, or remove values from the tree.
For example, if you want to find a value, you can start at the root and move left or right
depending on whether the value is smaller or larger than the current node, making the
search very efficient.
Example:
2. The left child of 10 is 5, and all values in the left subtree (3, 5, 7) are less than 10.
3. The right child of 10 is 15, and all values in the right subtree (15, 20) are greater than
10.
1. Insertion
Insertion in binary search tree includes adding a new node in a way that maintains the BST
property: for any node, all values in its left subtree are smaller, and all values in its right
subtree are greater.
Algorithm:
Example:
Step-by-Step Insertion:
Result:
3. Searching
Searching in a binary search tree involves finding a node with a given value by leveraging the
BST property to reduce the number of comparisons.
Algorithm:
Repeat steps 2-5 until you find the value or reach an empty spot (unsuccessful search).
A stack is used to build an expression tree. For each character, we cycle through the input
expressions and do the following.
If a character is an operator, pop both values from the stack and make both its children and
push the current node again.
Finally, the stack’s lone element will be the root of an expression tree.
Let us understand this above process using a postfix expression. The implementation of the
expression tree is described for the below expression.
ab+c*
Step1 : The first two symbols are operands, for which we generate a one-node tree
and push a reference to the stack.
Step2 : Next, read a’+’ symbol to pop two pointers to the tree, form a new tree, and push
a pointer to it onto the stack.
Step3 : In the next stage ‘c’ is read, we build a single node tree and store a pointer to it on
the stack.
Step4 : Finally, we read the last symbol’ ‘, pop two tree pointers, and build a new tree with
a,’‘as root, and a pointer to the final tree remains on the stack.
B-tree
B-tree is a special type of self-balancing search tree in which each node can contain more
than one key and can have more than two children. It is a generalized form of the binary
search tree.
Searching Example
k is found. k is found
Insertion Operation
If the tree is empty, allocate a root node and insert the key.
Now, there are elements greater than its limit. So, split at the median.
Push the median key upwards and make the left keys as a left child and the right keys as a
right child.
Example:
Let us consider the given tree. From the given tree we are to delete the following elements:
A = 20 , 53 , 89 , 90 , 85.
maximum children = m = 5
1. If the target key is at the leaf node:
If the target key is at the leaf node, we further study the given data to check if any of the
following cases exist:
Case 1: If the leaf node consists of the min number of keys according to the given
degree/order, then the key is simply deleted from the node.
Case 2a: The node can borrow a key from the immediate left sibling node,if it has more than
the minimum number of keys.The transfer of the keys take place through the parent node,
i.e, the maximum key of the left sibling moves upwards and replaces the parent; while the
parent key moves down to the target node from where the target key is simply deleted.
Case 2b: The node can borrow a key from the immediate right sibling node, if it has more
than the minimum number of keys.The transfer of the keys take place through the parent
node, i.e, the minimum key of the right sibling moves upwards and replaces the parent;
while the parent key moves down to the target node from where the target key is simply
deleted.
Case 2c: If neither of the siblings have keys more than the minimum number of keys
required then, merge the target node with either the left or the right sibling along with the
parent key of respective node.
Step 1:
The node has more than the minimum number of keys required.
Step 3:
Step 4:
4. Since the node in which the target key 53 exists has just the minimum number of
keys, we cannot delete it directly.
5. We check if the target node can borrow a key from it’s sibling nodes.
6. Since the target node doesn’t have any right sibling, it borrows from the left sibling
node.
7. As we have studied above how the process of borrow and replace takes place, we
apply it to the given structure.
Step 5:
Step 6:
Now, since the target node has keys more than the minimum number of keys required, the
key can be deleted directly.
Step 7:
Step 8:
Again, the target node holds just the minimum number of keys required and hence
the node cannot be deleted directly.
The target node now has to borrow a key from either of it’s siblings.
We check the left sibling; it also holds just the minimum number of keys required.
We check the right sibling node; it has one more than the minimum number of
nodes so the target node can borrow a key from it.
Step 9:
Step 10:
Now, as the target node has sufficient number of keys the target key can directly be deleted
from the target node.
Step 11:
Step 12:
We can see that the target node has just the minimum number of keys.
The target node has to borrow a key from either of it’s siblings.
Since each of the siblings just have the number of the minimum keys, it cannot borrow the
keys directly
Step 13:
Since the target node cannot borrow from either of the siblings, we merge the target node,
either of the sibling node and the corresponding parent to them.
Since the target node now has sufficient number of keys, the target key 90 can be
deleted directly.
The tree structure after the deletion of the element is shown as follows.
Step 15:
Now here the node to be deleted is not a leaf node but an internal node.
Step 16:
In case, when an internal node is to be deleted, we replace the key with it’s inorder
predecessor or inorder successor.
We can select either of the child nodes if they have sufficient number of keys.
But as we can see in this case the target internal node can only borrow from it’s right child,
i.e, inorder predecessor.
The key 85 moves down to the child node; key 87 moves up to the parent node.
Step 17:
Now, as the target key is moved to the leaf node, it can be simply deleted from the
leaf node.
The final tree structure after deletion of various nodes and preserving the b-tree
properties is shown as follows.