Data Structures And AlgorithmsUnit 618 min read
Searching & Sorting: Algorithms, Analysis, and Real-World Applications
Unit 6 of Data Structures And Algorithms covers fundamental searching (linear, binary) and sorting (insertion, selection, bubble, merge, quick, heap) algorithms, their time/space complexity, and practical implementations. Learn how to analyze, trace, and apply these algorithms to solve real-world problems efficiently.
Core Concepts: Searching Algorithms
1. Linear Search
Definition: A simple search algorithm that checks each element sequentially until the target is found or the list ends.
How it works:
- Start from the first element.
- Compare each element with the target.
- If found, return its position; else, continue until the end.
Visualization:
graph LR
A["Start"] --> B["Check arr[0] == target?"]
B -->|"Yes"| C["Return index 0"]
B -->|"No"| D["Check arr[1] == target?"]
D -->|"Yes"| E["Return index 1"]
D -->|"No"| F["Check arr[n-1] == target?"]
F -->|"Yes"| G["Return index n-1"]
F -->|"No"| H["Target not found"]Example:
Search for 12 in [11, 19, 5, 2, 7, 21, 8, 21, 12]:
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i
return -1
Trace:
| Step | Index (i) | arr[i] | Comparison (arr[i] == 12) | Action |
|---|---|---|---|---|
| 1 | 0 | 11 | No | Continue |
| 2 | 1 | 19 | No | Continue |
| 3 | 2 | 5 | No | Continue |
| 4 | 3 | 2 | No | Continue |
| 5 | 4 | 7 | No | Continue |
| 6 | 5 | 21 | No | Continue |
| 7 | 6 | 8 | No | Continue |
| 8 | 7 | 21 | No | Continue |
| 9 | 8 | 12 | Yes | Return 8 |
Time Complexity: (worst/average case). Space Complexity: (in-place).
2. Binary Search
Definition: A fast search algorithm for sorted arrays that repeatedly divides the search interval in half.
How it works:
- Compare the target with the middle element.
- If equal, return the index.
- If target < middle, search the left half; else, search the right half.
- Repeat until found or the interval is empty.
Visualization (Searching for 12 in [2, 5, 7, 8, 11, 12, 19, 21, 21]):
Example Code:
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
Trace for 12 in [2,5,7,8,11,12,19,21,21]:
| Step | low | high | mid | arr[mid] | Comparison (12 vs arr[mid]) | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 8 | 4 | 11 | 12 > 11 | low = 5 |
| 2 | 5 | 8 | 6 | 12 | 12 == 12 | Return 6 |
Time Complexity: (best/average/worst case). Space Complexity: (iterative).
## In the real world
- eSewa (Nepal): Uses binary search to quickly locate user transactions in its sorted database of millions of records, reducing search time from to .
- Khalti’s Payment Sorting: When processing bulk transactions, Khalti sorts payment amounts using merge sort (stable and efficient for large datasets) to group similar transactions for fraud detection.
- NTC’s Call Routing: The National Telecommunications Commission uses priority queues (a type of queue) to route emergency calls (highest priority) before regular calls, ensuring critical services are handled first.
Core Concepts: Sorting Algorithms
1. Insertion Sort
Definition: Builds the final sorted array one element at a time by inserting each new element into its correct position in the already-sorted part.
How it works:
- Start with the second element.
- Compare it with elements in the sorted subarray (left side).
- Shift elements to make space and insert the current element in its correct position.
Visualization (Sorting [90, 57, 80, 10, 22]):
graph TD
A["Initial: [90] [57] [80] [10] [22]"] --> B["Insert 57: [57, 90]"]
B --> C["Insert 80: [57, 80, 90]"]
C --> D["Insert 10: [10, 57, 80, 90]"]
D --> E["Insert 22: [10, 22, 57, 80, 90]"]Example Code:
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
Trace for [90, 57, 80, 10, 22]:
| Pass | i | key | j | arr[j] > key? | Shift? | Sorted Subarray |
|---|---|---|---|---|---|---|
| 1 | 1 | 57 | 0 | 90 > 57 | Yes | [57, 90] |
| 2 | 2 | 80 | 1 | 90 > 80 | Yes | [57, 80, 90] |
| 3 | 3 | 10 | 2 | 80 > 10 | Yes | [10, 57, 80, 90] |
| 4 | 4 | 22 | 3 | 90 > 22 | Yes | [10, 22, 57, 80, 90] |
Time Complexity:
- Best case: (already sorted).
- Average/Worst case: . Space Complexity: (in-place).
Advantages:
- Simple to implement.
- Efficient for small or nearly sorted datasets.
- Stable (preserves order of equal elements).
Disadvantages:
- Inefficient for large datasets ( time).
2. Selection Sort
Definition: Repeatedly selects the smallest (or largest) element from the unsorted part and swaps it with the first unsorted element.
How it works:
- Find the minimum element in the unsorted array.
- Swap it with the first unsorted element.
- Repeat for the remaining unsorted subarray.
Visualization (Sorting [64, 25, 12, 22, 11]):
graph TD
A["Initial: [64, 25, 12, 22, 11]"] --> B["Find min (11), swap with 64: [11, 25, 12, 22, 64]"]
B --> C["Find min (12), swap with 25: [11, 12, 25, 22, 64]"]
C --> D["Find min (22), swap with 25: [11, 12, 22, 25, 64]"]Example Code:
def selection_sort(arr):
for i in range(len(arr)):
min_idx = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
Trace for [64, 25, 12, 22, 11]:
| Pass | i | min_idx | Swap (arr[i] ↔ arr[min_idx]) | Array State |
|---|---|---|---|---|
| 1 | 0 | 4 | 64 ↔ 11 | [11, 25, 12, 22, 64] |
| 2 | 1 | 2 | 25 ↔ 12 | [11, 12, 25, 22, 64] |
| 3 | 2 | 3 | 25 ↔ 22 | [11, 12, 22, 25, 64] |
Time Complexity: (all cases). Space Complexity: (in-place).
Advantages:
- Simple to implement.
- Performs well for small datasets.
Disadvantages:
- Inefficient for large datasets.
- Unstable (does not preserve order of equal elements).
3. Bubble Sort
Definition: Repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order.
How it works:
- Compare adjacent elements.
- If they are in the wrong order, swap them.
- Repeat until the list is sorted.
Visualization (Sorting [5, 1, 4, 2, 8]):
graph TD
A["Initial: [5,1,4,2,8]"] --> B["Pass 1: [1,4,2,5,8]"]
B --> C["Pass 2: [1,2,4,5,8]"]
C --> D["Pass 3: [1,2,4,5,8] (sorted)"]Example Code:
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
Trace for [5, 1, 4, 2, 8]:
| Pass | j | Compare (arr[j] > arr[j+1]) | Swap? | Array State |
|---|---|---|---|---|
| 1 | 0 | 5 > 1 | Yes | [1,5,4,2,8] |
| 1 | 1 | 5 > 4 | Yes | [1,4,5,2,8] |
| 1 | 2 | 5 > 2 | Yes | [1,4,2,5,8] |
| 2 | 0 | 1 > 4 | No | [1,4,2,5,8] |
| 2 | 1 | 4 > 2 | Yes | [1,2,4,5,8] |
| 3 | 0 | 1 > 2 | No | [1,2,4,5,8] |
Time Complexity:
- Best case: (already sorted, with optimized version).
- Average/Worst case: . Space Complexity: (in-place).
Advantages:
- Simple to implement.
- Stable (preserves order of equal elements).
Disadvantages:
- Inefficient for large datasets.
- Performs many unnecessary swaps.
4. Merge Sort
Definition: A divide-and-conquer algorithm that recursively splits the array into halves, sorts them, and merges the sorted halves.
How it works:
- Divide the unsorted array into subarrays of size 1.
- Merge subarrays to produce new sorted subarrays until the entire array is sorted.
Visualization (Sorting [38, 27, 43, 3, 9, 82, 10]):
Example Code:
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2
L = arr[:mid]
R = arr[mid:]
merge_sort(L)
merge_sort(R)
i = j = k = 0
while i < len(L) and j < len(R):
if L[i] < R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
while i < len(L):
arr[k] = L[i]
i += 1
k += 1
while j < len(R):
arr[k] = R[j]
j += 1
k += 1
Trace for [38, 27, 43, 3, 9, 82, 10]:
| Step | Action | Subarrays | Merged Result |
|---|---|---|---|
| 1 | Divide | [38], [27], [43], [3], [9], [82], [10] | - |
| 2 | Merge [38,27] | - | [27,38] |
| 3 | Merge [43,3] | - | [3,43] |
| 4 | Merge [9,82] | - | [9,82] |
| 5 | Merge [27,38] and [3,43] | - | [3,27,38,43] |
| 6 | Merge [3,27,38,43] and [9,82] | - | [3,9,27,38,43,82] |
| 7 | Merge with [10] | - | [3,9,10,27,38,43,82] |
Time Complexity: (all cases). Space Complexity: (requires auxiliary space).
Advantages:
- Efficient for large datasets.
- Stable (preserves order of equal elements).
- Works well for linked lists.
Disadvantages:
- Requires extra space.
- Slower than quicksort for small datasets.
5. Quick Sort
Definition: A divide-and-conquer algorithm that selects a 'pivot' element and partitions the array into two subarrays: elements less than the pivot and elements greater than the pivot.
How it works:
- Choose a pivot (e.g., last element).
- Partition the array such that elements < pivot are on the left, and elements > pivot are on the right.
- Recursively sort the subarrays.
Visualization (Sorting [10, 7, 8, 9, 1, 5] with pivot = last element):
Example Code:
def quick_sort(arr, low, high):
if low < high:
pi = partition(arr, low, high)
quick_sort(arr, low, pi - 1)
quick_sort(arr, pi + 1, high)
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] < pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
Trace for [10, 7, 8, 9, 1, 5]:
| Step | Pivot | Partition Index (i) | Swaps | Array State |
|---|---|---|---|---|
| 1 | 5 | 0 | Swap 1 and 10 | [1,7,8,9,10,5] |
| 2 | 5 | 1 | Swap 5 and 10 | [1,5,8,9,10,7] |
| 3 | 7 | 2 | Swap 8 and 7 | [1,5,7,9,10,8] |
| 4 | 7 | 3 | No swaps | [1,5,7,9,10,8] (sorted) |
Time Complexity:
- Best/Average case: .
- Worst case: (when pivot is smallest/largest element). Space Complexity: (due to recursion stack).
Advantages:
- Fastest in practice for large datasets.
- In-place (minimal extra space).
- Cache-friendly (good locality).
Disadvantages:
- Unstable (does not preserve order of equal elements).
- Worst-case performance can be avoided with good pivot selection (e.g., median-of-three).
6. Heap Sort
Definition: Uses a binary heap data structure to sort elements. It first builds a max-heap (or min-heap) and repeatedly extracts the maximum (or minimum) element.
How it works:
- Build a max-heap from the input data.
- Extract the maximum element (root) and place it at the end of the array.
- Reduce the heap size and heapify the root to maintain the heap property.
- Repeat until the heap is empty.
Visualization (Heapify and Sort [12, 9, 1, 13, 16, 24, 21, 5]):
graph TD
A["Initial Array: [12,9,1,13,16,24,21,5]"] --> B["Max-Heap: [24,16,21,13,12,9,1,5]"]
B --> C["Extract 24: [16,12,21,13,5,9,1]"]
C --> D["Heapify: [21,12,16,13,5,9,1]"]
D --> E["Extract 21: [16,12,9,13,5,1]"]
E --> F["Heapify: [16,12,9,13,5,1]"]
F --> G["Final Sorted: [1,5,9,12,13,16,21,24]"]Example Code:
def heapify(arr, n, i):
largest = i
l = 2 * i + 1
r = 2 * i + 2
if l < n and arr[l] > arr[largest]:
largest = l
if r < n and arr[r] > arr[largest]:
largest = r
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heap_sort(arr):
n = len(arr)
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
Trace for [12, 9, 1, 13, 16, 24, 21, 5]:
| Step | Heap State | Extracted Max | Remaining Heap |
|---|---|---|---|
| 1 | [24,16,21,13,12,9,1,5] | 24 | [16,12,21,13,5,9,1] |
| 2 | [21,16,9,13,5,1,12] | 21 | [16,12,9,13,5,1] |
| 3 | [16,12,9,13,5,1] | 16 | [13,12,9,5,1] |
| 4 | [13,12,9,5,1] | 13 | [12,9,5,1] |
| 5 | [12,9,5,1] | 12 | [9,5,1] |
| 6 | [9,5,1] | 9 | [5,1] |
| 7 | [5,1] | 5 | [1] |
| 8 | [1] | 1 | [] |
Time Complexity: (all cases). Space Complexity: (in-place).
Advantages:
- Efficient for large datasets.
- In-place (no extra space).
- Guaranteed performance.
Disadvantages:
- Unstable (does not preserve order of equal elements).
- Slower in practice than quicksort due to poor cache performance.
Comparison of Sorting Algorithms
| Algorithm | Best Case | Average Case | Worst Case | Space Complexity | Stable? | Use Case |
|---|---|---|---|---|---|---|
| Insertion Sort | Yes | Small or nearly sorted datasets | ||||
| Selection Sort | No | Small datasets | ||||
| Bubble Sort | Yes | Educational purposes | ||||
| Merge Sort | Yes | Large datasets, external sorting | ||||
| Quick Sort | No | General-purpose, large datasets | ||||
| Heap Sort | No | Real-time systems |
Exam Tip
Understand the Definitions:
- Know the difference between internal (in-memory) and external (disk-based) sorting.
- Differentiate between stable (preserves order) and unstable sorts.
Algorithm Selection:
- For small datasets: Insertion Sort or Selection Sort.
- For large datasets: Merge Sort or Quick Sort.
- When stability matters: Merge Sort or Insertion Sort.
- When memory is constrained: Heap Sort or Quick Sort.
Tracing is Key:
- Always show step-by-step traces for sorting/searching algorithms in exams.
- Draw arrays/heaps after each operation (e.g., heapify, partition).
Time Complexity:
- Memorize the Big-O for best/average/worst cases of each algorithm.
- Example: Quick Sort is on average but in the worst case.
Real-World Applications:
- Relate algorithms to Nepali examples (e.g., eSewa’s binary search, Khalti’s merge sort).
- Explain how priority queues (used in Pathao’s ride allocation) work.
Common Pitfalls:
- Forgetting to sort the array first before binary search.
- Incorrect pivot selection in Quick Sort leading to worst-case performance.
- Misapplying stable vs. unstable sorts in scenarios where order matters.
Based on the TU BCA syllabus for Data Structures And Algorithms (CACS201), unit 6.
Discussion
Loading…