Data Structure and AlgorithmsUnit 815 min read
Sorting Algorithms: Techniques, Analysis & Applications
Unit 8 of Data Structure and Algorithms covers fundamental sorting techniques (Bubble, Selection, Insertion, Merge, Quick, Heap), their time/space complexity, stability, and real-world applications in Nepalese tech (eSewa, Ncell, Daraz) and global systems (Google, WhatsApp). Includes visual traces of each algorithm, co
Key Concepts and Definitions
What is Sorting?
Sorting is the process of arranging data in a particular order (ascending or descending). It is a fundamental operation in computer science used to organize data for efficient searching, indexing, and analysis.
Types of Sorting Algorithms
Sorting algorithms can be classified into two main categories:
- Comparison-based sorts: Compare elements to determine their order (e.g., Bubble Sort, Quick Sort).
- Non-comparison-based sorts: Use properties of the data (e.g., Counting Sort, Radix Sort).
1. Basic Sorting Algorithms
1.1 Bubble Sort
How it works: Bubble Sort repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. The process is repeated until the list is sorted.
flowchart TD
A["Start: Unsorted array"] --> B["Compare A[0] & A[1]"]
B -->|"If A[0] > A[1]"| C["Swap A[0] & A[1]"]
B -->|"Else"| D["No swap"]
C --> E["Move to next pair (A[1] & A[2])"]
D --> E
E --> F["Repeat until end of array"]
F --> G["Pass complete: Largest element bubbled to end"]
G --> H["Repeat for n-1 passes"]Time Complexity:
- Worst/Average Case:
- Best Case: (if already sorted and optimized)
Space Complexity: (in-place)
Example:
Sort the array [5, 3, 8, 4, 2] using Bubble Sort.
arr = [5, 3, 8, 4, 2]
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]
print(f"After pass {i+1}: {arr}")
Trace:
| Pass | Array State |
|---|---|
| 1 | [3, 5, 4, 2, 8] |
| 2 | [3, 4, 2, 5, 8] |
| 3 | [3, 2, 4, 5, 8] |
| 4 | [2, 3, 4, 5, 8] |
Advantages:
- Simple to understand and implement.
- No extra memory required.
Disadvantages:
- Inefficient for large datasets ( time).
Real-world use:
- eSewa: When sorting transactions by date or amount for user statements.
1.2 Selection Sort
How it works: Selection Sort divides the array into a sorted and unsorted part. It repeatedly selects the smallest (or largest) element from the unsorted part and moves it to the sorted part.
flowchart TD
A["Start: Unsorted array"] --> B["Find min in unsorted part"]
B --> C["Swap min with first unsorted element"]
C --> D["Move boundary between sorted/unsorted right"]
D --> E["Repeat until entire array is sorted"]Time Complexity:
- Worst/Average Case:
- Best Case: (even if sorted, it still scans the entire array)
Space Complexity:
Example:
Sort the array [64, 25, 12, 22, 11].
arr = [64, 25, 12, 22, 11]
n = len(arr)
for i in range(n):
min_idx = i
for j in range(i+1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
print(f"After pass {i+1}: {arr}")
Trace:
| Pass | Array State |
|---|---|
| 1 | [11, 25, 12, 22, 64] |
| 2 | [11, 12, 25, 22, 64] |
| 3 | [11, 12, 22, 25, 64] |
| 4 | [11, 12, 22, 25, 64] |
Advantages:
- Simple to implement.
- Performs well on small datasets.
Disadvantages:
- Inefficient for large datasets.
Real-world use:
- Ncell: Sorting customer call logs by duration to identify peak usage times.
1.3 Insertion Sort
How it works: Insertion Sort builds the final sorted array one item at a time. It takes each element and inserts it into its correct position in the already sorted part of the array.
flowchart TD
A["Start: Unsorted array"] --> B["Pick next element"]
B --> C["Compare with sorted elements"]
C -->|"Insert at correct position"| D["Shift elements if needed"]
D --> E["Repeat until all elements are sorted"]Time Complexity:
- Worst Case:
- Best Case: (if already sorted)
- Average Case:
Space Complexity:
Example:
Sort the array [12, 11, 13, 5, 6].
arr = [12, 11, 13, 5, 6]
for i in range(1, len(arr)):
key = arr[i]
j = i-1
while j >= 0 and key < arr[j]:
arr[j+1] = arr[j]
j -= 1
arr[j+1] = key
print(f"After insertion of {key}: {arr}")
Trace:
| Step | Array State |
|---|---|
| 1 | [11, 12, 13, 5, 6] |
| 2 | [11, 12, 13, 5, 6] |
| 3 | [5, 11, 12, 13, 6] |
| 4 | [5, 6, 11, 12, 13] |
Advantages:
- Efficient for small datasets or nearly sorted data.
- Stable (does not change the order of equal elements).
Disadvantages:
- Inefficient for large datasets.
Real-world use:
- Khalti: Sorting transaction records by timestamp for audit trails.
2. Advanced Sorting Algorithms
2.1 Merge Sort
How it works: Merge Sort is a divide-and-conquer algorithm. It divides the array into two halves, recursively sorts each half, and then merges the two sorted halves.
flowchart TD
A["Divide array into two halves"] --> B["Recursively sort left half"]
B --> C["Recursively sort right half"]
C --> D["Merge the two sorted halves"]Time Complexity:
- Worst/Average Case:
- Best Case:
Space Complexity: (requires auxiliary space)
Example:
Sort the array [38, 27, 43, 3, 9, 82, 10].
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
return arr
arr = [38, 27, 43, 3, 9, 82, 10]
print("Initial array:", arr)
sorted_arr = merge_sort(arr)
print("Sorted array:", sorted_arr)
Trace:
Initial array: [38, 27, 43, 3, 9, 82, 10]
Sorted array: [3, 9, 10, 27, 38, 43, 82]
Visualization of Merge Sort:
Step 1: Divide [38, 27, 43, 3, 9, 82, 10] into [38, 27, 43] and [3, 9, 82, 10]
Step 2: Divide further until single elements
Step 3: Merge sorted subarrays: [3, 9, 10, 27, 38, 43, 82]
Advantages:
- Efficient for large datasets ().
- Stable sort.
Disadvantages:
- Requires additional memory.
Real-world use:
- Google: Sorting search results by relevance (part of their indexing pipeline).
2.2 Quick Sort
How it works: Quick Sort is another divide-and-conquer algorithm. It selects a 'pivot' element and partitions the array into two subarrays: elements less than the pivot and elements greater than the pivot. It then recursively sorts the subarrays.
flowchart TD
A["Choose pivot"] --> B["Partition array into <pivot and >pivot"]
B --> C["Recursively sort left subarray"]
B --> D["Recursively sort right subarray"]Time Complexity:
- Worst Case: (when pivot is poorly chosen, e.g., smallest or largest element)
- Average Case:
- Best Case:
Space Complexity: (due to recursion stack)
Example:
Sort the array [10, 7, 8, 9, 1, 5] using Quick Sort (pivot = last element).
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
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)
arr = [10, 7, 8, 9, 1, 5]
print("Initial array:", arr)
quick_sort(arr, 0, len(arr)-1)
print("Sorted array:", arr)
Trace:
| Step | Pivot | Partitioned Array | Recursive Calls |
|---|---|---|---|
| 1 | 5 | [1, 5, 8, 9, 10, 7] | Sort left: [1], right: [8,9,10,7] |
| 2 | 7 | [1, 5, 7, 9, 10, 8] | Sort left: [1,5], right: [9,10,8] |
| 3 | 8 | [1, 5, 7, 8, 10, 9] | Sort left: [1,5,7], right: [10,9] |
| 4 | 9 | [1, 5, 7, 8, 9, 10] | Base case reached |
Advantages:
- Fastest in practice for large datasets.
- In-place sorting (minimal extra memory).
Disadvantages:
- Worst-case if pivot is poorly chosen.
- Unstable sort.
Real-world use:
- WhatsApp: Sorting message timestamps for chat history display.
2.3 Heap Sort
How it works: Heap Sort uses a binary heap data structure. It first builds a max-heap from the input data, then repeatedly extracts the maximum element and rebuilds the heap.
flowchart TD
A["Build max-heap from array"] --> B["Extract max element (root)"]
B --> C["Place max at end of array"]
C --> D["Reduce heap size and heapify"]
D --> E["Repeat until heap is empty"]Time Complexity:
- Worst/Average Case:
- Best Case:
Space Complexity:
Example:
Sort the array [4, 10, 3, 5, 1] using Heap Sort.
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)
arr = [4, 10, 3, 5, 1]
print("Initial array:", arr)
heap_sort(arr)
print("Sorted array:", arr)
Trace:
| Step | Heap State (Max at Root) | Extracted Max | Remaining Array |
|---|---|---|---|
| 1 | [10, 5, 3, 4, 1] | 10 | [4, 5, 3, 1] |
| 2 | [5, 4, 3, 1] | 5 | [4, 3, 1] |
| 3 | [4, 1, 3] | 4 | [3, 1] |
| 4 | [3, 1] | 3 | [1] |
| 5 | [1] | 1 | [] |
Advantages:
- Guaranteed time.
- In-place sorting.
Disadvantages:
- Not stable.
- Slower in practice than Quick Sort due to poor cache performance.
Real-world use:
- NEPSE: Sorting stock prices for daily trading reports.
3. Comparison of Sorting Algorithms
| Algorithm | Best Case | Average Case | Worst Case | Space Complexity | Stable? | Notes |
|---|---|---|---|---|---|---|
| Bubble Sort | Yes | Simple, inefficient | ||||
| Selection Sort | No | Minimizes swaps | ||||
| Insertion Sort | Yes | Efficient for small datasets | ||||
| Merge Sort | Yes | Stable, good for linked lists | ||||
| Quick Sort | No | Fastest in practice | ||||
| Heap Sort | No | In-place, not stable |
4. Stability in Sorting
A sorting algorithm is stable if it preserves the relative order of equal elements. For example:
- Stable: Bubble Sort, Insertion Sort, Merge Sort.
- Unstable: Selection Sort, Quick Sort, Heap Sort.
Example:
Sort the array [3, 1, 4, 1, 5, 9, 2, 6] where the second 1 must appear before the first 1 in the output if they are equal.
flowchart TD
A["Unsorted: [3, 1, 4, 1, 5, 9, 2, 6]"] --> B["Stable Sort: [1, 1, 2, 3, 4, 5, 6, 9]"]
B --> C["Unstable Sort: [1, 1, 2, 3, 4, 5, 6, 9] (order of 1's may change)"]Real-world use:
- Daraz: Sorting product listings by price while maintaining the order of products with the same price (e.g., based on user ratings).
5. Choosing the Right Sorting Algorithm
| Scenario | Recommended Algorithm |
|---|---|
| Small datasets (<100 elements) | Insertion Sort |
| Nearly sorted data | Insertion Sort |
| General-purpose sorting | Quick Sort |
| Stable sort required | Merge Sort |
| Memory constraints | Heap Sort or Quick Sort |
| External sorting (large data) | Merge Sort |
In the Real World
eSewa:
- Idea Used: Merge Sort or Quick Sort.
- How: eSewa sorts transactions by date and amount for generating user statements and financial reports. Efficient sorting ensures quick access to transaction history, improving user experience.
Ncell:
- Idea Used: Heap Sort or Quick Sort.
- How: Ncell uses sorting to prioritize customer calls based on duration or urgency. For example, calls with longer durations are sorted to identify peak usage times for network optimization.
Daraz:
- Idea Used: Stable Sorting (e.g., Merge Sort).
- How: Daraz sorts product listings by price while maintaining the order of products with identical prices (e.g., based on user ratings or reviews). This ensures consistency in search results.
Google Search:
- Idea Used: Advanced sorting algorithms (e.g., variants of Quick Sort or Merge Sort).
- How: Google sorts search results by relevance, which involves complex ranking algorithms that rely on efficient sorting to quickly retrieve the most relevant results for users.
NEPSE (Nepal Stock Exchange):
- Idea Used: Heap Sort or Merge Sort.
- How: NEPSE sorts stock prices and trading volumes to generate daily reports and rankings. Efficient sorting helps traders and analysts quickly access the most up-to-date information.
Khalti Transactions:
- Idea Used: Insertion Sort (for small datasets) or Merge Sort (for large datasets).
- How: Khalti sorts transactions by timestamp to create audit trails and detect fraudulent activities. Sorting ensures that transactions are processed in chronological order for accurate record-keeping.
Exam Tip
Understand Time Complexity:
- Memorize the time complexities of each algorithm (e.g., Bubble Sort is , Merge Sort is ). Examiners often ask to identify the best algorithm for a given scenario.
Practice Traces:
- Be able to manually trace the steps of any sorting algorithm (e.g., show how Bubble Sort sorts
[4, 2, 5, 1]). This is a common exam question.
- Be able to manually trace the steps of any sorting algorithm (e.g., show how Bubble Sort sorts
Stability Matters:
- Know which algorithms are stable (e.g., Merge Sort, Insertion Sort) and why stability is important in certain applications.
Comparison Tables:
- Expect questions comparing algorithms based on time complexity, space complexity, and stability. Create a comparison table like the one above to revise quickly.
Real-world Applications:
- Relate sorting algorithms to real-world systems (e.g., "Which sorting algorithm would you use for sorting transactions in eSewa?"). This tests your understanding of practical constraints (e.g., stability, speed).
Code Implementation:
- Be ready to write pseudocode or actual code for any sorting algorithm. Examiners may ask for implementations of Bubble Sort, Quick Sort, or Merge Sort.
Optimizations:
- Know basic optimizations (e.g., early termination in Bubble Sort if no swaps occur in a pass). This can be the difference between a correct and incorrect answer.
Based on the PU BE Computer (PU) syllabus for Data Structure and Algorithms (CMP160), unit 8.
Discussion
Loading…