'Lecture 1

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 21

`Lecture 1

In this playlist tutorial we will learn data structures in a very concise , effective , and problem driven
approach. By problem driven approach I mean to say that we will first undertand concepts , then
implement those concepts in programming languages and will solve plenty of questions on those
concepts to cement them .

Data Structures: Data structures refer to the way data is organized, stored, and managed in a
computer's memory. They provide a systematic way to represent and manipulate data, enabling
efficient operations like insertion, deletion, searching, and sorting. Common data structures include
arrays, linked lists, stacks, queues, trees, graphs, and hash tables, each with its own strengths and
weaknesses. Choosing the right data structure is crucial for optimizing the performance and efficiency
of programs.

Algorithms: Algorithms are step-by-step procedures or sets of rules designed to solve specific
problems or perform specific tasks. They are the building blocks of software applications and define
the logic and instructions for solving computational problems. Algorithms take input, perform a series
of operations or computations, and produce an output.

Why to learn Data Structures and Algorithms?

Tumhe product based company me placement jo chahiye( jokes)/

By combining the appropriate data structures with efficient algorithms, developers can design
programs that are optimised for speed, memory usage, and scalability.

Understanding data structures and algorithms is crucial for programmers, as it helps in designing
efficient solutions to problems, optimising code performance, and analysing and comparing the
efficiency of different algorithms. Mastery of data structures and algorithms empowers developers to
write better, faster, and more scalable software applications.

We can get more clarity of it by discussing a question.

Let's say we have to search for a number between 1 to 7 . then it can be done as -

Program 1: Linear Search

#include <stdio.h>

int linearSearch(int arr[], int size, int target) {

for (int i = 0; i < size; i++) {

if (arr[i] == target) {

return i;

}
return -1;

int main() {

int arr[] = {1, 2, 3, 4, 5, 6, 7};

int size = sizeof(arr) / sizeof(arr[0]);

int target = 7;

int index = linearSearch(arr, size, target);

if (index != -1) {

printf("Element %d found at index %d\n", target, index);

} else {

printf("Element %d not found\n", target);

return 0;

We perform a linear search to find a target element in an array. The algorithm iterates through each
element of the array until it finds a match or reaches the end. The data structure used here is a simple
array.

The time to perform sorting will depend on the length of array which is to be sorted

Program 2: Binary Search

#include <stdio.h>

int binarySearch(int arr[], int left, int right, int target) {

while (left <= right) {

int mid = left + (right - left) / 2;

if (arr[mid] == target) {

return mid;

if (arr[mid] < target) {


left = mid + 1;

} else {

right = mid - 1;

return -1;

int main() {

int arr[] = {1, 2, 3, 4, 5, 6, 7};

int size = sizeof(arr) / sizeof(arr[0]);

int target = 7;

int index = binarySearch(arr, 0, size - 1, target);

if (index != -1) {

printf("Element %d found at index %d\n", target, index);

} else {

printf("Element %d not found\n", target);

return 0;

This program demonstrates binary search, a more efficient search algorithm. It assumes the array is
sorted. The algorithm repeatedly divides the search space in half, eliminating the half that cannot
contain the target element. By this way it will need fewer operations to search for the number.

Memory layout -

Stack: The stack is a region of memory that is organised in a Last-In-First-Out (LIFO) manner. It is
typically used for storing local variables, function call information, and other data related to the
execution of a program. The stack is automatically managed by the compiler or runtime environment,
which handles the allocation and deallocation of memory. Each time a function is called, a new stack
frame is created, and when the function completes, its stack frame is removed. The stack is relatively
fast to access and has a fixed size determined during compilation or runtime.

Heap: The heap is another region of memory used for dynamic memory allocation. It is a pool of
memory that is generally larger than the stack and is more flexible in terms of memory management.
The heap is manually managed by the programmer, who explicitly requests memory from the
operating system and is responsible for deallocating it when it is no longer needed. Data stored in the
heap can persist beyond the scope of a single function, as long as it is properly managed. The heap
allows for dynamic memory allocation, meaning memory can be allocated and deallocated at runtime
as needed. However, this flexibility comes at the cost of potentially slower access times and the
possibility of memory leaks or fragmentation if not managed correctly.

Code/Data Segment:
● The code segment is where the compiled machine instructions of the program are stored. It is
typically read-only and contains the program's executable code.
● The data segment contains static and global variables that are initialized before the program
starts. It is divided into two sub-sections:
● Initialized data segment: It holds global and static variables with initial values.
● Uninitialized data segment (BSS): It holds global and static variables without initial
values. The memory for these variables is allocated, but they are initialized to 0 or
NULL.

Time Complexity and Space Complexity-

Time Complexity:

Time complexity is a measure of the amount of time an algorithm takes to run, based on the input
size. It quantifies the number of operations or steps required by an algorithm as a function of the input
size. Time complexity helps us analyze and compare the efficiency of different algorithms.

It describes the worst-case scenario of the algorithm's time requirements.

For example:

- O(1) represents constant time complexity, where the running time does not depend on the input size.
- O(n) represents linear time complexity, where the running time increases linearly with the input size.

- O(n^2) represents quadratic time complexity, where the running time increases quadratically with the
input size.

- O(log n) represents logarithmic time complexity, where the running time grows logarithmically with
the input size.

Asymptotic notation, also known as Big O notation, is a mathematical notation used to describe the
behaviour of functions or algorithms as the input size approaches infinity. It provides an upper bound
estimate of the growth rate of a function or the running time of an algorithm. Asymptotic notation is
commonly used in analysing time and space complexity.

There are several commonly used asymptotic notations:

​ Big O notation (O): It represents the upper bound or worst-case scenario of the algorithm's
time or space complexity. It describes the maximum growth rate of the function. Example:
O(n^2) represents a quadratic growth rate, where the function or algorithm grows at most
quadratically with the input size.

​ Omega notation (Ω): It represents the lower bound or best-case scenario of the algorithm's
time or space complexity. It describes the minimum growth rate of the function. Example: Ω(n)
represents a linear growth rate, where the function or algorithm grows at least linearly with the
input size.
​ Theta notation (Θ): It represents the tight bound or average-case scenario of the algorithm's
time or space complexity. It provides both the upper and lower bounds of the function.
Example: Θ(n) represents a linear growth rate, where the function or algorithm grows at the
same rate as the input size.

Let f(n)--> running time of an algorithm.

Then, Theta notation (Θ)- 0 ≤ c2 g(n) ≤ f(n) ≤ c1 g(n) ∀ n ≥ no.

Can you draw this diagram for Big O notation (O) and Omega notation (Ω) ?
Please keep in mind that it is always you to expect the time complexity in Theta notations as it
covers the best and worst case.

Factorial Time (O(n!)) > Exponential Time (O(2^n)) > Polynomial Time (O(n^k)) > Linearithmic Time

(O(n log n)) > Linear Time (O(n)) > Logarithmic Time (O(log n)) > Constant Time (O(1))

Best case, worst case, and average case are terms used to describe the runtime or performance of
an algorithm based on different scenarios or inputs. They provide insights into the extremes and
expected behavior of an algorithm.

1. Best Case:

The best case refers to the scenario in which an algorithm performs optimally or has the lowest
possible runtime. It represents the most favorable input or situation for the algorithm. The best case
scenario typically occurs when the input is structured in a way that allows the algorithm to minimize
the number of operations or iterations.

2. Worst Case:

The worst case refers to the scenario in which an algorithm performs the most poorly or has the
highest possible runtime. It represents the most unfavorable input or situation for the algorithm. The
worst case scenario typically occurs when the input is structured in a way that requires the algorithm
to perform the maximum number of operations or iterations.

3. Average Case:

The average case refers to the expected or typical runtime of an algorithm for a random or average
input. It takes into account the distribution of inputs and their likelihood of occurring. The average case
scenario considers the runtime over a range of possible inputs and calculates the average or
expected runtime.

The average case is often more difficult to analyze than the best and worst cases, as it requires
knowledge of the input distribution and probabilities. It involves considering all possible inputs and
their probabilities to calculate the expected runtime.

Understanding the best case, worst case, and average case scenarios helps in analyzing and
comparing algorithms. While worst case analysis provides an upper bound on the algorithm's
performance, best case analysis may provide insights into special input scenarios. Average case
analysis provides a more realistic expectation of the algorithm's runtime under typical conditions.

Let's consider an example of searching for a specific element in an array to illustrate the concepts of
best case, worst case, and average case scenarios.
Scenario:

Suppose we have an array of integers, and we want to find the index of a specific target element in
the array.

Algorithm:

We'll use a linear search algorithm to find the target element in the array. The linear search iterates
through each element of the array sequentially until a match is found.

Example Array: [5, 8, 2, 10, 3, 6]

Target Element: 10

1. Best Case Scenario:

- Best Case Input: The target element is the first element of the array (array[0] = 5).

- Algorithm Execution: In the first iteration, the algorithm matches the target element with the first
element of the array and finds a match.

- Result: The algorithm successfully finds the target element in the first step, resulting in the best
case scenario.

- Time Complexity: O(1) (constant time) as the algorithm terminates in a single step.

2. Worst Case Scenario:

- Worst Case Input: The target element is not present in the array.

- Algorithm Execution: In this scenario, the algorithm iterates through each element of the array
without finding a match until the end.

- Result: The algorithm exhaustively searches the entire array and doesn't find the target element,
resulting in the worst case scenario.

- Time Complexity: O(n) (linear time) as the algorithm needs to iterate through all n elements of the
array.
3. Average Case Scenario:

- Average Case Input: The target element is randomly located somewhere in the array.

- Algorithm Execution: In this scenario, the number of comparisons required to find the target
element will vary depending on its position in the array.

- Result: The average case scenario represents the expected performance of the algorithm,
assuming an equal probability distribution of target element positions.

- Time Complexity: O(n) (linear time) on average, as the algorithm performs an average of n/2
comparisons in the array.

By considering these different scenarios, we can analyze the best case, worst case, and average
case performance of the linear search algorithm. This example demonstrates how the performance of
an algorithm can vary based on the input characteristics and the different scenarios encountered.

Space Complexity:

Space complexity refers to the amount of memory or space required by an algorithm to solve a
problem, also based on the input size. It quantifies the additional memory needed as the problem size
increases.

It's important to note that time and space complexity analysis provide a high-level understanding of an
algorithm's efficiency and scalability. They help in comparing algorithms, making informed decisions
about algorithm selection, and predicting how algorithms will perform as the input size increases.
However, time and space complexity do not provide exact measurements of the running time or
memory usage of an algorithm. Actual performance may vary based on factors like hardware,
compiler optimizations, and specific implementation details.

1. Constant Time Complexity (O(1)):

#include <stdio.h>

void printFirstElement(int arr[], int size) {


if (size > 0) {
printf("First element: %d\n", arr[0]);
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
printFirstElement(arr, size);
return 0;
}

This program has constant time complexity because the function `printFirstElement()` always
accesses the first element of the array, regardless of the array's size.

2. Linear Time Complexity (O(n)):

#include <stdio.h>

void printArray(int arr[], int size) {


for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}

int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
printArray(arr, size);
return 0;
}

This program has linear time complexity because the function `printArray()` prints each element of the
array once, and the number of iterations is directly proportional to the array's size.

3. Quadratic Time Complexity (O(n^2)):


#include <stdio.h>

void printPairs(int arr[], int size) {


for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
printf("(%d, %d) ", arr[i], arr[j]);
}
printf("\n");
}
}

int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
printPairs(arr, size);
return 0;
}

This program has quadratic time complexity because the function `printPairs()` prints all possible pairs
of elements from the array, resulting in nested loops that iterate `n` times for each element.

4. Logarithmic Time Complexity (O(log n)):

#include <stdio.h>

int binarySearch(int arr[], int left, int right, int key) {


while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == key) {
return mid;
}
if (arr[mid] < key) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}

int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
int key = 3;
int index = binarySearch(arr, 0, size - 1, key);
printf("Index of %d: %d\n", key, index);
return 0;
}

This program demonstrates binary search, which has logarithmic time complexity. The
`binarySearch()` function repeatedly divides the search space in half until the key is found or the
search space is exhausted.

5. Factorial Time Complexity (O(n!)):

#include <stdio.h>

unsigned long long factorial(unsigned int n) {


if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}

int main() {
unsigned int n = 5;
unsigned long long result = factorial(n);
printf("Factorial of %d: %llu\n", n, result);
return 0;
}

This program calculates the factorial of a number using a recursive function. It has factorial time
complexity because the number of recursive calls grows rapidly with the input size.
Lecture 2
Abstract Data Types (ADTs)-

Abstract Data Types (ADTs) are like blueprints or templates for organizing and working with data in
computer programs. They define a set of values and the operations that can be performed on those
values. ADTs hide the internal details of how the data is stored and processed, focusing on what can
be done with the data rather than how it is done.

Think of an ADT as a conceptual idea rather than an actual implementation. It describes the
properties and behaviors of a data structure without specifying how it is built. This allows
programmers to design and work with data structures at a higher level of abstraction, separate from
the nitty-gritty implementation details.

Abstract Data Types (ADTs) and Data Types are related concepts, but they serve different purposes in
programming:

Data Types:

- Data types refer to the classification of data items or variables in a programming language. They
define what type of data can be stored in a variable and what operations can be performed on that
data.

- Common data types in programming languages include integers, floating-point numbers, characters,
strings, boolean values, arrays, structures, and more.

- Data types provide a way to categorize data and allocate memory for storing different kinds of
values. They help the compiler or interpreter understand how to handle the data and enforce rules on
the operations that can be performed on that data.

- Examples:

- `int` (integer) data type for whole numbers: `int age = 25;`

- `double` data type for floating-point numbers: `double pi = 3.14159;`

- `char` data type for characters: `char letter = 'A';`

Abstract Data Types (ADTs):

- Abstract Data Types are high-level data structures that define a set of operations or methods to
manipulate data. They hide the implementation details and focus on providing a clear interface for the
user to interact with the data.

- ADTs are "abstract" because they do not specify how the data is stored or the inner workings of the
operations; they only define what the operations do and how they can be used.
- ADTs provide an abstraction layer, allowing developers to work with data structures without worrying
about the low-level implementation details.

- Examples of Abstract Data Types include Stack, Queue, Linked List, Binary Search Tree, and Hash
Table.

- ADTs are essential for data encapsulation, data hiding, and creating reusable and modular code.

- Example - Arrays , Linked list, Stack etc.

Arrays-
Array is a commonly used Abstract Data Type (ADT) that represents a collection of elements of the
same type. It provides a way to store and access data in a contiguous block of memory, where each
element can be identified by its index.

The Array ADT defines the following key characteristics:

​ Size: An array has a fixed size, which is determined at the time of creation. This size specifies
the number of elements the array can hold.
​ Element Type: An array can store elements of a specific type, such as integers, characters, or
objects. All elements in the array must be of the same type.
​ Indexing: Elements in an array are accessed and identified using an index, which represents
the position of an element within the array. The index typically starts from 0 and increments
sequentially.
​ Random Access: Arrays support random access, which means you can directly access any
element in the array by using its index. This allows for efficient retrieval and modification of
elements.

The operations commonly associated with the Array ADT include:

​ Accessing an Element: Retrieve the value of an element at a specific index in the array.
​ Modifying an Element: Update the value of an element at a specific index in the array.
​ Inserting an Element: Add a new element to the array at a given position, shifting existing
elements as necessary.
​ Deleting an Element: Remove an element from the array at a specified index, adjusting the
remaining elements as needed.
​ Size Query: Determine the current size or length of the array, which represents the number of
elements it currently holds.
Arrays are widely used due to their simplicity and efficiency in accessing elements. They are
especially useful when the number of elements is known in advance and does not change frequently.
However, arrays have a fixed size, and resizing can be costly in terms of memory and time if done
frequently.

The implementation of the Array ADT can vary depending on the specific needs of the application and
the programming language being used. Two common techniques for implementing arrays are
contiguous memory allocation and dynamic memory allocation.

​ Contiguous Memory Allocation: In this approach, the array elements are stored in a
contiguous block of memory. Each element occupies a fixed amount of memory, and the
elements are arranged sequentially. This allows for efficient random access by directly
calculating the memory address of an element using its index. Languages like C and C++
typically use contiguous memory allocation for arrays. However, this approach has a limitation
in that the size of the array must be known in advance and cannot be easily changed once
allocated.
​ Dynamic Memory Allocation: Dynamic memory allocation allows arrays to be resized during
runtime. Languages like Java, C#, and Python provide built-in dynamic array structures, such
as ArrayList, List, or List in Python. These dynamic arrays automatically handle resizing and
memory management. Behind the scenes, they may use techniques like dynamic memory
allocation, where the array is initially allocated with a certain capacity, and when more
elements are added, the array is resized and the data is copied to a larger block of memory.

–Implementation of Array ADT in C –

#include <stdio.h>

#include <stdlib.h>

struct myArray {

int total_size; // Total size of the array

int used_size; // Number of elements currently used in the array

int *ptr; // Pointer to the array

};

// Function to create the array and initialize its properties


void createArray(struct myArray *a, int tSize, int uSize) {

a->total_size = tSize;

a->used_size = uSize;

a->ptr = (int *)malloc(tSize * sizeof(int));

// Function to display the elements of the array

void show(struct myArray *a) {

printf("Array elements: ");

for (int i = 0; i < a->used_size; i++) {

printf("%d ", (a->ptr)[i]);

printf("\n");

// Function to set values for the array elements

void setVal(struct myArray *a) {

for (int i = 0; i < a->used_size; i++) {

printf("Enter element %d: ", i);

scanf("%d", &(a->ptr)[i]);

// Function to insert an element at a specific index in the array

void insertElement(struct myArray *a, int index, int value) {

if (index >= 0 && index <= a->used_size && a->used_size <

a->total_size) {

for (int i = a->used_size; i > index; i--) {

(a->ptr)[i] = (a->ptr)[i - 1];

(a->ptr)[index] = value;

(a->used_size)++;

printf("Element inserted successfully.\n");


} else {

printf("Invalid index or array is full. Cannot insert element.\n");

// Function to delete an element at a specific index in the array

void deleteElement(struct myArray *a, int index) {

if (index >= 0 && index < a->used_size) {

for (int i = index; i < a->used_size - 1; i++) {

(a->ptr)[i] = (a->ptr)[i + 1];

(a->used_size)--;

printf("Element deleted successfully.\n");

} else {

printf("Invalid index. Cannot delete element.\n");

// Function to search for an element in the array

int searchElement(struct myArray *a, int value) {

for (int i = 0; i < a->used_size; i++) {

if ((a->ptr)[i] == value) {

return i; // Return the index if the element is found

return -1; // Return -1 if the element is not found

int main() {

struct myArray marks;

createArray(&marks, 10, 5);

setVal(&marks);
show(&marks);

insertElement(&marks, 2, 99);

show(&marks);

deleteElement(&marks, 4);

show(&marks);

int value = 30;

int index = searchElement(&marks, value);

if (index != -1) {

printf("Element %d found at index %d.\n", value, index);

} else {

printf("Element %d not found.\n", value);

free(marks.ptr);

return 0;

Check the practice questions done in video- Arrays.zip


Lecture 3

Linked List-

A linked list is a data structure that consists of a sequence of elements, called nodes, where each
node contains two parts: the data and a reference (or pointer) to the next node in the sequence. The
last node in the list typically points to null, indicating the end of the list.

Linked lists are used in computer programming and data structures because they offer several
advantages over arrays in certain scenarios:

​ Dynamic Size: Unlike arrays, linked lists can dynamically grow and shrink during program
execution, as memory for each new node can be allocated separately. This makes them
suitable for situations where the size of the data is not known beforehand or may change over
time.
​ Efficient Insertions and Deletions: Inserting or deleting elements in a linked list is generally
more efficient than in an array, especially in large lists. In a linked list, you can simply adjust
the pointers to add or remove a node, while in an array, you may need to shift elements to
make space for a new element or close a gap after removing an element.
​ Memory Utilization: Linked lists use memory more efficiently than arrays, particularly in
situations where memory allocation is costly. In an array, you need contiguous memory blocks
to store elements, while linked lists use individual memory blocks (nodes) that can be
scattered across the memory.
​ No Pre-allocation Needed: In arrays, you often need to pre-allocate a fixed amount of
memory to hold the elements, which can lead to either wasted space or the need to resize the
array (which can be expensive). Linked lists do not have this limitation and can dynamically
allocate memory for each new element.

However, linked lists also have some drawbacks compared to arrays:

​ Random Access: Accessing an element in a linked list is slower than in an array because
you have to traverse the list from the beginning to find the desired node. In contrast, arrays
allow direct access using an index, which is much faster.
​ Memory Overhead: Linked lists require extra memory for storing the pointers/references to
the next nodes. This overhead can be significant compared to the actual data being stored in
the list.

**The linked list we discussed above is also referred to as a Singly linked List-

Doubly Linked list -


A doubly linked list is a type of linked list in which each node contains two pointers: one that points to
the next node in the list, and another that points to the previous node in the list. This allows for
bidirectional traversal of the list.

Circular Linked list -

A circular linked list is a variation of a linked list in which the last node points back to the first node,
creating a circular structure. This means that there is no "end" node in a circular linked list, as the last
node points to the first node, forming a loop.

Implementation code and problem discussed - LinkedList.zip

Lecture 4
The Stack Abstract Data Type (ADT) is a fundamental data structure that follows the Last-In-First-Out
(LIFO) principle. It behaves like a collection of elements, where new elements are added to the top,
and elements are removed from the top as well. Imagine a stack of plates where you can only add or
remove plates from the top of the stack.

A Stack ADT typically provides the following essential operations:

1. Push: Adds an element to the top of the stack.

2. Pop: Removes and returns the top element from the stack.

3. Peek (or Top): Returns the top element without removing it.

4. isEmpty: Checks if the stack is empty.

5. Size: Returns the number of elements in the stack.

6. Clear: Removes all elements from the stack, making it empty.

Stacks can be implemented using various data structures, with the most common options being:

1. Array-based implementation: The stack is implemented using a fixed-size or dynamic array,


where push and pop operations are performed by manipulating the array's indices.

2. Linked list-based implementation: In this approach, each element in the stack is represented as
a node in a linked list. The top of the stack is the first node, and push and pop operations involve
adding/removing nodes at the beginning of the linked list.

Stacks are widely used in computer science and programming for various applications, including:
1. Expression evaluation: Stacks are used to evaluate expressions, particularly in converting infix
expressions to postfix expressions for easier evaluation.

2. Function call stack: During the execution of a program, function calls and their local variables are
managed using a stack-like structure called the call stack.

3. Undo/Redo functionality: Stacks can be used to implement undo and redo functionality in
applications that require a history of actions.

4. Backtracking algorithms: Many backtracking algorithms use stacks to keep track of the current state
and explore different paths.

The simplicity and efficiency of the stack data structure make it a valuable tool for solving a wide
range of problems in computer science and programming.

You might also like