W3 - Shorts 📚🔍- Learn to use Algorithms in C (Linear and Binary Search; Selection, Bubble and Merge Sort; Recursion)
Read more here
CS50 IDE: https://cs50.dev/ (Online VSCode)
CS50 Library Documentation: https://manual.cs50.io/#cs50.h
#CS50x #C #Computer_Science #VisualStudioCode #Algorithms
Table of Contents:
A) Linear Search Algorithm 🎦
Video: https://youtu.be/TwsgCHYmbbA
A.1) What is Linear Search?
Linear search is an algorithm used to find an element in an array.
- Algorithm Definition: A step-by-step set of instructions to complete a task.
- How it Works:
- Iterate through the array from left to right.
- At each step, check if the current element matches the target.
- Stop if:
- The element is found.
- The entire array is traversed without finding the element.
A.2) Pseudocode for Linear Search
- Start at the first element in the array.
- While not at the end of the array:
a. If the current element equals the target, STOP (success).
b. Otherwise, move to the next element. - If the end of the array is reached without finding the target, STOP (failure).
A.3) Example Walkthrough
Case 1: Element is in the Array
Array:
Target Value:
- Step 1: Compare the first element,
, with . , so move to the next element.
- Step 2: Compare the second element,
, with . , so move to the next element.
- Step 3: Compare the third element,
, with . , so move to the next element.
- Step 4: Compare the fourth element,
, with . , so STOP. The target is found.
Case 2: Element is NOT in the Array
Array:
Target Value:
- Step 1: Compare each element in turn with
: , , , , .
- Step 2: Reach the end of the array without finding
. - Conclusion: The target is not in the array. Linear search has completed.
A.4) Best and Worst Case Scenarios
-
Best Case:
- The target element is the first element.
- Steps Required:
. - Complexity:
(constant time).
-
Worst Case:
- The target element is the last element, or it is not in the array.
- Steps Required: Look through all
elements. - Complexity:
(linear time).
A.4.1) Complexity Analysis
- Best Case:
- The target is found in the first position.
- Worst Case:
- The target is the last element or not in the array.
- Average Case:
- On average, half the array will be traversed before finding the target.
A.5) Key Points: Linear Search
- Advantages: Simple to implement, no need for sorting the array.
- Disadvantages: Inefficient for large arrays due to its linear complexity.
- Use Case: Suitable for small arrays or unsorted data where simplicity is more important than performance.
B) Binary Search Algorithm 🎦
Video: https://youtu.be/T98PIp4omUA
B.1) What is Binary Search?
Binary search is an efficient algorithm to find an element in a sorted array.
-
Key Idea: Divide and Conquer
- Reduce the search area by half at each step.
- This makes it significantly faster than linear search.
-
Condition: The array must be sorted before applying binary search. If not, the algorithm cannot guarantee correct results.
B.2) Pseudocode for Binary Search
- Define the start and end indices of the array:
- Initially,
start = 0
andend = n - 1
.
- Initially,
- Repeat the following steps until the subarray has size 0:
- Calculate the midpoint:
- Compare the midpoint value with the target:
- If
target == array[mid]
, STOP. The target is found. - If
target < array[mid]
, search the left subarray: - If
target > array[mid]
, search the right subarray:
- If
- Calculate the midpoint:
- If the start index becomes greater than the end index, STOP. The target is not in the array.
B.3) Example Walkthrough
Case 1: Target Found in Array
Array:
Target Value:
-
Step 1:
- Start:
, End: . - Midpoint: $$ \text{mid} = \lfloor \frac{0 + 6}{2} \rfloor = 3 $$
- Value at
array[3]
is. - Since
, update:
- Start:
-
Step 2:
- Start:
, End: . - Midpoint: $$ \text{mid} = \lfloor \frac{4 + 6}{2} \rfloor = 5 $$
- Value at
array[5]
is. - Since
, update:
- Start:
-
Step 3:
- Start:
, End: . - Midpoint: $$ \text{mid} = \lfloor \frac{4 + 4}{2} \rfloor = 4 $$
- Value at
array[4]
is. - Target found! STOP.
- Start:
Case 2: Target Not Found in Array
Array:
Target Value:
-
Step 1:
- Start:
, End: . - Midpoint: $$ \text{mid} = \lfloor \frac{0 + 6}{2} \rfloor = 3 $$
- Value at
array[3]
is. - Since
, update:
- Start:
-
Step 2:
- Start:
, End: . - Midpoint: $$ \text{mid} = \lfloor \frac{4 + 6}{2} \rfloor = 5 $$
- Value at
array[5]
is. - Since
, update:
- Start:
-
Step 3:
- Start:
, End: . - Midpoint: $$ \text{mid} = \lfloor \frac{6 + 6}{2} \rfloor = 6 $$
- Value at
array[6]
is. - Since
, update:
- Start:
-
Step 4:
- Start:
, End: . - Start index is now greater than the end index, so STOP.
- Target is not in the array.
- Start:
B.4) Best and Worst Case Scenarios
-
Best Case:
- The target is found at the first midpoint calculation.
- Steps Required:
. - Complexity:
.
-
Worst Case:
- The array is repeatedly halved until only one element remains.
- Steps Required:
- Complexity:
.
-
Average Case:
- On average, the array is halved multiple times to locate the target.
- Complexity:
.
B.5) Key Points: Binary Search
- Advantages:
- Much faster than linear search for large arrays.
- Reduces the search space by half at each step.
- Disadvantages:
- Requires the array to be sorted beforehand.
- Sorting the array adds preprocessing cost.
- Use Case: Suitable for searching large datasets that are already sorted or where sorting is acceptable as a preprocessing step.
______________________________________________________________
C) Selection Sort Algorithm🎦
Video: https://youtu.be/3hH8kTHFw2A
C.1) What is Selection Sort?
Selection Sort is a simple algorithm used to sort an array by repeatedly selecting the smallest unsorted element and placing it at the beginning of the array.
- Key Idea:
- Divide the array into a sorted part and an unsorted part.
- Iteratively move the smallest element from the unsorted part to the sorted part.
C.2) Pseudocode for Selection Sort
- Repeat until no unsorted elements remain:
- Search through the unsorted part of the array to find the smallest value.
- Swap the smallest value with the first element of the unsorted part.
C.3) Step-by-Step Example
Unsorted Array:
Steps of Selection Sort:
-
Iteration 1:
- Find the smallest element in the array: ( 1 ).
- Swap ( 1 ) with the first element (( 5 )):
- Now, ( 1 ) is sorted.
-
Iteration 2:
- Find the smallest element in the unsorted part: ( 2 ).
- Swap ( 2 ) with the first element of the unsorted part (( 5 )):
- Now, ( 2 ) is sorted.
-
Iteration 3:
- Find the smallest element in the unsorted part: ( 3 ).
- Swap ( 3 ) with the first element of the unsorted part (( 4 )):
- Now, ( 3 ) is sorted.
-
Iteration 4:
- Find the smallest element in the unsorted part: ( 4 ).
- Swap ( 4 ) with the first element of the unsorted part (( 5 )):
- Now, ( 4 ) is sorted.
-
Iteration 5:
- Find the smallest element in the unsorted part: ( 5 ).
- Swap ( 5 ) with the first element of the unsorted part (( 6 )):
- Now, ( 5 ) is sorted.
-
Iteration 6:
- Only ( 6 ) remains. It is already sorted:
- Only ( 6 ) remains. It is already sorted:
C.4) Complexity Analysis
Worst-Case Scenario
- For every element, we need to search the entire unsorted part to find the smallest value.
- Number of comparisons:
- First pass:
- Second pass:
- Third pass:
- ...
- Total:
.
- First pass:
- Time Complexity:
.
Best-Case Scenario
- Even if the array is already sorted, the algorithm still performs the same number of comparisons.
- Time Complexity:
.
Key Insight:
- Selection Sort has the same complexity for both the best and worst cases.
C.4.1) Advantages and Disadvantages (Selection Sort)
Advantages:
- Simple to implement.
- Useful for small datasets.
Disadvantages:
- Inefficient for large datasets.
- Always
regardless of the input.
C.5) Key Points: Selection Sort
- Selection Sort builds the sorted array incrementally by moving the smallest element to the sorted portion.
- It has a quadratic time complexity (
) in both best and worst cases. - It's a good choice for small datasets but impractical for large ones.
D) Bubble Sort Algorithm 🎦
Video: https://youtu.be/RT-hUXUWQ2I
Overview: Bubble Sort
Bubble sort is a sorting algorithm that repeatedly steps through the array, compares adjacent elements, and swaps them if they are in the wrong order. The process repeats until no swaps are needed, indicating the array is sorted.
Pseudo-code: Algorithm Steps
-
Initialize:
- Set a
swap_counter
to a non-zero value (e.g.,-1
initially).
- Set a
-
Repeat Until:
- The
swap_counter
becomes0
(indicating no swaps are required).
- The
-
For Each Pass:
- Reset the
swap_counter
to0
. - Compare each pair of adjacent elements:
- If they are out of order: Swap them and increment the
swap_counter
by1
.
- If they are out of order: Swap them and increment the
- Reset the
-
Optimization:
- After each pass, the largest unsorted element is bubbled to its correct position at the end of the array, so the sorted portion grows from right to left.
Step-by-Step Example
Initial Array
Unsorted array: [5, 2, 1, 3, 6, 4]
-
First Pass:
- Compare
5
and2
, swap:[2, 5, 1, 3, 6, 4]
(swap_counter = 1
) - Compare
5
and1
, swap:[2, 1, 5, 3, 6, 4]
(swap_counter = 2
) - Compare
5
and3
, swap:[2, 1, 3, 5, 6, 4]
(swap_counter = 3
) - Compare
5
and6
, no swap:[2, 1, 3, 5, 6, 4]
- Compare
6
and4
, swap:[2, 1, 3, 5, 4, 6]
(swap_counter = 4
)
Sorted portion:
[6]
. - Compare
-
Subsequent Passes:
- The process repeats for the unsorted portion until no swaps are required, at which point the array is fully sorted.
Complexity Analysis
Worst-Case Scenario
- Scenario: The array is in reverse order (e.g.,
[6, 5, 4, 3, 2, 1]
). - Steps:
- For each of the
elements, it must be compared to and swapped with each of the other elements.
- For each of the
- Time Complexity:
.
Best-Case Scenario
- Scenario: The array is already sorted (e.g.,
[1, 2, 3, 4, 5, 6]
). - Steps:
- A single pass through the array confirms no swaps are needed, and the algorithm terminates.
- Time Complexity:
.
Key Points:
- Worst Case:
- Best Case:
- Bubble Sort is slightly better than Selection Sort in the best case, as it can terminate early if no swaps are needed.
- Bubble Sort is inefficient for large datasets but is easy to implement and understand.
Advantages and Disadvantages (Bubble Sort)
Advantages:
- Simple to implement.
- Can detect if the array is already sorted.
Disadvantages:
- Inefficient for large datasets.
- Always
in the average and worst cases.
Bubble Sort Summary
- Bubble sort is intuitive but impractical for large datasets due to its quadratic time complexity.
- It "bubbles" the largest unsorted elements to the end of the array in each pass, gradually building the sorted portion.
E) Understanding Recursion 🎦
Video: https://youtu.be/mz6tAJMVmfM
Recursion is a technique in programming where a function calls itself as part of its execution. It's particularly useful for solving problems that can be broken into smaller, similar subproblems.
Components of a Recursive Function
-
Base Case:
- A simple condition that stops the recursive process.
- Prevents infinite recursion by halting the function when a certain state is reached.
-
Recursive Case:
- The function calls itself with modified arguments to make progress toward the base case.
- Solves a smaller piece of the original problem.
Example: Factorial Function Using Recursion
The factorial of a positive integer
For example:
Recursive Definition:
Code Implementation:
def factorial(n):
if n == 1: # Base Case
return 1
return n * factorial(n - 1) # Recursive Case
Example: Fibonacci Sequence Using Recursion
The Fibonacci sequence is defined as:
For example:
Recursive Definition
Code Implementation
def fibonacci(n):
if n == 1: # Base Case
return 0
if n == 2: # Base Case
return 1
return fibonacci(n - 1) + fibonacci(n - 2) # Recursive Case
Example: Collatz Conjecture Using Recursion
The Collatz conjecture states that for any positive integer
- If
, stop. - If
is even, the next number is . - If
is odd, the next number is .
The conjecture speculates that this process will eventually reach
Recursive Definition:
The number of steps
Code Implementation
def collatz_steps(n):
if n == 1: # Base Case
return 0
if n % 2 == 0: # Recursive Case for even numbers
return 1 + collatz_steps(n // 2)
return 1 + collatz_steps(3 * n + 1) # Recursive Case for odd numbers
Comparison of Recursive vs Iterative Factorial
Recursive Factorial
def factorial_recursive(n):
if n == 1: # Base Case
return 1
return n * factorial_recursive(n - 1) # Recursive Case
Iterative Factorial
def factorial_iterative(n):
product = 1
for i in range(1, n + 1):
product *= i
return product
Both approaches produce the same result, but recursion often leads to more elegant, compact code.
Advantages and Disadvantages of Recursion
Advantages:
- Elegant and Simple Code: Reduces the need for loops or manual tracking.
- Natural for Tree/Divide-and-Conquer Problems: Useful for problems like Fibonacci, factorial, and tree traversals.
Disadvantages:
- Performance Overhead: Recursive calls require more memory due to the function call stack.
- Risk of Stack Overflow: Without a proper base case, recursion can result in infinite loops and crash programs.
Key Takeaways: Recursion
- Recursion is a powerful tool for solving problems that can be divided into smaller subproblems.
- Always define a base case to prevent infinite recursion.
- Recursive code is elegant but can be inefficient compared to iterative solutions for certain tasks.
F) Merge Sort Algorithm 🎦
Video: https://youtu.be/Ns7tGNbtvV4
Overview: Merge Sort Algorithm
Merge Sort is a divide-and-conquer algorithm that recursively splits an array into smaller subarrays, sorts them, and then merges them back together. It offers significant efficiency improvements over simpler algorithms like bubble sort and insertion sort.
Steps in Merge Sort
-
Split the Array
- Divide the array into two halves recursively until each subarray contains only one element. A single-element array is inherently sorted.
-
Sort the Subarrays
- Recursively sort the left and right halves.
-
Merge the Halves
- Compare elements from the two sorted subarrays and merge them into a single sorted array.
Pseudocode
function mergeSort(array):
if size of array <= 1:
return array # Base case: single-element arrays are sorted
mid = size of array / 2
left = mergeSort(array[0:mid]) # Sort the left half
right = mergeSort(array[mid:end]) # Sort the right half
return merge(left, right) # Merge the sorted halves
function merge(left, right):
result = []
while left and right:
if left[0] <= right[0]:
append left[0] to result and remove it from left
else:
append right[0] to result and remove it from right
append any remaining elements in left or right to result
return result
Example Walkthrough
Input Array:
-
Split the Array
Recursively divide the array:- Split 1:
and - Split 2 (left):
and - Split 3 (right):
and
- Split 1:
-
Sort Each Half
is sorted. - Sort
: split into and , merge into . - Merge
and into . is sorted. - Sort
: split into and , merge into . - Merge
and into .
-
Merge Final Halves
Mergeand : - Compare
and → Add . - Compare
and → Add . - Compare
and → Add . - Compare
and → Add . - Compare
and → Add . - Add remaining
.
- Compare
Result:
Complexity Analysis
Worst-Case Scenario
- Steps: Divide the array into subarrays (
steps) and merge them back ( comparisons per step). - Time Complexity:
.
Best-Case Scenario
- Even if the array is already sorted, Merge Sort doesn’t have a shortcut to detect it.
- Time Complexity:
.
Advantages and Disadvantages (Merge Sort)
Advantages
-
Efficiency:
- Faster than bubble sort, insertion sort, and selection sort for large datasets.
-
Stable Sort:
- Preserves the relative order of equal elements.
-
Predictable Time Complexity:
- Always
, regardless of input.
- Always
Disadvantages
-
Space Complexity:
- Requires extra space for merging subarrays (
).
- Requires extra space for merging subarrays (
-
Overhead for Small Arrays:
- For small datasets, simpler algorithms like insertion sort may outperform merge sort due to recursion overhead.
Key Points: Merge Sort
- Merge Sort is a powerful divide-and-conquer sorting algorithm.
- Best suited for large datasets where stability and predictable performance are required.
- It leverages recursion to break down a sorting problem into manageable subproblems.
Z) 🗃️ Glossary
File | Definition |
---|
Uncreated files | Origin Note |
---|