Complete DSC Notes 1
Complete DSC Notes 1
Derived data types in C are formed using fundamental (primitive) data types such as int, char,
float, and double. These derived types do not define new types but rather extend or modify
existing types to handle more complex data structures.
Derived data types are created using C's language constructs such as:
1. Arrays
2. Pointers
An array is a collection of elements of the same data type stored in contiguous memory locations.
A pointer is a derived data type that stores the memory address of another variable.
A normal variable holds a value, but a pointer variable holds an address of a value.
int a = 10;
int *ptr = &a; // 'ptr' stores the address of 'a'
Memory Representation
Pointer to an Array
Conclusion
Derived data types in C are powerful extensions of fundamental data types, allowing for efficient
memory management, better code organization, and enhanced functionality. Understanding
them helps in writing modular, scalable, and optimized programs.
User-defined data types in C allow programmers to define new data types using existing primitive
types (int, char, float, etc.). They help represent complex data in a structured way.
Primitive data types (int, float, etc.) store only single values.
Complex real-world objects (e.g., student records, employee data) require multiple data
fields.
User-defined data types group related data into a single unit, making code more
modular and readable.
Types of User-Defined Data Types in C
1. Structures (struct)
2. Unions (union)
3. Enumerations (enum)
4. Typedef (typedef) and #define Macros
Conclusion
Derived data types help in handling collections (arrays), references (pointers), and function
manipulations.
User-defined data types provide structure to complex data, making programs more
manageable and efficient.
3. Structures:
Problem: A university wants to store student details like name, roll number, marks, and
department.
Without Structures: You’d need to define multiple separate arrays, making it hard to manage
and prone to errors.
For Student 1 :
char student1_name[50][100];
int roll1_number[100];
float marks1[100];
char department1[100][50];
For Student 2 :
char student2_name[50][100];
int roll2_number[100];
float marks2[100];
char department2[100][50];
.
.
.
Go on…..
Problem: A company wants to store employee details like name, ID, salary, and department.
Without Structures: Separate variables for each property can lead to disorganized code.
Problem: An online store needs to store details of each product, such as name, price, and
stock.
Problem: A bank needs to store customer details like account numbers, name, and balance.
Arrays store only one data type (e.g., all integers, all floats).
What is a Structure in C?
A structure in C is a user-defined data type that allows grouping different data types under a single
name. It helps in organizing complex data efficiently, making it easy to manage and manipulate.
1. Declaring a Structure
struct Student {
char name[50];
int roll_no;
float marks;
};
s1.roll_no = 101;
s1.marks = 95.5;
strcpy(s1.name, "John Doe");
Direct Initialization
Here are some simple structure examples in C to help you understand how structures work.
#include <stdio.h>
#include <string.h>
// Defining a structure
struct Student {
char name[50];
int roll_no;
float marks;
};
int main() {
// Declaring a structure variable
struct Student s1;
// Assigning values
strcpy(s1.name, "Alice");
s1.roll_no = 101;
s1.marks = 85.5;
// Printing values
printf("Student Name: %s\n", s1.name);
printf("Roll No: %d\n", s1.roll_no);
printf("Marks: %.2f\n", s1.marks);
return 0;
}
Output:
Student Name: Alice
Roll No: 101
Marks: 85.50
Example 2: Structure with User Input
This program takes student details from the user and displays them.
#include <stdio.h>
int main() {
struct Student s1;
// Printing values
printf("\nStudent Details:\n");
printf("Name: %s\n", s1.name);
printf("Roll No: %d\n", s1.roll_no);
printf("Marks: %.2f\n", s1.marks);
return 0;
}
Sample Output:
Enter name: Bob
Enter roll number: 102
Enter marks: 90.5
Student Details:
Name: Bob
Roll No: 102
Marks: 90.50
Example 3: Structure Initialization
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
struct Student s1 = {101, "Alice", 89.5}; // Direct Initialization
printf("Roll No: %d, Name: %s, Marks: %.2f\n", s1.rollNo, s1.name,
s1.marks);
return 0;
}
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
struct Student s1 = {.name = "Bob", .rollNo = 102, .marks = 92.5};
// Named Initialization
printf("Roll No: %d, Name: %s, Marks: %.2f\n", s1.rollNo, s1.name,
s1.marks);
return 0;
}
Array of Structures
An array of structures is used when you need to store and process multiple records of the same
structure type efficiently. Instead of creating multiple separate structure variables, an array of
structures allows handling large data sets systematically.
Use Cases
To declare an array of structures, define the structure first and then create an array of that structure
type.
struct Student {
int rollNo;
char name[20];
float marks;
};
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
// Array of structure initialization
struct Student students[3] = {
{101, "Alice", 85.5},
{102, "Bob", 90.2},
{103, "Charlie", 78.8}
};
return 0;
}
Explanation:
struct Employee {
int id;
char name[30];
float salary;
};
int main() {
struct Employee employees[2]; // Array of 2 employees
return 0;
}
Explanation:
This approach is useful in handling large datasets in real-world applications like student
databases, employee management, inventory tracking, and more.
Pointer to Structure
A pointer to a structure is a pointer variable that stores the address of a structure. It allows
dynamic memory allocation, efficient data manipulation, and reduces memory usage.
The ptr variable is a pointer that can store the address of a Student structure.
The dot operator (.) is used for accessing structure members using a normal variable.
The arrow operator (->) is used to access members via a pointer.
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
struct Student s1 = {101, "Alice", 89.5}; // Normal structure
variable
struct Student *ptr = &s1; // Pointer to structure
return 0;
}
Output:
Roll No: 101
Name: Alice
Marks: 89.50
Explanation:
struct Employee {
int id;
char name[30];
float salary;
};
int main() {
struct Employee e1;
struct Employee *ptr = &e1; // Pointer to structure
return 0;
}
Explanation:
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
struct Student *ptr;
if (ptr == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
// Taking input
printf("Enter Roll No, Name, and Marks:\n");
scanf("%d", &ptr->rollNo);
scanf("%s", ptr->name);
scanf("%f", &ptr->marks);
// Displaying output
printf("\nStudent Details:\n");
printf("Roll No: %d, Name: %s, Marks: %.2f\n", ptr->rollNo, ptr-
>name, ptr->marks);
return 0;
}
Explanation:
struct Car {
char name[20];
float price;
};
int main() {
struct Car cars[2] = {{"Toyota", 50000.0}, {"Honda", 45000.0}};
struct Car *ptr = cars; // Pointer to structure array
return 0;
}
Explanation:
Summary
Concept Explanation
Pointer to Structure Stores the address of a structure variable.
Accessing Members Use ptr->member instead of (*ptr).member.
Dynamic Memory Allocation Use malloc() for runtime memory allocation.
Array of Structure Pointers Efficient for handling multiple records.
1. Pass by Value
2. Pass by Reference (Using Pointers)
3. Pass an Array of Structures
1. Pass Structure to Function by Value
When passing by value, a copy of the structure is made and passed to the function.
Changes inside the function do not affect the original structure.
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
struct Student s1 = {101, "Alice", 89.5};
return 0;
}
Key Points:
Instead of passing a copy, we can pass the address of the structure using a pointer.
This allows modifications inside the function to reflect in the original structure.
Example 1 : Pass by Reference
#include <stdio.h>
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
struct Student s1 = {102, "Bob", 80.0};
return 0;
}
Key Points:
✔ The address of s1 is passed (&s1), so changes inside the function modify the original
structure.
✔ We use arrow operator (->) to access members inside the function.
✔ Efficient for large structures, as no copy is created.
✔ Use Case: When we need to modify structure members inside the function.
struct Student {
int rollNo;
char name[20];
float marks;
};
// Function to display student details
void displayStudents(struct Student students[], int size) {
for (int i = 0; i < size; i++) {
printf("Roll No: %d, Name: %s, Marks: %.2f\n",
students[i].rollNo, students[i].name, students[i].marks);
}
}
int main() {
struct Student students[3] = {
{103, "Charlie", 78.5},
{104, "David", 92.0},
{105, "Emma", 85.5}
};
return 0;
}
Key Points:
Summary
Changes Original
Method What is Passed? Best Use Case
Structure?
A copy of the Small structures where
Pass by Value No
structure modification is not needed
Pointer (Address Large structures where
Pass by Reference Yes
of structure) modification is required
Pass an Array of Base address of Handling multiple records
Yes
Structures array efficiently
Conclusion
✔ Pass by Value → Simple but inefficient for large structures.
✔ Pass by Reference → Efficient and allows modification.
✔ Passing Array of Structures → Best for handling multiple records.
Using pointers is recommended for better performance and memory efficiency when dealing
with large structures.
Different Structure Declarations in C
In C, structures can be declared in multiple ways. Let's analyze the different declarations and
understand their differences.
✔ Explanation
This defines a structure called Student but does not declare any variables.
Memory is not allocated yet.
To use it, we must declare variables like:
struct Student s1, s2;
Access members using dot (.) operator:
s1.rollNo = 101;
strcpy(s1.name, "Alice");
✔ Explanation
Student.rollNo = 102;
strcpy(Student.name, "Bob");
Key Differences:
✔ Explanation
Here, Student is a named structure type, and s1, s2 are declared together.
Equivalent to:
struct Student {
int rollNo;
char name[20];
};
Accessing members:
s1.rollNo = 103;
strcpy(s1.name, "Charlie");
Key Difference:
Case 1 (struct Student) → Just defines the structure type, no variables created.
Case 3 (struct Student { ... } s1, s2;) → Defines the structure and declares
variables.
Key Difference:
Summary
No No (Only inside
4 Inside main() Yes (s1, s2)
(Anonymous) main())
Nested Structures in C
Nested structures in C allow us to define one structure inside another. They help manage and group
related data logically and hierarchically. This approach is particularly useful for complex data
models, where the relationships between the data entities must be well-defined and organized.
1. Logical Grouping: They help organize related data hierarchically within a single
structure.
2. Ease of Access: Simplify accessing and updating specific components of a larger
structure.
3. Reusability: By nesting structures, you can reuse one structure in multiple larger
structures, reducing redundancy.
4. Improved Readability: Helps to maintain clean and readable code by separating
complex data into manageable parts.
person1.name;
person1.address.street;
Pointer to structure: Use the arrow (->) operator when accessing nested fields via pointers.
personPtr->address.city;
Here’s an example of a nested structure where a student's date of birth (DOB) is included as a
nested structure.
int main() {
struct Student student1;
return 0;
}
Sample Input and Output
Input:
Output:
Student Details:
ID: 101
Name: John
Date of Birth: 15-08-2005
Explanation
1. Structure Definition:
o The struct Date represents the day, month, and year fields for the date of birth.
o The struct Student contains the id, name, and a nested struct Date named
dob.
2. Input and Access:
o student1.dob.day is used to access the day part of the DOB.
o Similarly, student1.dob.month and student1.dob.year are used for the month
and year.
3. Formatting:
o The %02d ensures two-digit formatting for day and month, and %04d ensures four-
digit formatting for the year.
This example is highly practical for applications like student records management systems!
A real-life scenario where nested structures are useful is for managing a person's details, including
their address.
#include <stdio.h>
int main() {
struct Person person1;
// Input data
printf("Enter Name: ");
scanf("%s", person1.name);
printf("Enter Age: ");
scanf("%d", &person1.age);
printf("Enter Street: ");
scanf("%s", person1.address.street);
printf("Enter City: ");
scanf("%s", person1.address.city);
printf("Enter Zip Code: ");
scanf("%d", &person1.address.zipCode);
// Display data
printf("\nDetails:\n");
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Address: %s, %s, %d\n", person1.address.street,
person1.address.city, person1.address.zipCode);
return 0;
}
#include <stdio.h>
int main() {
struct Employee emp;
// Display details
printf("\nEmployee Details:\n");
printf("Name: %s\n", emp.name);
printf("ID: %d\n", emp.empId);
printf("Department ID: %d\n", emp.dept.deptId);
printf("Department Name: %s\n", emp.dept.deptName);
return 0;
}
This example models a library system with books, their authors, and publishers.
#include <stdio.h>
struct Publisher {
char name[30];
char location[30];
};
int main() {
struct Book book1;
// Input details
printf("Enter Book Title: ");
scanf("%s", book1.title);
printf("Enter Author Name: ");
scanf("%s", book1.author.name);
printf("Enter Author Country: ");
scanf("%s", book1.author.country);
printf("Enter Publisher Name: ");
scanf("%s", book1.publisher.name);
printf("Enter Publisher Location: ");
scanf("%s", book1.publisher.location);
return 0;
}
Author Details:
o Access author's name: book1.author.name
o Access author's country: book1.author.country
Publisher Details:
o Access publisher's name: book1.publisher.name
o Access publisher's location: book1.publisher.location
This example demonstrates the importance of grouping related fields (like author and publisher)
logically within a larger structure.
Nested structures are powerful for modeling real-world entities with logical hierarchies, ensuring
clean and manageable code.
Case Study 1:
Structures are needed to write programs for complex numbers in C (and similar languages) because
complex numbers have two components: a real part and an imaginary part. These two parts are
logically connected and should be treated as a single entity for better organization, readability, and
efficiency in operations. Here's a detailed explanation:
Using structures, you can define a custom data type Complex to represent the real and imaginary
parts of a complex number. Here's an example:
#include <stdio.h>
Without Structures:
You would need separate variables for the real and imaginary parts:
Here, managing separate variables for every operation becomes tedious, especially as the
program grows.
With Structures:
#include <stdio.h>
int main() {
struct Complex c1 = {2.5, 3.0}; // First complex number
struct Complex c2 = {1.0, 4.5}; // Second complex number
struct Complex sum = addComplex(c1, c2);
printf("Sum: %.2f + %.2fi\n", sum.real, sum.imaginary);
return 0;
}
Operations Made Easier with Structures
With a structure, you can define reusable functions to perform various operations on complex
numbers, such as:
Addition:
Subtraction:
Multiplication:
Division: Division requires normalizing the denominator and can also be implemented
cleanly using structures.
CASE STUDY 2 :
Example 1 : dynamic memory allocation to handle an array of structures using double pointers
(struct Student **ptr).
#include <stdio.h>
#include <stdlib.h>
struct Student {
int rollNo;
char name[20];
float marks;
};
int main() {
int n,i;
printf("Enter the number of students\n");
scanf("%d",&n);
struct Student ** ptr;
// Allocating memory dynamically
ptr = (struct Student **)calloc(n,sizeof(struct Student*));
if (ptr == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
for(i=0;i<n;i++){
ptr[i] = (struct Student *)malloc(sizeof(struct Student));
printf("Enter Roll No, Name, and Marks of student %d:\n",i+1);
scanf("%d", &(ptr[i]->rollNo));
scanf("%s", ptr[i]->name);
scanf("%f", &(ptr[i]->marks));
}
// Displaying output
for(i=0;i<n;i++){
printf("\nStudent %d Details:\n",i+1);
printf("Roll No: %d, Name: %s, Marks: %.2f\n", ptr[i]->rollNo, ptr[i]-
>name, ptr[i]->marks);
}
// Freeing allocated memory
free(ptr);
return 0;
}
A double pointer (struct Student **) is used to represent a dynamically allocated array of
pointers to structures. Here's why:
2. Is it a 2D array?
No, this is not a true 2D array. However, the use of a double pointer might give the appearance
of a 2D array because:
The first level of pointers (ptr) is like a "row" array, storing addresses of the individual
struct Student objects.
The second level represents each struct Student object, where each "row" can have its
own allocated memory.
In reality, this is more like an array of dynamically allocated structures rather than a true 2D
array. The relationship can be visualized as:
ptr ->[ pointer to Student1, pointer to Student2, ..., pointer to Student n ]
| | |
V V V
{ struct Student } { struct Student } { struct Student }
ptr is a double pointer. It holds the addresses of multiple struct Student * pointers.
calloc allocates space for n pointers (of type struct Student *).
After this step, ptr is an array of n uninitialized pointers (all set to NULL by calloc).
For each i, this line dynamically allocates memory for a single struct Student object.
ptr[i] is now pointing to the memory block allocated for that specific student.
scanf("%d", &(ptr[i]->rollNo));
scanf("%s", ptr[i]->name);
scanf("%f", &(ptr[i]->marks));
Similar to the input step, this dereferences ptr[i] to access the data fields of the i-th
student.
free(ptr);
Issue here: While free(ptr) releases the memory allocated for the array of pointers, the
memory allocated for each struct Student object is not freed explicitly. This can lead
to a memory leak.
4. Why not use a single pointer (struct Student *ptr)?
A single pointer (struct Student *ptr) could be used to allocate a contiguous block of
memory for all n students, like this:
This is simpler and does not require a double pointer. You can access each student as:
However, this method allocates a single block of memory for all n students, which:
The double-pointer method provides flexibility, as each struct Student can be allocated and
deallocated independently.
Each struct Student now contains the entered roll number, name, and marks:
free(ptr);
This only frees the memory allocated for the array of pointers ( ptr) but not the memory
allocated for each student. To avoid memory leaks:
7. Conclusion
Self-Referential Structures
Syntax:
struct Node {
int data;
struct Node *next; // Pointer to the same type of structure
};
In this example, the structure Node contains a pointer (next) to another Node.
Why is it Required?
Self-referential structures are essential for creating dynamic and recursive data structures.
They allow structures to "link" with one another to form complex data relationships.
Advantages:
1. Dynamic Data Structures: Enable the creation of linked lists, trees, and graphs, where
the size can grow or shrink dynamically.
2. Efficient Memory Usage: Memory is allocated only when needed, avoiding the need to
allocate a large fixed array.
3. Recursive Algorithms: Useful in recursive definitions of data structures like trees
(binary trees, binary search trees, etc.).
4. Flexibility: Provide a flexible way to handle relationships among data elements, like
parent-child or predecessor-successor relationships.
1. Linked Lists:
o Used in memory management, file systems, and data queues.
2. Binary Trees:
o Common in databases and searching algorithms.
3. Graphs:
o Used in network routing and shortest path algorithms.
4. Stacks and Queues:
o Used in operating systems, scheduling, and recursion management.
Simpler Examples:
Self-referential structures can also be used in simpler scenarios, like creating relationships
among objects, organizing data that references itself, or managing data with cycles. Below are
examples that are simpler and easier to grasp than data structures like linked lists and trees.
An employee structure can have a pointer to another employee (e.g., their manager).
#include <stdio.h>
#include <stdlib.h>
struct Employee {
int id;
char name[20];
struct Employee *manager; // Pointer to the manager (another Employee)
};
int main() {
struct Employee emp1, emp2;
// Initialize employee 2
emp2.id = 102;
sprintf(emp2.name, "Bob");
emp2.manager = &emp1; // Bob's manager is Alice
// Display information
printf("Employee: %s, ID: %d\n", emp2.name, emp2.id);
printf("Manager: %s, ID: %d\n", emp2.manager->name, emp2.manager->id);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
struct Person {
char name[20];
struct Person *parent; // Pointer to the parent (another Person)
};
int main() {
struct Person child, parent;
// Initialize parent
sprintf(parent.name, "John");
// Initialize child
sprintf(child.name, "Emma");
child.parent = &parent; // Emma's parent is John
return 0;
}
A self-referential structure can be used to represent objects that refer back to each other.
#include <stdio.h>
struct Friend {
char name[20];
struct Friend *bestFriend; // Pointer to another Friend
};
int main() {
struct Friend alice, bob;
// Initialize Alice
sprintf(alice.name, "Alice");
alice.bestFriend = &bob; // Alice's best friend is Bob
// Initialize Bob
sprintf(bob.name, "Bob");
bob.bestFriend = &alice; // Bob's best friend is Alice
// Display relationships
printf("%s's best friend is %s.\n", alice.name, alice.bestFriend->name);
printf("%s's best friend is %s.\n", bob.name, bob.bestFriend->name);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
struct Node {
int id;
struct Node *connectedNode; // Pointer to another Node
};
int main() {
struct Node node1, node2;
// Initialize nodes
node1.id = 1;
node2.id = 2;
// Display connections
printf("Node %d is connected to Node %d\n", node1.id,
node1.connectedNode->id);
printf("Node %d is connected to Node %d\n", node2.id,
node2.connectedNode->id);
return 0;
}
#include <stdio.h>
struct Product {
char name[20];
float price;
struct Product *relatedProduct; // Pointer to a related product
};
int main() {
struct Product product1, product2;
// Initialize products
sprintf(product1.name, "Laptop");
product1.price = 50000.0;
sprintf(product2.name, "Mouse");
product2.price = 1000.0;
return 0;
}
6. Course Prerequisites
#include <stdio.h>
struct Course {
char name[30];
struct Course *prerequisite; // Pointer to the prerequisite course
};
int main() {
struct Course course1, course2;
// Initialize courses
sprintf(course1.name, "C Programming");
course1.prerequisite = NULL; // No prerequisite for this course
A union in C is a user-defined data type similar to a structure but with one key difference: all
members of a union share the same memory location. This means that at any given time, a
union can hold only one value, and the size of a union is equal to the size of its largest member.
1. Efficient Memory Usage: Unlike structures, which allocate memory for all members, a
union allocates memory only for the largest member, making it ideal for scenarios where
only one value needs to be stored at a time.
2. Type Flexibility: Unions allow multiple representations of the same memory, enabling
type punning (treating the same memory as different data types).
3. Efficient Handling of Intermediate Results: When performing arithmetic operations,
unions can store intermediate values in different formats, reducing redundant memory
allocation.
#include <stdio.h>
// Structure Example
struct StructExample {
int a;
float b;
char c;
};
// Union Example
union UnionExample {
int a;
float b;
char c;
};
int main() {
printf("Size of struct: %lu\n", sizeof(struct StructExample));
printf("Size of union: %lu\n", sizeof(union UnionExample));
return 0;
}
Output (on a 32-bit system):
Size of struct: 12
Size of union: 4
Structure allocates space for all members (int = 4 bytes, float = 4 bytes, char = 1 byte,
padding).
Union allocates space only for the largest member (int or float = 4 bytes).
In embedded systems, memory is limited. Using unions saves memory where multiple variables
are used at different times but never together.
A temperature sensor might store data in multiple formats (integer for raw values, float for
precise readings):
#include <stdio.h>
union SensorData {
int rawValue;
float calibratedValue;
};
int main() {
union SensorData temp;
return 0;
}
#include <stdio.h>
union FloatInt {
float f;
unsigned int i;
};
int main() {
union FloatInt num;
num.f = 3.14; // Store float value
return 0;
}
Use Case: This technique is useful in low-level bitwise operations, such as checking IEEE-754
floating-point representations.
Unions are useful in operations where an intermediate result can be stored in a different format
to save space.
#include <stdio.h>
union Result {
int intValue;
float floatValue;
};
int main() {
union Result res;
res.intValue = 10;
printf("Integer Result: %d\n", res.intValue);
res.floatValue = 10.5 / 2;
printf("Float Result: %.2f\n", res.floatValue);
return 0;
}
First, the result is stored as an integer.
Later, the same memory is used for a floating-point calculation.
In applications like parsing files or network packets, a union can store different types of values.
#include <stdio.h>
union Packet {
int intData;
char charData[4];
};
int main() {
union Packet p;
return 0;
}
Use Case: Used in network protocols where a packet may contain different data types.
2. Operating Systems
Used in kernel structures to manage different types of resources with minimal memory.
Example: Interrupt Descriptor Table (IDT) uses unions for different descriptor formats.
3. Network Programming
Packet headers often contain different types of data that need to be interpreted differently.
Unions help in parsing binary file formats, where a section of data can have different
interpretations.
Conclusion
Unions provide an efficient way to manage memory by sharing space among different
variables.
They are particularly useful when only one value needs to be stored at a time.
They are widely used in low-level programming, embedded systems, network
protocols, and arithmetic operations.
While they have limitations (overwriting previous values), they are powerful tools when
used correctly.
Case Study 1:
This project demonstrates how unions can be used efficiently to store student data where different
attributes are required based on the student's type (Regular/Exchange Student). Instead of storing
all details separately for every student, a union helps save memory by storing only the relevant
information.
Features:
Stores student details such as Roll Number, Name, and Type (Regular or Exchange).
Uses union to store additional details:
o Regular students: CGPA and attendance.
o Exchange students: Country of origin and duration of the program.
Efficient memory usage since only one type of student-specific data is stored at a time.
C Code Implementation
#include <stdio.h>
#include <string.h>
struct {
char country[30];
int duration; // Months
} exchange;
} details;
}s1;
return 0;
}
Sample Output
Student Roll No: 101
Name: Alice Johnson
Student Type: Regular
CGPA: 9.10
Attendance: 85%
---------------------------
What is typedef in C?
typedef (Type Definition) is a keyword in C programming used to create a new name (alias)
for an existing data type. It makes the code more readable, simplifies complex type
definitions, and improves code portability.
Syntax of typedef
typedef existing_datatype new_typename;
int main() {
UINT num = 100; // 'num' is now of type 'unsigned int'
printf("Value: %u\n", num);
return 0;
}
Why use this? Instead of writing unsigned int repeatedly, we can simply use UINT.
struct Student {
int rollno;
char name[50];
float cgpa;
};
int main() {
struct Student s1 = {101, "Alice", 9.2};
printf("Roll No: %d, Name: %s, CGPA: %.2f\n", s1.rollno, s1.name,
s1.cgpa);
return 0;
}
Problem: Every time we declare a structure variable, we must write struct Student.
int main() {
Student s1 = {101, "Alice", 9.2}; // No need to write 'struct'
printf("Roll No: %d, Name: %s, CGPA: %.2f\n", s1.rollno, s1.name,
s1.cgpa);
return 0;
}
Advantage: Now, we can simply use Student instead of struct Student, making the code
cleaner.
int main() {
FuncPtr fptr = add; // Function pointer assignment
printf("Sum: %d\n", fptr(10, 20));
return 0;
}
int main() {
INT_ALIAS x = 10; // Works
INT_TYPE y = 20; // Works
printf("x: %d, y: %d\n", x, y);
return 0;
}
Prefer typedef over #define for type definitions since the compiler performs type
checking.
Conclusion
typedef is used to create aliases for existing types.
It simplifies complex type definitions, making the code cleaner and easier to maintain.
Works well with structures, pointers, and function pointers.
Prefer typedef over #define for type safety.
Command-line arguments:
Command-line arguments are a powerful way to provide input to a program when it's executed
from the command line or terminal. They allow users to customize a program's behavior without
modifying its source code.
Essentially, they're extra pieces of information you type after the program's name when
you run it.
These arguments are then passed to the program, which can use them to alter its operation.
In C, command-line arguments are handled by the main() function, which can take two
parameters:
Syntax:
int argc:
o This stands for "argument count."
o It's an integer that holds the number of command-line arguments passed to the
program, including the program's name itself.
o Therefore, argc is always at least 1.
char *argv[]:
o This stands for "argument vector."
o It's an array of strings (character pointers).
o Each element of argv points to a command-line argument.
o argv[0] contains the program's name.
o argv[1] to argv[argc-1] contain the additional arguments.
Usage:
Example:
If you have a program named ./a.out, you might run it like this:
In this case:
argc would be 4.
argv[0] would be "a.out".
argv[1] would be "cvr".
argv[2] would be "mvsr".
argv[3] would be "cbit".
Importance:
Real-Time Scenarios:
File Processing:
o A program that processes files might take the filenames as command-line
arguments. For example: ./a.out file1.txt file2.csv.
Image Manipulation:
o An image processing tool could take arguments for input and output filenames, as
well as options for resizing or filtering: ./a.out input.jpg output.png -
resize 500x500.
Compilers and Interpreters:
o Compilers and interpreters heavily rely on command-line arguments to specify
source files, output files, and various compilation or execution options: gcc
mycode.c -o myprogram.
System Utilities:
o Many system utilities, like grep, find, and ls, use command-line arguments to
customize their behavior.
Database interactions:
o A program that connects to a database could take the database name, username and
password as command line arguments.
In essence, command-line arguments are a fundamental tool for creating flexible and powerful
command-line applications.
#include <stdio.h>
if (argc > 1) {
printf("Arguments:\n");
for (int i = 1; i < argc; i++) {
printf(" argv[%d]: %s\n", i, argv[i]);
}
} else {
printf("No additional arguments provided.\n");
}
return 0;
}
2. Simple Calculator:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
double result;
switch (operator) {
case '+':
result = num1 + num2;
break;
case '-':
result = num1 - num2;
break;
case '*':
result = num1 * num2;
break;
case '/':
if (num2 != 0) {
result = num1 / num2;
} else {
printf("Error: Division by zero.\n");
return 1;
}
break;
default:
printf("Error: Invalid operator.\n");
return 1;
}
printf("Result: %lf\n", result);
return 0;
}
This program searches for a given string in a file specified as a command-line argument.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char line[MAX_LINE_LENGTH];
while (fgets(line, sizeof(line), file) != NULL) {
if (strstr(line, search_string) != NULL) {
printf("%s", line);
}
}
fclose(file);
return 0;
}
Case Study:
To understand how main() and main(int argc, char *argv[]) are related, it's crucial to grasp
the role of the operating system and the C runtime environment.
How it Works:
1. Command-Line Input:
o When you type a command in the terminal, the OS parses the input, separating the
program name from the arguments.
2. OS to C Runtime:
o The OS passes the command-line arguments to the C runtime environment.
3. C Runtime Processing:
o The C runtime:
Counts the number of arguments and stores it in argc.
Creates an array of strings (argv) where each element points to a command-
line argument.
argv[0] typically holds the program's name.
4. Calling main():
o The C runtime then calls your main() function, passing argc and argv as
arguments.
o If your main() function is defined as main(int argc, char *argv[]), it receives
these values.
o If your main() function is defined as main(), the C runtime still performs all the
steps to gather the command line arguments, but your program simply does not
utilize them.
Key Points:
Whether you use main() or main(int argc, char *argv[]), the OS and C runtime
handle the command-line arguments.
The difference lies in whether your program chooses to access and use those arguments.
Therefore, main() is essentially a version of main(int argc, char *argv[]) where the
arguments are not used.
The C standard allows for these variations of the main function.
The system, through the C runtime environment, always sets up the command line
arguments, so they are there, it is just a matter of if the programmer chooses to use them.
The C runtime ensures that the command-line arguments are available; it's up to the programmer
to decide whether to use them within the main() function.
Enumeration (enum) :
What is Enumeration in C?
Enumeration (enum) in C is a user-defined data type used to assign named integer constants
to a set of values, improving code readability and maintainability.
Syntax of Enumeration
enum enum_name {
constant1,
constant2,
constant3
};
#include <stdio.h>
enum Days{ MONDAY=1, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
=0};
int main() {
enum Days today;
today = WEDNESDAY;
Explanation:
By default, enum assigns integer values starting from 0 (i.e., SUNDAY = 0, MONDAY = 1,
TUESDAY = 2, ...).
Instead of if(day == 1), we can write if(day == MONDAY), making the code self-
explanatory.
Easier Maintenance
If values change, you only need to modify them in the enum definition.
For example:
This ensures that variables of enum type are not assigned arbitrary values, maintaining data
integrity.
enum Status {
SUCCESS = 1,
FAILURE = -1,
PENDING = 0
};
#include <stdio.h>
int main() {
enum Status result = SUCCESS;
if (result == SUCCESS) {
printf("Operation was successful!\n");
}
return 0;
}
Output:
1.Menu-Driven Programs
int main() {
enum Weekdays today = Wed;
printf("Today is day number %d\n", today); // Output: 2 (since Mon=0)
return 0;
}
Explanation:
int main() {
handle_response(OK); // Output: Request succeeded!
handle_response(NotFound); // Output: Error code: 404
return 0;
}
Explanation:
int main() {
control_light(RED); // Output: STOP
control_light(GREEN); // Output: GO
return 0;
}
Explanation:
enum Permissions {
READ = 1 << 0, // 0001
WRITE = 1 << 1, // 0010
EXECUTE = 1 << 2 // 0100
};
int main() {
int user_perms = READ | WRITE; // 0011 (3 in decimal)
if (user_perms & READ) {
printf("User has read access.\n");
}
return 0;
}
Explanation:
int main() {
Direction dir = EAST;
printf("Direction: %s\n", direction_to_string(dir)); // Output: East
return 0;
}
Explanation:
void display_menu() {
printf("\n1. Add\n2. Delete\n3. Edit\n0. Exit\n");
}
int main() {
enum MenuOption choice;
do {
display_menu();
printf("Enter choice: ");
scanf("%d", &choice);
switch (choice) {
case ADD: printf("Adding...\n"); break;
case DELETE: printf("Deleting...\n"); break;
case EDIT: printf("Editing...\n"); break;
}
} while (choice != EXIT);
return 0;
}
Explanation:
int main() {
ErrorCode err = process_file("data.txt");
if (err == SUCCESS) {
printf("File processed!\n");
} else {
printf("Error code: %d\n", err);
}
return 0;
}
Explanation:
int main() {
struct Task task = {1, PENDING};
union Data data;
data.num = 65;
printf("Task %d status: %d\n", task.id, task.status); // Output: 1, 0
printf("Union str: %s\n", data.str); // Output: "A" (ASCII 65)
return 0;
}
Explanation:
Key Takeaways
Types of Files in C
In C programming, files are broadly classified into two types:
1.Text Files
2.Binary Files
Each type is used for different purposes based on the data format and how the system processes
the file.
1. Text Files
A text file stores data in human-readable ASCII characters. These files can be opened and edited
using a text editor (like Notepad, VS Code).
2. Binary Files
A binary file stores data in machine-readable binary format (0s and 1s). These files are faster
and take up less storage space compared to text files.
In a text file, data is stored as ASCII or Unicode characters. Each character is represented using
its ASCII value (1 byte per character).
Example:
✔ The integer 123 is stored as three separate ASCII characters in the file (49 50 51 in
binary).
A binary file stores raw memory representation of data, meaning the data is stored exactly as it
is in RAM.
Example:
When writing an integer (123), C converts it to ASCII (49 50 51) before storing it in a
text file.
When reading, C converts ASCII characters back to an integer (123).
When writing an integer (123), C stores the raw binary data (01111011).
When reading, C directly loads the binary representation into memory without
conversion.
Difference Between Standard Input/Output (Console) and File
Input/Output in C
In C, we can read and write data using standard input/output (console) or file input/output (disk
files). The way they work is fundamentally different.
int main() {
int num;
return 0;
}
The console (or terminal) acts as an interface between the user and the program. In C, the
standard input (stdin) and standard output (stdout) streams are automatically available when a
program starts executing. The connection between a C program and the console is managed by
the Operating System (OS), using file descriptors, buffers, and system calls.
1. Console I/O Connection in C
printf("Hello, world!\n");
or
scanf("%d", &x);
internally, the OS redirects these calls to read from or write to standard I/O streams (stdin,
stdout, stderr). These streams are connected to the terminal window (console).
The OS provides each running program with three default file streams:
o stdin (Standard Input - Keyboard)
o stdout (Standard Output - Screen)
o stderr (Standard Error - Screen)
When the program starts, these streams are already open and ready to use.
Input from the user is temporarily stored in a buffer by the OS until the program reads
it.
Output from the program is also stored in a buffer before being displayed on the
screen.
Operating systems provide system calls to handle console input and output. In Linux/Unix, printf
and scanf internally use write() and read() system calls:
#include <stdio.h>
#include <unistd.h> // For low-level system calls
int main() {
write(1, "Hello, World!\n", 14); // File descriptor 1 = stdout
return 0;
}
Explanation:
write(1, "Hello, World!\n", 14) directly writes to stdout (file descriptor 1).
1 is the file descriptor for the console output.
"Hello, World!\n" is written to the console.
Example: How scanf() Works Internally
#include <stdio.h>
#include <unistd.h>
int main() {
char buffer[20];
read(0, buffer, 20); // File descriptor 0 = stdin
printf("You entered: %s", buffer);
return 0;
}
Explanation:
read(0, buffer, 20); reads up to 20 bytes from stdin (file descriptor 0).
The user input is stored in buffer and then printed.
Yes, but the implementation differs based on the programming language and OS.
Programming
Console I/O Mechanism
Language
Uses printf(), scanf(), puts(), gets(), internally calls write(),
C
read().
C++ Uses cin, cout, which internally use printf() and scanf().
Uses input() (stdin) and print() (stdout), which interact with
Python
system calls.
Uses System.in, System.out, which are connected to the OS I/O
Java
streams.
JavaScript (Node.js) Uses console.log() for output and process.stdin for input.
The way the console is connected to the program depends on the OS.
Windows OS:
MacOS:
Yes! Since the console is treated as a file, you can redirect input/output:
./program1 | ./program2
Conclusion
✔ The console is directly connected to the program via stdin, stdout, stderr.
✔ OS manages I/O via system calls (write(), read()).
✔ All programming languages provide built-in functions that internally use system calls.
✔ Each OS has a different mechanism, but the fundamental process is the same.
✔ Console I/O is temporary, while File I/O provides permanent storage.
FILE in C:
<stdio.h> (Standard Input/Output Header) is a library in C that provides functions for handling
input and output operations, including console I/O and file I/O.
The FILE data type is defined in <stdio.h>, and it is used to represent a file stream in C. It is
declared as:
This abstracts the file system, allowing programs to read and write data to files using various
functions.
Mode Description
"r" Opens file for reading (must exist).
"w" Creates a new file or overwrites an existing file.
"a" Opens file for appending (adds content at the end).
"r+" Opens for reading and writing (doesn't create a new file).
"w+" Opens for reading and writing, but overwrites if the file exists.
"a+" Opens for reading and appending (keeps existing content).
1. Character I/O in C
Character I/O refers to reading and writing data one character at a time in text files. It is simple,
human-readable, and widely used for storing structured data like CSV files, logs, and reports.
FILE *file;
file = fopen("data.txt", "w"); // Opens "data.txt" in write mode
if (file == NULL) {
printf("Error opening file!");
return 1;
}
fclose(file); // Closes the file
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w"); // Open file in write mode
if (file == NULL) {
printf("Error opening file!");
return 1;
}
How It Works?
The ASCII value of 'A' (65) is converted to binary (01000001) and stored.
How is it stored?
File: output.txt
Hexadecimal: 41 0A (41 = 'A', 0A = newline)
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "r"); // Open file in read mode
if (file == NULL) {
printf("Error opening file!");
return 1;
}
char ch;
while ((ch = fgetc(file)) != EOF) { // Read until End-of-File
printf("%c", ch);
}
How It Works?
Encoding in Memory
#include <stdio.h>
int main() {
FILE *file = fopen("message.txt", "w");
if (file == NULL) {
printf("Error opening file!");
return 1;
}
fputs("Hello, File Handling!", file);
fclose(file);
return 0;
}
How It Works?
Storage in File
File: message.txt
Hex: 48 65 6C 6C 6F 2C 20 46 69 6C 65 20 48 61 6E 64 6C 69 6E 67
#include <stdio.h>
int main() {
FILE *file = fopen("message.txt", "r");
if (file == NULL) {
printf("Error opening file!");
return 1;
}
char line[100];
fgets(line, 100, file); // Reads max 100 chars
fclose(file);
return 0;
}
How It Works?
Storage Format
This program takes the filename from the command line and reads characters from standard
input (keyboard) until the user enters EOF (Ctrl+D in Linux/Mac or Ctrl+Z in Windows),
then writes them to the specified file.
Key Concepts
C Program
#include <stdio.h>
char ch;
printf("Enter text (Press Ctrl+D to stop input on Linux/Mac or Ctrl+Z on
Windows):\n");
fclose(file);
printf("\nData written to file '%s' successfully!\n", argv[1]);
return 0;
}
How to Compile and Run
./program output.txt
cat output.txt
Output in output.txt
Explanation
Now, running the program again adds new text instead of deleting old content.
7. C Program Using fgets() and fputs() with Character Arrays
This program:
C Program
#include <stdio.h>
FILE *file;
char buffer[100]; // Character array to store input
// Write to file
fputs(buffer, file);
fclose(file);
printf("Data written to file successfully!\n");
fclose(file);
return 0;
}
Parameters:
Parameters:
fputs(buffer, file);
3.Input Text
Enter a line of text: Hello, World!
4.Output
Data written to file successfully!
Reading from file:
Hello, World!
This program:
1. Takes the source and destination filenames from the command line.
2. Reads the contents from the source file.
3. Writes the contents to the destination file.
4. Closes both files after copying.
C Program
#include <stdio.h>
return 0;
}
Explanation
1.Opening Files
3.Error Handling
4.Closing Files
fclose(sourceFile);
fclose(destFile);
Ensures that file resources are released.
Sample Execution
3.Output
File copied successfully from source.txt to destination.txt
Syntax:
printf("format string", arguments);
Example:
#include <stdio.h>
int main() {
int age = 22;
float height = 5.9;
char name[] = "Sofiya";
return 0;
}
Output:
Name: Sofiya
Age: 22 years
Height: 5.9 feet
Syntax:
fprintf(FILE *file_pointer, "format string", arguments);
Example:
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
int id = 101;
float marks = 95.6;
fclose(file);
printf("Data written to file successfully.\n");
return 0;
}
Output in output.txt:
Student ID: 101
Marks: 95.60
3.sprintf() – Store Formatted Data in a String
Syntax:
sprintf(char *str, "format string", arguments);
Example:
#include <stdio.h>
int main() {
char buffer[100];
int num = 50;
float pi = 3.14159;
return 0;
}
Output:
The value of pi is 3.14 and num is 50
Syntax:
scanf("format string", &variable);
Example:
#include <stdio.h>
int main() {
int age;
float weight;
char name[30];
printf("Name: %s, Age: %d, Weight: %.2f kg\n", name, age, weight);
return 0;
}
Input:
Sofiya 22 58.5
Output:
Name: Sofiya, Age: 22, Weight: 58.50 kg
Example:
#include <stdio.h>
int main() {
FILE *file = fopen("input.txt", "r");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
char name[30];
int age;
float gpa;
fclose(file);
return 0;
}
Example:
#include <stdio.h>
int main() {
char data[] = "Sofiya 22 9.39";
char name[30];
int age;
float gpa;
return 0;
}
Output:
Name: Sofiya, Age: 22, GPA: 9.39
Format Specifiers
Summary
2. Binary I/O in C
Binary Input/Output (I/O) in C deals with reading and writing raw binary data to and from files.
Unlike text I/O, which processes data as human-readable characters, binary I/O works with raw
byte sequences, making it faster and more efficient for large or complex data structures.
Binary files in C are handled using fopen(), fwrite(), fread(), fseek(), ftell(), and
fclose().
Function Description
fopen() Opens a file in binary mode ("rb", "wb", "ab", etc.).
fwrite()Writes data to a binary file.
fread() Reads data from a binary file.
fseek() Moves the file pointer to a specific location.
Function Description
ftell() Returns the current position of the file pointer.
fclose() Closes an open file.
Syntax:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
int main() {
FILE *file = fopen("data.bin", "wb"); // Open file in binary write mode
if (file == NULL) {
printf("Error opening file!\n");
return 1;
}
fclose(file);
printf("Data written to binary file successfully.\n");
return 0;
}
Syntax:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
int main() {
FILE *file = fopen("data.bin", "rb"); // Open file in binary read mode
if (file == NULL) {
printf("Error opening file!\n");
return 1;
}
fclose(file);
return 0;
}
Output:
Data read from binary file:
10 20 30 40 50
Storing and Reading Structures in Binary Files
Binary I/O is particularly useful for structs because it allows direct memory storage.
struct Student {
int rollNo;
char name[30];
float marks;
};
int main() {
struct Student s1 = {101, "Sofiya", 95.5};
return 0;
}
Output:
Student Details:
Roll No: 101
Name: Sofiya
Marks: 95.50
Using fseek() to Move File Pointer
Syntax:
int fseek(FILE *stream, long offset, int whence);
whence Value Meaning
SEEK_SET Beginning of file
SEEK_CUR Current position
SEEK_END End of file
This moves the file pointer past the first two records, pointing at the third record.
The ftell() function tells the current position of the file pointer.
Syntax:
long ftell(FILE *stream);
Example:
long position = ftell(file);
printf("Current File Pointer Position: %ld\n", position);
// Main function
int main() {
FILE *file = fopen("students.bin", "rb+");
if (file == NULL) {
file = fopen("students.bin", "wb+"); // Create if not exists
if (file == NULL) {
printf("Error opening file!\n");
return 1;
}
}
switch (choice) {
case 1:
addStudent(file);
break;
case 2:
displayStudents(file);
break;
case 3:
printf("Enter Roll No to Search: ");
scanf("%d", &rollNo);
searchStudent(file, rollNo);
break;
case 4:
getFilePosition(file);
break;
case 5:
fclose(file);
return 0;
default:
printf("Invalid choice! Try again.\n");
}
}
}
Explanation of Key Functions
1.seek(file, 0, SEEK_END); → Moves file pointer to end of file before writing a new record.
2.rewind(file); → Moves file pointer back to the start of the file before reading.
3.ftell(file); → Returns current file pointer position, useful to track records.
4.fseek(file, index * sizeof(struct Student), SEEK_SET); → Jumps to specific
record in file.
Sample Run:
1. Add Student
2. Display All Students
3. Search Student by Roll No
4. Get File Position
5. Exit
Enter choice: 1
Enter choice: 2
Student Records:
Roll No: 101 | Name: Sofiya | Marks: 94.50
Enter choice: 3
Enter Roll No to Search: 101
Student Found at Position: 0
Roll No: 101 | Name: Sofiya | Marks: 94.50
Enter choice: 4
Current File Pointer Position: 24
---------------------------------The End-------------------------------
UNIT 2
A data structure is a specialized format for organizing, processing, and storing data efficiently.
The choice of data structure can affect the performance of algorithms and impact memory
usage, speed, and processing power.
If the books are arranged randomly, finding a book would take hours.
If books are arranged in an ordered manner, finding a book takes minutes.
If the books are indexed in a computer system, a search takes seconds!
1. Arrays
2. Linked Lists
Definition: A collection of nodes, where each node contains data and a pointer to the
next node.
Operations: Insertions & deletions are O(1) (efficient), but searching is O(n).
Advantages: Dynamic size (unlike arrays), no memory waste.
Types: Singly Linked List, Doubly Linked List, Circular Linked List.
Example:
struct Node {
int data;
struct Node* next;
};
3. Stack
Example:
#include <stdio.h>
#define SIZE 10
int stack[SIZE], top = -1;
void push(int x) { stack[++top] = x; }
void pop() { top--; }
4. Queue
#include <stdio.h>
#define SIZE 5
int queue[SIZE], front = -1, rear = -1;
void enqueue(int x) { queue[++rear] = x; }
void dequeue() { front++; }
1. Trees
Definition: A tree is a hierarchical structure where each node has child nodes.
Types: Binary Tree, Binary Search Tree (BST), AVL Tree, B-Trees.
Operations: Insert, Delete, Search, Traverse.
Time Complexity: Insert/Search/Delete = O(log n) in a balanced tree.
struct Node {
int data;
struct Node *left, *right;
};
2. Graphs
Example:
struct Graph {
int vertices;
int adjMatrix[10][10]; // 10x10 adjacency matrix
};
**List:
1. What is a List?
A list is a linear data structure that stores a collection of elements in a specific order. It allows
insertion, deletion, searching, and traversal of elements.
A list can be implemented in multiple ways based on how data is stored and accessed.
An array-based list stores elements contiguously in memory. It is fixed in size, and elements
are stored in continuous memory locations.
#include <stdio.h>
#define SIZE 10
int main() {
insert(0, 10);
insert(1, 20);
insert(1, 15); // Insert 15 at index 1
display(); // Output: 10 15 20
return 0;
}
#include <stdio.h>
#define SIZE 10
int list[SIZE];
int count = 0;
int main() {
list[count++] = 10;
list[count++] = 20;
list[count++] = 30;
display(); // Output: 10 20 30
delete(1); // Delete element at index 1
display(); // Output: 10 30
return 0;
}
#include <stdio.h>
#define SIZE 10
int main() {
int index = search(30);
if (index != -1)
printf("Element found at index %d\n", index);
else
printf("Element not found\n");
return 0;
}
#include <stdio.h>
int main() {
int list[] = {10, 20, 30, 40, 50};
int size = sizeof(list) / sizeof(list[0]);
#include <stdio.h>
#define SIZE 10
int main() {
update(2, 100); // Update index 2 with 100
for (int i = 0; i < count; i++) {
printf("%d ", list[i]);
}
printf("\n");
return 0;
}
Advantages & Disadvantages of Array-Based Lists
✔ Advantages
Disadvantages
If dynamic resizing and frequent insertions/deletions are needed, use linked lists instead.
In many programming scenarios, arrays are not sufficient due to their fixed size, inefficient
insertions/deletions, and memory usage. This is where linked lists come in.
Limitations of Arrays:
Fixed Size: Once an array is declared, its size is fixed, leading to either wasted memory
(if the array is too large) or memory shortage (if the array is too small).
Expensive Insertions/Deletions: Inserting or deleting an element in an array requires
shifting multiple elements, which is time-consuming (O(n) complexity).
Contiguous Memory: Arrays require memory to be allocated contiguously, which may
not always be available.
A linked list is a linear data structure where elements (called nodes) are not stored in
contiguous memory locations. Each node contains:
1.Data – The actual value stored in the node.
2.Pointer (next) – A reference to the next node in the list.
1. Singly Linked List – Each node has a single pointer to the next node.
2. Doubly Linked List – Each node has pointers to both the next and previous nodes.
3. Circular Linked List – The last node points back to the first node, forming a circular
chain.
1. Music Playlist
3. Undo/Redo Functionality
Text editors and graphic software use Linked Lists to store different versions of a
document.
5. Task Scheduling in OS 🖥
Operating Systems use Circular Linked Lists for CPU scheduling in Round Robin.
Key Differences Between Arrays & Linked Lists
This program covers all possible operations on a Singly Linked List, including:
Insertion (at beginning, at end, at a specific position)
Deletion (at beginning, at end, at a specific position)
Searching (to find an element)
Reversing the linked list
Displaying the linked list
Counting the number of nodes
#include <stdio.h>
#include <stdlib.h>
// Node structure
struct Node {
int data;
struct Node* next;
};
// Main function
int main() {
struct Node* head = NULL;
int choice, value, position;
while (1) {
printf("\n--- Singly Linked List Operations ---\n");
printf("1. Insert at Beginning\n2. Insert at End\n3. Insert at
Position\n");
printf("4. Delete from Beginning\n5. Delete from End\n6. Delete from
Position\n");
printf("7. Search an Element\n8. Reverse Linked List\n9. Display
Linked List\n");
printf("10. Count Nodes\n11. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("Enter value to insert: ");
scanf("%d", &value);
insertAtBeginning(&head, value);
break;
case 2:
printf("Enter value to insert: ");
scanf("%d", &value);
insertAtEnd(&head, value);
break;
case 3:
printf("Enter value and position: ");
scanf("%d %d", &value, &position);
insertAtPosition(&head, value, position);
break;
case 4:
deleteFromBeginning(&head);
break;
case 5:
deleteFromEnd(&head);
break;
case 6:
printf("Enter position to delete: ");
scanf("%d", &position);
deleteFromPosition(&head, position);
break;
case 7:
printf("Enter value to search: ");
scanf("%d", &value);
search(head, value);
break;
case 8:
reverse(&head);
break;
case 9:
display(head);
break;
case 10:
printf("Total Nodes: %d\n", countNodes(head));
break;
case 11:
printf("Exiting program...\n");
exit(0);
default:
printf("Invalid choice! Try again.\n");
}
}
return 0;
}
Understanding Stacks in Data Structures
1. What is a Stack?
A Stack is a linear data structure that follows the LIFO (Last In, First Out) principle. This
means that the last element added to the stack is the first one to be removed.
2. Why Do We Need a Stack When We Already Have Arrays and Linked Lists?
Key Difference:
Arrays and Linked Lists allow accessing any element at any time, but stacks restrict
access to only the top element (Last In, First Out).
This restriction helps in specific problem-solving scenarios like function calls,
recursion, undo operations, and backtracking.
It only defines operations (PUSH, POP, PEEK) without specifying how they are
implemented.
The underlying implementation can be done using arrays or linked lists, but users only
care about the operations, not how the stack is built internally.
Example:
Imagine a vending machine:
You press a button (operation) to get a snack.
You don't need to know how the machine internally processes your request.
Similarly, in a stack:
You PUSH, POP, and PEEK without worrying about whether it's implemented using an
array or linked list.
Function Calls (Recursion) → The Call Stack manages function calls in programming.
Undo/Redo Operations → Used in text editors like MS Word.
Browser History → Backtracking in web browsers.
Expression Evaluation → Converting infix to postfix, evaluating expressions.
Backtracking Algorithms → Solving mazes, Sudoku, etc.
// Main function
int main() {
struct Stack s;
initialize(&s); // Initialize the stack
while (1) {
printf("\nSTACK OPERATIONS:\n");
printf("1. Push\n2. Pop\n3. Peek\n4. Display\n5. Exit\nEnter your
choice: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("Enter value to push: ");
scanf("%d", &value);
push(&s, value);
break;
case 2:
value = pop(&s);
if (value != -1) {
printf("Popped element: %d\n", value);
}
break;
case 3:
value = peek(&s);
if (value != -1) {
printf("Top element: %d\n", value);
}
break;
case 4:
display(&s);
break;
case 5:
printf("Exiting program.\n");
exit(0);
default:
printf("Invalid choice! Try again.\n");
}
}
return 0;
}
// Push operation
void push(int value) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
if (!newNode) {
printf("Stack Overflow!\n");
return;
}
newNode->data = value;
newNode->next = top;
top = newNode;
printf("Pushed %d to stack.\n", value);
}
// Pop operation
int pop() {
if (isEmpty()) {
printf("Stack Underflow! Cannot pop.\n");
return -1;
}
// Peek operation
int peek() {
if (isEmpty()) {
printf("Stack is empty.\n");
return -1;
}
return top->data;
}
// Display stack
void display() {
if (isEmpty()) {
printf("Stack is empty.\n");
return;
}
// Main function
int main() {
int choice, value;
while (1) {
printf("\n--- Stack Menu ---\n");
printf("1. Push\n2. Pop\n3. Peek\n4. Display\n5. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("Enter value to push: ");
scanf("%d", &value);
push(value);
break;
case 2:
value = pop();
if (value != -1)
printf("Popped value: %d\n", value);
break;
case 3:
value = peek();
if (value != -1)
printf("Top element: %d\n", value);
break;
case 4:
display();
break;
case 5:
printf("Exiting program...\n");
exit(0);
default:
printf("Invalid choice! Try again.\n");
}
}
return 0;
}
Output Example
--- Stack Menu ---
1. Push
2. Pop
3. Peek
4. Display
5. Exit
Enter your choice: 1
Enter value to push: 10
Pushed 10 to stack.
A Stack is a Last-In-First-Out (LIFO) data structure, meaning the last element inserted is the
first one to be removed. Due to this behavior, stacks are widely used in various real-world
applications.
How it Works?
When a function is called, the system stores return addresses and local variables in a
stack (call stack).
When the function completes, the return address is popped to resume execution from
where it left off.
Each call to factorial(n) is pushed onto the stack. Once n == 0, the function calls start
returning, popping from the stack.
Real-Time Example:
✔ Function Calls in C, Java, Python
✔ Recursive Algorithms (Factorial, Fibonacci, Tower of Hanoi)
2. Undo/Redo Operations
How it Works?
Applications like Microsoft Word, Photoshop, Code Editors (VS Code) use a stack to
store actions.
Undo (Ctrl + Z) → Pops the last action from the stack and restores the previous state.
Redo (Ctrl + Y) → Pushes the undone action back onto the stack.
Real-Time Example:
✔ Text Editors (Undo/Redo Feature)
✔ Image Editing (Revert Previous Actions)
3. Backtracking Algorithms
How it Works?
Real-Time Example:
✔ Maze Solving
✔ Puzzle Solving (Sudoku, N-Queens)
✔ Pathfinding Algorithms (DFS - Depth First Search)
How it Works?
Postfix:
5 3 + 8 * = (5 + 3) * 8 = 64
Steps:
1. Push 5
2. Push 3
3. + → Pop 3 and 5, compute 5+3=8, push 8
4. Push 8
5. * → Pop 8 and 8, compute 8*8=64
Real-Time Example:
✔ Compilers (Expression Parsing)
✔ Calculators
How it Works?
Example:
Valid Expression: ( [ a + b ] * c )
( → Push
[ → Push
] → Pop [ (matched )
) → Pop ( (matched )
Real-Time Example:
✔ Code Editors (Syntax Checking)
✔ XML / HTML Tag Matching
How it Works?
Every time you visit a webpage, the URL is pushed onto a stack.
When you click Back, the last visited page is popped.
If you click Forward, it is pushed back.
Real-Time Example:
✔ Web Browsers (Chrome, Firefox, Edge)
✔ Navigation Systems
7. Memory Management (Stack Memory Allocation)
How it Works?
Real-Time Example:
✔ Program Execution (Function Calls)
✔ Stack Overflow Errors (Infinite Recursion)
8. Reversing a String
How it Works?
Example:
Input: "HELLO"
Stack: H → E → L → L → O
Output: "OLLEH"
Real-Time Example:
✔ String Reversal in Programming
How it Works?
The Tower of Hanoi problem uses a stack to move disks between rods following the
LIFO principle.
Real-Time Example:
✔ Algorithmic Problem Solving
✔ AI and Robotics
Differences: Stack vs Other Data Structures
Summary
✔ Stack is needed when we require a Last-In-First-Out (LIFO) approach.
✔ Used in Function Calls, Undo/Redo, Expression Parsing, Backtracking, Memory
Management, Navigation, and more.
✔ Implemented using Arrays or Linked Lists.
Examples of Expressions:
1. Arithmetic Expression: 5 + 3 * 2
2. Logical Expression: a > b && b < c
3. Relational Expression: x == y
Example:
A+B
A+B*C-D/E
In infix notation, we use operator precedence and associativity to determine the order of
operations.
Even though humans prefer infix expressions, computers struggle with them due to ambiguity
and need for parentheses.
In postfix notation (Reverse Polish Notation - RPN), the operator appears after the
operands.
Example:
A + B → AB+
(A + B) * C → AB+C*
No Parentheses Needed
No Operator Precedence Issues
Can be evaluated in a single pass using a stack.
Postfix: 5 3 + 8 * (equivalent to (5 + 3) * 8)
Steps:
In prefix notation (Polish Notation), the operator appears before the operands.
Example:
A + B → +AB
(A + B) * C → *+ABC
No Parentheses Needed
No Operator Precedence Issues
Can be evaluated from Right to Left using a stack.
Prefix: * + 5 3 8 (equivalent to (5 + 3) * 8)
Steps:
Prefix Uses
Summary
Infix Notation → Easy for humans, but requires parentheses & precedence rules.
Postfix Notation → No parentheses needed, efficient evaluation with stacks.
Prefix Notation → Similar to postfix but evaluated right to left.
Why Convert? → Simplifies parsing, avoids precedence issues, and is machine-friendly.
Example :
C program that converts an infix expression to a postfix expression using a stack, along with
detailed step-by-step explanations for each part.
Concepts Recap:
Algorithm Steps:
1. If the character is an operand, add it directly to the postfix string.
2. If the character is an operator, push it to the stack after popping operators with higher or
equal precedence.
3. If it’s '(', push to stack.
4. If it’s ')', pop and add to postfix until '(' is found.
5. After the entire input is read, pop all remaining operators from the stack.
// Stack structure
char stack[MAX];
int top = -1;
// Main function
int main() {
char infix[MAX], postfix[MAX];
infixToPostfix(infix, postfix);
Function Breakdown:
Example Expression:
Infix: A + B * C - D / E
Expected: ABC*+DE/-
Operator Precedence Table:
We'll use:l
Initial State:
postfix = ""
stack = []
Step 1: Read A
Step 2: Read +
Step 3: Read B
Step 5: Read C
Step 6: Read -
Step 7: Read D
Step 8: Read /
Final Output:
Postfix: ABC*+DE/-
Diagram Table:
Postfix: 23*54*+9-
You evaluate it left to right, using a stack:
Expression: 23*54*+9-
Step by step:
1. Push 2
2. Push 3
3. * → 2*3 = 6 → Push 6
4. Push 5
5. Push 4
6. * → 5*4 = 20 → Push 20
7. + → 6+20 = 26 → Push 26
8. Push 9
9. - → 26-9 = 17 → Final result = 17
int stack[MAX];
int top = -1;
switch (c) {
case '+': result = val1 + val2; break;
case '-': result = val1 - val2; break;
case '*': result = val1 * val2; break;
case '/': result = val1 / val2; break;
}
push(result);
}
else {
printf("Invalid character in expression: %c\n", c);
exit(1);
}
i++;
}
int main() {
char postfix[MAX];
push() / pop()
isdigit(c)
evaluatePostfix()
Sample Input/Output:
Input:
Postfix: 23*54*+9-
Output:
Result: 17
Queue ADT (Abstract Data Type)
➤ Definition:
A Queue is a linear data structure that follows the FIFO (First In, First Out) principle. The
element inserted first is removed first.
➤ Real-life Examples:
Operation Description
enqueue(x) Insert an element x at the rear of the queue.
dequeue() Remove and return the element from the front of the queue.
peek() or front() Return the front element without removing it.
isEmpty() Checks if the queue is empty.
isFull() (for array implementation) Checks if the queue is full.
➤ Queue Terminology:
➤ Concept:
➤ Initial Conditions:
front = -1;
rear = -1;
C Program for Queue using Arrays (Linear Queue)
#include <stdio.h>
#define MAX 5
int queue[MAX];
int front = -1, rear = -1;
do {
printf("\n LINEAR QUEUE MENU \n");
printf("1. Enqueue\n2. Dequeue\n3. Display\n4. Exit\nEnter your
choice: ");
scanf("%d", &choice);
switch(choice) {
case 1: printf("Enter value to enqueue: ");
scanf("%d", &val);
enqueue(val);
break;
case 2: dequeue(); break;
case 3: display(); break;
case 4: printf(" Exiting...\n"); break;
default: printf(" Invalid choice!\n");
}
} while(choice != 4);
return 0;
}
Implementation of Queue using Linked List
➤ Concept:
➤ Structure:
struct Node {
int data;
struct Node* next;
};
➤ Maintain:
➤ Operations:
Enqueue:
Dequeue:
struct Node {
int data;
struct Node* next;
};
if (rear == NULL)
front = rear = newNode;
else {
rear->next = newNode;
rear = newNode;
}
printf(" %d inserted to queue.\n", val);
}
void dequeue() {
if (front == NULL)
printf(" Queue Underflow!\n");
else {
struct Node* temp = front;
printf(" Deleted: %d\n", temp->data);
front = front->next;
if (front == NULL)
rear = NULL;
free(temp);
}
}
void display() {
if (front == NULL)
printf(" Queue is Empty!\n");
else {
struct Node* temp = front;
printf(" Queue elements: ");
while (temp != NULL) {
printf("%d ", temp->data);
temp = temp->next;
}
printf("\n");
}
}
int main() {
int choice, val;
do {
printf("\n LINKED LIST QUEUE MENU \n");
printf("1. Enqueue\n2. Dequeue\n3. Display\n4. Exit\nEnter your
choice: ");
scanf("%d", &choice);
switch(choice) {
case 1: printf("Enter value to enqueue: ");
scanf("%d", &val);
enqueue(val);
break;
case 2: dequeue(); break;
case 3: display(); break;
case 4: printf(" Exiting...\n"); break;
default: printf(" Invalid choice!\n");
}
} while(choice != 4);
return 0;
}
Circular Queue
In a linear array queue, once rear == MAX-1, the queue is considered full even if front
> 0.
➤ Solution:
A Circular Queue is a linear data structure that connects the last position back to the first position
in a circular manner. It overcomes the limitation of a linear array queue where space at the front
cannot be reused after dequeuing.
➤ Initial Conditions:
front = -1;
rear = -1;
➤ isFull condition:
➤ isEmpty condition:
(front == -1)
Visual Behavior
Indexes:
0 1 2 3 4
+---+---+---+---+---+
| | | | | | <- Circular Array
+---+---+---+---+---+
Example Operations:
1. Initial State:
2. Enqueue 10:
front = 0, rear = 0
Queue: [10, _, _, _, _]
front moves to 1
Queue: [10 (X), 20, 30, 40, _]
↑
front
Circular Queue in C
#include <stdio.h>
#define MAX 5
int queue[MAX];
int front = -1, rear = -1;
void dequeue() {
if (front == -1) {
printf("Queue is empty (Underflow).\n");
} else {
printf("Deleted element: %d\n", queue[front]);
if (front == rear) {
// Queue has only one element
front = rear = -1;
} else {
front = (front + 1) % MAX;
}
}
}
void display() {
if (front == -1) {
printf("Circular Queue is empty.\n");
} else {
printf("Circular Queue elements are: ");
int i = front;
while (1) {
printf("%d ", queue[i]);
if (i == rear)
break;
i = (i + 1) % MAX;
}
printf("\n");
}
}
int main() {
int choice, value;
do {
printf("\nCIRCULAR QUEUE MENU\n");
printf("1. Enqueue\n");
printf("2. Dequeue\n");
printf("3. Display\n");
printf("4. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("Enter value to insert: ");
scanf("%d", &value);
enqueue(value);
break;
case 2:
dequeue();
break;
case 3:
display();
break;
case 4:
printf("Exiting the program.\n");
break;
default:
printf("Invalid choice. Please try again.\n");
}
return 0;
}
Summary Table of Index Changes
Operation front rear Queue Content
Initial -1 -1 [_____]
Enqueue 10 0 0 [10 _ _ _ _ ]
Enqueue 20 0 1 [10 20 _ _ _ ]
Enqueue 30 0 2 [10 20 30 _ _ ]
Dequeue 1 2 [ X 20 30 _ _ ]
Enqueue 40 1 3 [ X 20 30 40 _ ]
Enqueue 50 1 4 [ X 20 30 40 50 ]
Enqueue 60 1 0 [60 20 30 40 50 ] (wrapped)
Comparison Table
What is a Tree?
A Tree is a non-linear, hierarchical data structure used to represent data with a parent-child
relationship. Unlike arrays, linked lists, stacks, and queues which are linear in nature, a tree allows
branching of data in a structured way.
Tree Structure
A (Root)
/ \
B C
/ \ \
D E F
Root Node: A
Internal Nodes: B, C
Leaf Nodes: D, E, F
Edges: A→B, A→C, B→D, B→E, C→F (5 total)
Subtrees: Tree rooted at B or C
Levels: A (level 0), B/C (level 1), D/E/F (level 2)
Feature Description
Non-linear Nodes are connected in a hierarchy, not a sequence.
Hierarchical Each node can have sub-nodes (children), forming a hierarchy.
Rooted Structure A single node acts as the entry point (root).
Recursive Nature Each subtree is itself a tree (enables recursive processing).
Feature Description
Directed Edges Each connection (edge) shows a direction from parent to child.
A tree is made of a node and its subtrees, each of which is also a tree.
This recursive property makes it easy to implement many tree operations using recursion
(like traversals).
Advantages of Trees
Disadvantages of Trees
Summary Points
Tree Terminology
Understanding the terminology of trees is essential for learning how trees work and for building
advanced tree-based data structures like Binary Search Trees, Heaps, and Tries.
1. Node
2. Root
3. Edge
Example:
A
/ \
B C
A is parent of B and C.
5. Siblings
7. Internal Node
Ancestor: Any node above the current node (towards the root).
Descendant: Any node below the current node (towards the leaves).
9. Subtree
10. Path
Example:
Level 0 → A
Level 1 → B, C
Level 2 → D, E, F
A ← Height 2
/ \
B C ← Height 1
/
D ← Height 0
Example:
A ← Depth 0
/ \
B C ← Depth 1
/
D ← Depth 2
17. Forest
When we want to use trees in programming or store them in memory, we must represent them
using appropriate data structures. There are three major ways to represent trees in user programs:
Structure Example:
Node (Index) 0 1 2 3 4 5
Parent[] -1 0 0 1 1 2
Tree Representation:
0
/ \
1 2
/ \ \
3 4 5
Advantages:
Disadvantages:
Concept:
The data/value
A pointer to its first child
A pointer to its next sibling
Representation:
A
/ \
B C
/ \
D E
Will be stored as:
A -> firstChild → B
B -> firstChild → D
D -> nextSibling → E
B -> nextSibling → C
Advantages:
Disadvantages:
Concept:
A left child
A right child
Node Structure in C:
struct Node {
int data;
struct Node* left;
struct Node* right;
};
This structure is specifically for binary trees, where each node can have at most 2 children.
Tree:
10
/ \
20 30
/ \
40 50
Represented as:
Advantages:
Disadvantages:
arr[0] is root
For node at index i:
o Left child = 2*i + 1
o Right child = 2*i + 2
o Parent = Floor((i - 1) / 2)
Example Tree:
10
/ \
20 30
/ \
40 50
Summary
Method Best For Key Pointers
Parent Array Simple tree with fast parent access Only parent info stored
Linked List General trees Child & sibling pointers
Binary Struct Binary Trees Left & right pointers
Array (Heap) Complete Binary Trees Calculated indices
Formal Definition:
Example:
1
/ \
2 3
/ \ / \
4 5 6 7
Perfect Binary Tree
A full binary tree in which all leaf nodes are at the same level.
Example:
1
/ \
2 3
/ \ / \
4 5 6 7
All levels are completely filled except possibly the last, and the last level is filled from left to
right.
Example:
1
/ \
2 3
/ \ /
4 5 6
A binary tree that degenerates to a linked list due to having only one child at each level.
Types:
Left Skewed:
1
/
2
/
3
Right Skewed:
1
\
2
\
3
A binary tree is balanced if the difference in height between the left and right subtree of every
node is not more than 1.
Summary
Binary Tree: At most 2 children per node.
Special Types:
o Full: 0 or 2 children
o Perfect: Full + equal leaf depth
o Complete: All levels full except last
o Skewed: Like a linked list
o Balanced: Height difference ≤ 1
Used in searching, sorting, compression, decision-making, and more.
Properties of Binary Trees
Understanding the mathematical and structural properties of binary trees is essential to estimate
memory usage, traversal time, number of nodes, and height-related constraints.
Binary Tree Traversals
Traversal means visiting every node in a specific order to process (read/print) the values stored in
the binary tree.
Since trees are non-linear, we need a well-defined strategy to systematically visit all nodes.
Inorder (LNR)
Preorder (NLR)
Postorder (LRN)
Left subtree
Node (Root)
Right subtree
Example:
Given:
A
/ \
B C
/ \
D E
Inorder: D B E A C
Node (Root)
Left subtree
Right subtree
Example:
Same tree:
A
/ \
B C
/ \
D E
Preorder: A B D E C
Left subtree
Right subtree
Node (Root)
Example:
Same tree:
A
/ \
B C
/ \
D E
Postorder: D E B C A
Real-World Analogies
Preorder: You enter a building (root), go left room to right room.
Inorder: You explore left wing, then center, then right wing.
Postorder: You clean up each room, then leave the building last.
The left subtree of a node contains only nodes with keys less than the node's key.
The right subtree of a node contains only nodes with keys greater than the node's key.
Both the left and right subtrees must also be binary search trees.
No duplicate nodes are typically allowed.
Example: Inserting the values: 50, 30, 70, 20, 40, 60, 80 results in:
50
/ \
30 70
/ \ / \
20 40 60 80
2. Binary Search Tree ADT (Abstract Data Type)
ADT Definition:
Data:
Operations:
insert(tree, key)
delete(tree, key)
search(tree, key)
inorder(tree)
preorder(tree)
postorder(tree)
These operations hide the implementation details and expose functionality only.
3. Applications of BST
Dictionary implementations
Symbol tables in compilers
Dynamic sorting and ranking systems
Auto-complete and suggestion systems
Memory management (free list organization)
// Iterative Insertion
Node* insert(Node* root, int key) {
Node* newNode = createNode(key);
if (root == NULL) return newNode;
Node* curr = root;
Node* parent = NULL;
return root;
}
// Iterative Search
Node* search(Node* root, int key) {
while (root != NULL) {
if (key == root->data) return root;
else if (key < root->data) root = root->left;
else root = root->right;
}
return NULL;
}
while (!isEmpty(&s)) {
Node* curr = pop(&s);
printf("%d ", curr->data);
if (curr->right) push(&s, curr->right);
if (curr->left) push(&s, curr->left);
}
}
while (!isEmpty(&s1)) {
Node* curr = pop(&s1);
push(&s2, curr);
if (curr->left) push(&s1, curr->left);
if (curr->right) push(&s1, curr->right);
}
while (!isEmpty(&s2)) {
printf("%d ", pop(&s2)->data);
}
}
However, insertion, search, and deletion do not need stack — they use simple while loops and
pointers.
// Recursive Insertion
Node* insert(Node* root, int key) {
if (root == NULL)
return createNode(key);
if (key < root->data)
root->left = insert(root->left, key);
else if (key > root->data)
root->right = insert(root->right, key);
return root;
}
// Recursive Search
Node* search(Node* root, int key) {
if (root == NULL || root->data == key)
return root;
if (key < root->data)
return search(root->left, key);
else
return search(root->right, key);
}
}
}
// Recursive Delete
Node* deleteNode(Node* root, int key) {
if (root == NULL)
return root;
if (key < root->data)
root->left = deleteNode(root->left, key);
else if (key > root->data)
root->right = deleteNode(root->right, key);
else {
// Node with one or no child
if (root->left == NULL) {
Node* temp = root->right;
free(root);
return temp;
}
else if (root->right == NULL) {
Node* temp = root->left;
free(root);
return temp;
}
// Node with two children
Node* temp = findMin(root->right);
root->data = temp->data;
root->right = deleteNode(root->right, temp->data);
}
return root;
}
int main() {
Node* root = NULL;
int choice, key;
do {
printf("\n1. Insert\n2. Search\n3. Delete\n4. Inorder\n5. Preorder\n6.
Postorder\n7. Exit\nEnter choice: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("Enter key to insert: ");
scanf("%d", &key);
root = insert(root, key);
break;
case 2:
printf("Enter key to search: ");
scanf("%d", &key);
if (search(root, key))
printf("Key found!\n");
else
printf("Key not found.\n");
break;
case 3:
printf("Enter key to delete: ");
scanf("%d", &key);
root = deleteNode(root, key);
break;
case 4:
printf("Inorder Traversal: ");
inorder(root);
break;
case 5:
printf("Preorder Traversal: ");
preorder(root);
break;
case 6:
printf("Postorder Traversal: ");
postorder(root);
break;
}
} while (choice != 7);
return 0;
}
Comparison
Recursive code is easier to understand and shorter.
Iterative is better in cases of deep trees to avoid stack overflow.
GRAPHS
1. Definition of Graph
A graph is a non-linear data structure consisting of a set of vertices (nodes) and a set of edges
(connections) that link pairs of vertices. It provides a powerful abstract model for relationships
between discrete entities.
V is a set of vertices or nodes. These are the fundamental entities or points within the graph.
E is a set of edges, where each edge is a pair of vertices (u, v). An edge signifies a
relationship or connection between vertex u and vertex v.
2. Basic Concepts
a) Vertex (Node)
b) Edge
c) Adjacency
Theoretical Explanation: Two vertices are considered adjacent if they are directly
connected by an edge. If an edge (u,v) exists in the graph, then u is adjacent to v, and if the
graph is undirected, v is also adjacent to u. Adjacency is a core concept for navigating and
exploring graphs.
d) Degree
e) Path
f) Cycle
Theoretical Explanation: A cycle is a special type of path that starts and ends at the same
vertex, forming a closed loop. Crucially, a cycle must not repeat any intermediate vertices
or edges.
Example: A to B to C to A forms a cycle.
g) Connected Graph
Theoretical Explanation: A graph is deemed connected if there exists at least one path
between every possible pair of distinct vertices in the graph. In essence, it means you can
reach any vertex from any other vertex by traversing edges. If even one pair of vertices has
no path between them, the graph is disconnected.
3. Properties of Graphs
Property Description
**Order ($ V
**Size ($ E
A simple graph is a graph that contains no loops (an edge connecting a vertex to
Simple itself) and no multiple edges (more than one edge connecting the same pair of
Graph vertices). Most theoretical graph problems assume simple graphs unless otherwise
specified.
A multigraph is a graph that allows for multiple edges between the same pair of
Multigraph
vertices. For example, two cities might be connected by multiple roads.
An edge that connects a vertex to itself. This signifies a self-relationship or self-
Loop
transition for the entity represented by the vertex.
A weighted graph is a graph where each edge has an associated numeric value,
Weighted known as a "weight" or "cost." These weights can represent various metrics such
Graph as distance, time, cost, capacity, or strength of connection. For example, on a road
map, the weight could be the distance between two cities.
An unweighted graph is one where edges do not carry any specific numeric value
Unweighted or cost. In such graphs, the existence of an edge simply indicates a connection,
Graph and all connections are typically treated as having equal "cost" or significance
(often implicitly 1).
4. Types of Graphs
a) Undirected Graph
A ----- B
\ /
\ /
C
o Description: This diagram shows three vertices, A, B, and C. The lines between
them indicate undirected edges.
o Edges: (A-B), (A-C), (B-C). Since it's undirected, (A-B) is equivalent to (B-A).
o Real-world analogy: Friendships on Facebook are typically undirected: if Alice is
friends with Bob, Bob is automatically friends with Alice.
Theoretical Explanation: In a directed graph, each edge has a specific direction, indicated
by an arrow. An edge (u,v) means there is a connection from vertex u to vertex v, but not
necessarily vice-versa.
A ----> B
^ |
| v
C <---- D
o Description: This diagram shows four vertices, A, B, C, D, with arrows on the lines
indicating the direction of the connections.
o Edges: (A to B), (B to C), (D to C), (D to A).
o Real-world analogy: Following someone on Instagram is a directed relationship:
Alice can follow Bob without Bob necessarily following Alice back. Task
dependencies in project management are also directed (Task A must complete
before Task B starts).
c) Weighted Graph
A
/ \
2/ \3
/ \
B-------C
1
o Description: This diagram shows three vertices A, B, C, with numbers on the
edges. These numbers represent the 'weights' of the edges.
o Edges and Weights:
Edge (A-B) has a weight of 2.
Edge (A-C) has a weight of 3.
Edge (B-C) has a weight of 1.
o Real-world analogy: In a navigation system, roads (edges) have weights
representing their length in kilometers or the estimated travel time in minutes.
d) Unweighted Graph
1 --- 2
| |
4 --- 3
o Description: This is a simple square graph with four vertices (1, 2, 3, 4) and four
edges. There are no numbers on the edges.
o Edges: (1-2), (2-3), (3-4), (4-1).
o Real-world analogy: A simple network showing who is friends with whom, where
the 'strength' of friendship isn't being quantified.
e) Cyclic Graph
A ----- B
| |
| |
D ----- C
o Description: This graph has four vertices A, B, C, D, forming a square.
o Cycle: A to B to C to D to A is a cycle. Any starting point allows traversing back
to it without repeating edges.
o Real-world analogy: A road network where you can drive in a loop and return to
your starting point.
f) Acyclic Graph
g) Connected Graph
1 --- 2 --- 3
|
4
o Description: All four vertices (1, 2, 3, 4) are interconnected such that you can travel
from any vertex to any other vertex. For example, to go from 1 to 4, you can follow
1 to 2 to 4.
h) Disconnected Graph
A --- B X --- Y
| /
C -------- Z
o Description: This diagram shows two distinct groups of vertices. The group {A,
B, C} is connected, and the group {X, Y, Z} is connected, but there are no edges
connecting any vertex from {A, B, C} to any vertex from {X, Y, Z}.
o Disconnected: For example, there's no path from vertex A to vertex X.
A-----B
|\ /|
| \ / |
| X |
| / \ |
|/ \|
D-----C
o Description: This diagram shows a K_4 graph, meaning it has 4 vertices (A, B, C,
D). Every single vertex is connected directly to every other vertex.
o Edges: (A-B), (A-C), (A-D), (B-C), (B-D), (C-D). There are 4(4−1)/2=6 edges.
o Real-world analogy: A small meeting where everyone shakes hands with everyone
else.
Theoretical Explanation: This classification relates to the number of edges relative to the
maximum possible number of edges.
o Sparse Graph: A graph with relatively few edges. The number of edges (∣E∣) is
approximately proportional to the number of vertices (∣V∣), i.e., ∣E∣approx∣V∣.
o Dense Graph: A graph with many edges. The number of edges (∣E∣) is closer to
the maximum possible number of edges, which is approximately ∣V∣2 (for a
complete graph).
Visual Example (Conceptual):
o Sparse: Imagine a few scattered dots with only a handful of lines connecting them.
o Dense: Imagine many dots, and almost every dot has a line connecting it to every
other dot, making the diagram look very "full."
Implication: The density of a graph often influences the choice of the most efficient
storage structure and algorithms.
k) Bipartite Graph
Theoretical Explanation: A bipartite graph is one whose vertices can be divided into two
disjoint and independent sets, say U and W, such that every edge connects a vertex in U to
one in W. Crucially, there are no edges within U or within W.
Visual Diagram Example:
l) Tree
Theoretical Explanation: A tree is a special type of undirected graph that is both acyclic
(contains no cycles) and connected. For a graph with n vertices to be a tree, it must also
have exactly n−1 edges. Trees are fundamental hierarchical data structures.
A
/ \
B C
/ \
D E
o Description: This is a classic tree structure. It's connected (you can reach any node
from any other) and has no cycles. It has 5 vertices and 4 edges (5-1).
Recommendation Systems
Vertices: Represent artificial "neurons" or nodes in the network. These neurons process
information.
Edges: Represent the connections or "synapses" between neurons.
Weighted Graph: These are highly sophisticated weighted directed graphs.
o Weights: Each edge has a numerical weight that determines the strength and
influence of the connection. These weights are adjusted during the learning process
to optimize the network's output.
o Direction: Information flows in a specific direction (e.g., from input layer to hidden
layers to output layer).
Applications: Image recognition, natural language processing, predictive modeling,
machine translation.
Biological Networks
1. Adjacency Matrix
A --- B
\ /
\ /
C
o Explanation:
Matrix[0][1] = 1 because there's an edge from A to B.
Matrix[1][0] = 1 because it's undirected, so there's also
an edge from B
to A. This results in a symmetric matrix (the matrix is identical to its
transpose).
Diagonal elements (Matrix[i][i]) are typically 0 for simple graphs,
indicating no loops (no edge from a vertex to itself).
Structure for Directed Graph (Digraph) Example: Suppose we have a directed graph with
edges: A to B, B to C.
A ----> B ----> C
o Vertices: A, B, C. Indices: A=0, B=1, C=2.
Adjacency Matrix Representation:
o Explanation:
Matrix[0][1] = 1 because there's a directed edge from A to B.
Matrix[1][0] = 0 because there is NO directed edge from B to A.
This matrix is generally not symmetric.
A
/ \
2/ \3
/ \
B-------C
1
o Explanation:
Instead of 1, the actual weight is stored. Matrix[0][1] = 2 means the edge
(A-B) has a weight of 2.
For non-existent edges, 0 is used here, but often infty or a very large number
is used in pathfinding algorithms to signify an unreachable path.
Example 2 :
Feature Benefit
Checking if an edge exists between two specific vertices (i,j) takes O(1) constant
Fast Edge
time. You simply access Matrix[i][j]. This is extremely efficient for queries
Lookup
about direct connections.
Simple It's a straightforward 2D array, making it very easy to understand, implement,
Structure and debug. Array indexing is intuitive.
Feature Benefit
When a graph has many edges (dense), an adjacency matrix is efficient because
Best for Dense
most cells will contain a '1' or a weight. The space cost of O(V 2) is justified as a
Graphs
significant portion of the matrix is utilized.
Certain graph algorithms (e.g., detecting cycles using matrix multiplication,
Matrix
finding transitive closure) can be efficiently implemented using standard matrix
Operations
operations, leveraging linear algebra concepts.
Limitation Description
It always requires O(V2) space, regardless of the number of edges. For sparse
High Space graphs (where EllV2), this leads to a lot of wasted memory, as most of the matrix
Usage cells will be 0 (or false). For example, a graph with 1000 vertices needs
10002=1,000,000 cells, even if it has very few edges.
To find all neighbors of a specific vertex u, you need to iterate through its entire
Slow to
row (or column) in the matrix. This takes O(V) time, even if the vertex has only
Iterate
one neighbor. For example, to find all friends of Alice, you'd scan her entire row
Neighbors
in the matrix.
Typically, adjacency matrices are implemented as static arrays, meaning their size
(number of vertices) is fixed at compile time or upon initial allocation. Adding or
Fixed Size
removing vertices dynamically can be computationally expensive as it often
requires resizing and copying the entire matrix.
2. Adjacency List
Definition: An adjacency list represents a graph as an array (or list) of linked lists (or
dynamic arrays/vectors). Each index in the main array corresponds to a vertex, and the
linked list at that index contains all the vertices directly adjacent to it.
o For each vertex u in V, an adjacency list Adj[u] stores a list of all vertices v such
that there is an edge (u,v) in the graph.
Structure for Unweighted Undirected Graph Example:
Feature Benefit
This is its primary advantage. It only stores existing edges. The total
Efficient for Sparse space required is O(V+E) (V for the array of lists, and E for all the
Graphs elements in the lists). For sparse graphs (EllV2), this is significantly more
memory-efficient than an adjacency matrix.
To find all neighbors of a vertex u, you just iterate through its
Faster Neighbor corresponding linked list. This takes O(textdegreeofu) time, which is
Iteration much faster than O(V) for a sparse graph with a vertex of low degree.
This is crucial for traversal algorithms like BFS and DFS.
Avoids the wasted space inherent in adjacency matrices for sparse
Memory Efficient graphs, leading to better cache performance and overall memory
utilization.
It's easier to add or remove vertices and edges dynamically, as linked lists
Dynamic Size or dynamic arrays can easily grow or shrink without requiring full
reallocations of a large matrix.
Adding an edge is O(1) (just append to the list). Removing an edge is
Good for Edge
O(textdegreeofvertex) to find and remove the specific item from the list,
Addition/Removal
which is efficient for sparse graphs.
Disadvantages of Adjacency List
Limitation Description
Checking if an edge exists between two arbitrary vertices (u,v) requires
traversing the adjacency list of u to see if v is present. In the worst case,
Slower Edge Lookup
this could take O(textdegreeofu) time. For an undirected graph, it's
O(textmin(degreeofu,textdegreeofv)).
Some algorithms are naturally expressed and efficiently implemented
Harder to Implement
using matrix operations (e.g., matrix multiplication for path counting).
for Matrix-Friendly
Converting an adjacency list to a matrix, or adapting such algorithms,
Algorithms
can be more complex or less efficient.
Slightly More Managing linked lists or dynamic arrays (vectors) can be slightly more
Complex complex than simple 2D array indexing, especially in languages without
Implementation built-in dynamic list structures.
3. Comparison Table
The optimal choice between an Adjacency Matrix and an Adjacency List largely depends on the
specific characteristics of the graph and the most common operations to be performed on it.
Let's explore these core operations theoretically, detailing how they are performed and illustrating
them with real-world examples from a hypothetical social network called "FriendConnect".
Inserting a vertex means introducing a new independent entity into the graph.
Conceptually, you're adding a new point or "dot" to your diagram. Initially, this new vertex
is isolated; it has no connections (edges) to any existing vertices. Its degree (number of
connections) is zero.
o In an Adjacency List: You would typically find an available slot (an unused index
in an array, or simply append a new entry if using dynamic lists/maps) and create
an empty list for this new vertex, signifying it has no neighbors.
o In an Adjacency Matrix: You would add a new row and a new column, and all
entries in this new row/column would be initialized to 0 (or false), indicating no
connections.
Time Complexity (General):
o Adjacency List: O(1) (assuming an available ID is quickly found or new space is
simply appended).
o Adjacency Matrix: O(V2) if you need to resize the matrix (copy all existing data),
or O(1) if you have pre-allocated space.
Real-time Example (FriendConnect):
When Diana is inserted as a vertex, she becomes part of the network, but she doesn't have
any friends yet. She exists as an isolated profile, waiting to connect with others. Her
FriendConnect profile is created, but her "friends list" is empty.
2. Delete Vertex (Remove Node)
Theoretical Explanation:
Deleting a vertex v involves removing the entity v from the graph entirely. This also
necessitates the removal of all edges that were connected to v. Any connections that v had
with other vertices are severed.
o In an Adjacency List: You would mark the vertex as "inactive" or "deleted." Then,
you must traverse the adjacency lists of all other remaining vertices to find and
remove any references (edges) to the deleted vertex v. You also clear the adjacency
list of v itself.
o In an Adjacency Matrix: You would mark the corresponding row and column for
vertex v as invalid or clear all 1s (or weights) from them, effectively eliminating its
connections and presence.
Time Complexity (General):
o Adjacency List: O(V+E) in the worst case, as you might need to iterate through all
vertices and their lists to remove incoming edges.
o Adjacency Matrix: O(V) (clearing a row and column).
Real-time Example (FriendConnect):
When Bob's vertex is deleted, his profile is removed. Crucially, any friendships he had
(e.g., with Alice and Charlie) are also dissolved. Alice and Charlie's friend lists would
automatically update to remove Bob, as their connection to him no longer exists.
Adding an edge means establishing a connection between two existing vertices, say u and
v. This operation is only possible if both u and v already exist in the graph. The nature of
the graph (directed or undirected, weighted or unweighted) influences how the edge is
added.
Let's say "Alice" and "Charlie" are both on FriendConnect but aren't friends yet.
When Alice sends a friend request to Charlie, and Charlie accepts, an edge is added
between their vertices. Now, Alice appears on Charlie's friend list, and Charlie appears on
Alice's friend list, establishing a mutual, undirected connection.
Deleting an edge means removing an existing connection between two vertices u and v.
This operation assumes the edge (u, v) already exists.
o Undirected Edge: You would remove v from u's adjacency list and u from v's
adjacency list. In an adjacency matrix, you would set matrix[u][v] = 0 and
matrix[v][u] = 0.
o Directed Edge: For an edge (u, v), you would remove v from u's adjacency list. In
an adjacency matrix, you would set matrix[u][v] = 0.
Time Complexity (General):
o Adjacency List: O(degree) for the vertices involved (to traverse the list and find the
edge to remove).
o Adjacency Matrix: O(1).
Real-time Example (FriendConnect):
Imagine "Alice" and "Charlie" are friends, but they have a disagreement and decide to
unfriend each other.
When Alice unfriends Charlie, the edge between their vertices is deleted. They no longer
appear on each other's friend lists, and their direct connection in the network is severed.
Finding a vertex means determining if a particular entity (identified by its name or ID)
exists within the graph. This is a basic query operation.
o In an Adjacency List (with array indexing): If vertices are identified by integer IDs,
you can simply check if the ID is within the valid range and if the corresponding
slot in your vertex array (or is_active flag) is marked as active.
o In an Adjacency List (with hash map): If vertices have arbitrary names (like
strings), you would typically use a hash map to map the name to an internal integer
ID, and then perform the check as above.
o In an Adjacency Matrix: Similar to the adjacency list with array indexing, you'd
check the validity of the row/column index corresponding to the vertex.
Time Complexity (General):
o Adjacency List (with direct ID mapping): O(1).
o Adjacency List (with hash map): Average O(1), worst case O(V) (due to hash
collisions).
o Adjacency Matrix: O(1).
Real-time Example (FriendConnect):
A user wants to send a message to "Eve". Before sending, the system needs to verify if
"Eve" is actually a registered user (an active vertex) on FriendConnect.
The "Find Vertex" operation checks the database or internal representation of the user base.
If "Eve"'s profile exists and is active, the operation returns true, allowing the message to
be sent. Otherwise, it indicates that Eve is not found.
These fundamental operations enable the dynamic manipulation of graph structures, making
graphs incredibly versatile for modeling and solving complex problems across various domains.
Theoretical Explanation: BFS explores a graph level by level. It starts at a given source
vertex, visits all its immediate neighbors, then visits all the neighbors of those neighbors,
and so on. It uses a queue data structure to keep track of the vertices to visit.
1. Start with a source vertex s. Mark s as visited and enqueue it.
2. While the queue is not empty:
Dequeue a vertex u.
Visit u (e.g., print its name).
For each unvisited neighbor v of u:
Mark v as visited and enqueue v.
Time Complexity: O(V + E), where V is the number of vertices and E is the number of
edges. This is because each vertex and each edge is visited at most once.
Real-time Example (FriendConnect): Imagine "Alice" wants to find all her "friends of
friends" (people two steps away) on FriendConnect to expand her network. BFS is perfect
for this.
1. Start: Alice (source vertex).
2. Level 1: BFS would first find all of Alice's direct friends (e.g., Bob, Carol).
3. Level 2: Then, it would find all the friends of Bob (excluding Alice, as she's already
visited) and all the friends of Carol (excluding Alice). These are Alice's "friends of
friends."
4. Level 3 and beyond: If Alice wanted to find friends three or more steps away, BFS
would continue to explore outwards, layer by layer, until all reachable friends were
discovered.
This is similar to how social networks suggest connections or display different degrees of
separation between users.
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 100
// BFS function
void bfs(int graph[MAX_VERTICES][MAX_VERTICES], int numVertices, int
startVertex) {
struct Queue* q = createQueue();
int visited[MAX_VERTICES];
visited[startVertex] = 1;
enqueue(q, startVertex);
while (!isEmpty(q)) {
int currentVertex = dequeue(q);
printf("%d ", currentVertex);
int main() {
int numVertices;
printf("Enter the number of vertices: ");
scanf("%d", &numVertices);
// Adjacency Matrix
int graph[MAX_VERTICES][MAX_VERTICES];
int startVertex;
printf("Enter the starting vertex for BFS (0 to %d): ", numVertices
- 1);
scanf("%d", &startVertex);
return 0;
}
Theoretical Explanation: DFS explores a graph by going as deep as possible along each
branch before backtracking. It uses a stack data structure (implicitly, via recursion, or
explicitly) to keep track of the vertices to visit.
1. Start with a source vertex s. Mark s as visited.
2. Visit s (e.g., print its name).
3. For each unvisited neighbor v of s:
Recursively call DFS on v.
Time Complexity: O(V + E), where V is the number of vertices and E is the number of
edges. Similar to BFS, each vertex and each edge is visited at most once.
Real-time Example (FriendConnect): Imagine "Alice" wants to find a specific friend,
"David," but she suspects David is connected to her through a long chain of mutual friends,
and she wants to trace a single path to him. DFS is suitable for this "pathfinding" or
"reachability" scenario.
1. Start: Alice.
2. Explore a Path: DFS would go from Alice to Bob, then from Bob to Frank, then
from Frank to Gina, and so on, following one continuous path as deep as possible.
3. Backtrack: If it reaches a dead end (a user with no unvisited friends) or a path that
doesn't lead to David, it "backtracks" to the last user with unvisited friends and
explores another path.
4. Find David: It continues this process until David is found or all reachable users
have been explored. This is similar to exploring a specific branch of a family tree
to find a distant relative.
C Program (using Adjacency Matrix):
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 100
int main() {
int numVertices;
printf("Enter the number of vertices: ");
scanf("%d", &numVertices);
// Adjacency Matrix
int graph[MAX_VERTICES][MAX_VERTICES];
int startVertex;
printf("Enter the starting vertex for DFS (0 to %d): ", numVertices
- 1);
scanf("%d", &startVertex);
return 0;
}