0% found this document useful (0 votes)
35 views8 pages

Sorting Algorithms Notes

This document provides a comprehensive overview of sorting algorithms, detailing their ideas, pseudocode, time and space complexities, stability, and practical guidance on usage. It covers both comparison sorts (like Bubble, Merge, and Quick Sort) and non-comparison sorts (like Counting and Radix Sort), along with hybrid algorithms such as TimSort. The document includes a summary table for quick reference and discusses the theoretical lower bounds for comparison sorts.

Uploaded by

patilchinmay510
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views8 pages

Sorting Algorithms Notes

This document provides a comprehensive overview of sorting algorithms, detailing their ideas, pseudocode, time and space complexities, stability, and practical guidance on usage. It covers both comparison sorts (like Bubble, Merge, and Quick Sort) and non-comparison sorts (like Counting and Radix Sort), along with hybrid algorithms such as TimSort. The document includes a summary table for quick reference and discusses the theoretical lower bounds for comparison sorts.

Uploaded by

patilchinmay510
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Comprehensive Sorting Algorithms — Detailed Notes

**Scope:** This document covers sorting algorithms from


basics to advanced. For each algorithm you'll find: idea,
detailed explanation, pseudocode, time complexity
(best/average/worst) with derivations, space complexity,
stability, variations, and example walkthroughs. --- ## Table
of Contents 1. Introduction & Lower Bound for Comparison
Sorts 2. Summary Table (quick reference) 3. Elementary sorts
- Bubble Sort - Selection Sort - Insertion Sort 4. Shell Sort 5.
Merge Sort (top-down and bottom-up) 6. Quick Sort (Lomuto,
Hoare, randomized, 3-way) 7. Heap Sort 8. Non-comparison
sorts - Counting Sort - Radix Sort (LSD and MSD) - Bucket
Sort 9. Hybrid & Practical Algorithms - IntroSort - TimSort 10.
When to use which sort (practical guidance) 11. Appendix:
Recurrence solving and Master Theorem sketches 12.
References & further reading --- ## 1. Introduction & Lower
Bound for Comparison Sorts **Problem statement.** Given an
array of `n` items (numbers or keys), arrange them in
non-decreasing order. **Comparison sorts vs
non-comparison sorts.** A comparison sort determines order
only by pairwise comparisons of elements. Non-comparison
sorts (counting, radix, bucket) exploit structure of keys
(integers with limited range, digits, etc.). **Lower bound for
comparison sorts (sketch).** Any comparison sort can be
modeled by a decision tree where each internal node is a
binary comparison and leaves represent permutations of the
input. There are `n!` possible permutations, so the decision
tree must have at least `n!` leaves. If a binary tree has height
`h`, it has at most `2^h` leaves, so `2^h >= n!` ⇒ `h >=
log2(n!)`. Using Stirling's approximation: `log2(n!) = Θ(n log
n)`. Therefore any comparison sort must perform Ω(n log n)
comparisons in the worst case. So **comparison sorts** have
a theoretical lower bound of **Ω(n log n)** time (worst-case),
hence `O(n log n)` is optimal among comparison sorts. --- ##
2. Summary Table (quick reference) | Algorithm | Best |
Average | Worst | Space | Stable | Notes |
|---|---:|---:|---:|---:|---|---| | Bubble Sort | O(n) | O(n^2) | O(n^2) |
O(1) | Yes | Educational only | | Selection Sort | O(n^2) |
O(n^2) | O(n^2) | O(1) | No | Minimal swaps | | Insertion Sort |
O(n) | O(n^2) | O(n^2) | O(1) | Yes | Good for
small/mostly-sorted | | Shell Sort | depends | depends |
depends | O(1) | No | Gap sequence matters | | Merge Sort |
O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | Stable,
predictable | | Quick Sort | O(n log n) | O(n log n) | O(n^2) |
O(log n) avg | No | Fast in practice; introsort avoids
worst-case | | Heap Sort | O(n log n) | O(n log n) | O(n log n) |
O(1) | No | In-place, predictable worst-case | | Counting Sort |
O(n + k) | O(n + k) | O(n + k) | O(k + n) | Yes | k = range size | |
Radix Sort | O(d(n + b)) | O(d(n + b)) | O(d(n + b)) | O(n + b) |
Stable (if inner stable) | d digits, b base | | Bucket Sort | O(n +
k) avg | O(n + k) | O(n^2) worst | O(n + k) | Depends | Assumes
uniform distribution | | IntroSort | O(n log n) | O(n log n) | O(n
log n) | O(log n) | Depends | std::sort in many libraries | |
TimSort | O(n) best | O(n log n) | O(n log n) | O(n) | Yes |
Python/Java stable hybrid | --- ## 3. Elementary sorts ### 3.1
Bubble Sort **Idea.** Repeatedly scan the array, swapping
adjacent elements that are out of order. After each full pass
the largest remaining element "bubbles" to the end.
**Pseudocode (optimized):** ``` for (i = 0; i < n-1; ++i) {
swapped = false for (j = 0; j < n-1-i; ++j) { if (A[j] > A[j+1]) {
swap(A[j], A[j+1]); swapped = true } } if (!swapped) break } ```
**Time complexity derivation:** - Worst/average: Without the
early-stop optimization, number of comparisons is (n-1) +
(n-2) + ... + 1 = n(n-1)/2 = Θ(n^2). Number of swaps in
worst-case is also Θ(n^2). - Best: If the array already sorted,
the optimized version detects no swaps and completes in
O(n) (single pass). **Space:** O(1). Stable. **When to use:**
Practically none — educational or for tiny arrays in exercises.
### 3.2 Selection Sort **Idea.** Repeatedly select the minimal
(or maximal) remaining element and place it at the front (or
back). **Pseudocode:** ``` for (i = 0; i < n-1; ++i) { minIdx = i
for (j = i+1; j < n; ++j) if (A[j] < A[minIdx]) minIdx = j swap(A[i],
A[minIdx]) } ``` **Complexity derivation:** - Comparisons:
(n-1) + (n-2) + ... + 1 = Θ(n^2) in all cases → best=avg=worst
Θ(n^2). - Swaps: Only n-1 swaps, which is useful when swaps
are expensive. **Space:** O(1). Not stable by default (but can
be made stable at extra cost). **When to use:** Simple and
predictable, but rarely used in practice for large n. ### 3.3
Insertion Sort **Idea.** Build the final sorted array one
element at a time by inserting each new element into its
correct position among previously sorted elements (like
sorting playing cards in your hand). **Pseudocode:** ``` for (i
= 1; i < n; ++i) { key = A[i] j = i - 1 while (j >= 0 and A[j] > key) {
A[j+1] = A[j] j-- } A[j+1] = key } ``` **Complexity derivation:** -
Worst-case (reverse order): For each i, shift i elements ⇒
comparisons/shifts ~ 1 + 2 + ... + (n-1) = Θ(n^2). - Best-case
(already sorted): Each insertion checks once ⇒ Θ(n)
comparisons and Θ(1) shifts. - Average: On random input
average number of shifts is about n^2/4 → Θ(n^2). **Space:**
O(1). Stable. **When to use:** Great for small arrays or
nearly-sorted arrays. Often used as the base-case sorter in
hybrids (e.g., Timsort, introsort) for subarrays of size ≤ 16–32.
--- ## 4. Shell Sort **Idea.** Generalization of insertion sort:
perform insertion sort on elements separated by a gap `g`,
decreasing `g` over iterations until it becomes 1. Early passes
move elements closer to their final position, reducing the
cost of the final insertion sort. **Pseudocode (high-level):** ```
gaps = choose_gap_sequence(n) for gap in gaps: for i = gap
to n-1: temp = A[i] j = i while j >= gap and A[j - gap] > temp:
A[j] = A[j - gap] j -= gap A[j] = temp ``` **Gap sequences and
complexity:** - Complexity depends on gap sequence. -
Original Shell sequence (`n/2, n/4, ...`) yields worst-case
O(n^2). - Pratt, Ciura, Sedgewick sequences improve
performance; with good sequences complexity can be as low
as `O(n^{4/3})` or `O(n^{3/2})` for some choices. **Space:**
O(1). Not stable. **When to use:** Simple in-place
improvement over insertion sort. Good in practice for
medium-size arrays when simple and low-memory. --- ## 5.
Merge Sort **Idea.** Divide-and-conquer: split the array into
two halves, recursively sort each half, then merge the sorted
halves. **Top-down recursive pseudocode:** ``` mergesort(A,
left, right): if left >= right: return mid = (left + right) / 2
mergesort(A, left, mid) mergesort(A, mid+1, right) merge(A,
left, mid, right) // linear-time merge using temp array ```
**Merge step (linear time):** - Use two pointers i (left half) and
j (right half), copy smaller element into temp and advance
pointer. After one half is exhausted copy remainder.
**Complexity derivation (recurrence):** - Let T(n) be time to
sort n elements. We split into two halves of size n/2 and
merge in Θ(n) time. - Recurrence: `T(n) = 2 T(n/2) + cn`. - Solve
by Master Theorem: a = 2, b = 2 → n^{log_b a} = n^{1} = n.
Since f(n) = Θ(n), T(n) = Θ(n log n). - **Therefore** best =
average = worst = Θ(n log n) (recurrence deterministic
regardless of input ordering). **Space:** Θ(n) auxiliary (for
merging). There are in-place merge variants but they're
complex and slower. **Stability:** Stable by default (if merge
copies equal keys from left before right). **Bottom-up
(iterative) merge sort:** start by merging runs of size 1, then
2, then 4, doubling each time. Same complexity. **When to
use:** Stable, predictable time. Good for linked lists (merge
sort on lists is O(1) space for merging because splicing is
O(1)) and external sorting (merge of sorted runs on disk).
**Example Walkthrough:** - Sort `[5,2,3,1]` → split `[5,2]` &
`[3,1]` → sort `[5,2]` → `[2,5]` and `[1,3]` → merge `[1,2,3,5]`. ---
## 6. Quick Sort **Idea.** Pick a pivot, partition array into
elements less than pivot and greater than pivot, recursively
sort the partitions. **Partition (Lomuto) pseudocode
(simple):** ``` partition(A, low, high): pivot = A[high] i = low for
j = low to high-1: if A[j] <= pivot: swap(A[i], A[j]) i++ swap(A[i],
A[high]) return i // pivot final index ``` **Hoare partition (often
faster, different invariants) exists too.** **Randomized
quicksort:** choose pivot randomly (swap with A[high] first).
This avoids pathological inputs. **Complexity:** -
**Worst-case:** O(n^2) when pivot choices are extremely
unbalanced (e.g., sorted input and pivot = first or last element
repeatedly). The recurrence becomes `T(n) = T(n-1) + Θ(n)` →
Θ(n^2). - **Average-case (random pivot):** Θ(n log n). Sketch
of derivation: - Let T(n) be expected time. The pivot splits
array into sizes `k` and `n-1-k` with equal probability for each
k. The expectation yields: \\[ E[T(n)] = \\frac{1}{n}
\\sum_{k=0}^{n-1} (E[T(k)] + E[T(n-1-k)]) + Θ(n). \\] This
simplifies to `E[T(n)] = (2/n) \\sum_{k=0}^{n-1} E[T(k)] + Θ(n)`.
Solving this recurrence gives `E[T(n)] = Θ(n log n)`. -
**Best-case:** perfectly balanced splits each level → T(n) =
2T(n/2) + Θ(n) → Θ(n log n). **Space:** average call stack
O(log n) for balanced recursion; O(n) worst-case. In-place (no
large aux arrays). Not stable. **Practical notes &
optimizations:** - Use randomized pivot or median-of-three to
improve pivot quality. - For arrays with many duplicate keys,
use 3-way partitioning (Dutch National Flag) to get linear time
for many duplicates. - For small subarrays (size ≤ 16), switch
to insertion sort. - Introsort: set recursion depth limit (e.g., 2 *
log n). If exceeded, switch to heapsort to guarantee O(n log
n). **Example:** partition `[4,5,3,7,2]` with pivot 2 (last) →
many elements > pivot, pivot ends up at index 0 → bad split
→ leads toward worst-case if repeated. --- ## 7. Heap Sort
**Idea.** Use a binary heap (usually max-heap) stored in the
same array. Build a max-heap, then repeatedly swap the root
(max) with the last element, reduce heap size by 1, and
heapify the root. **Pseudocode:** ``` buildMaxHeap(A): for i =
floor(n/2)-1 down to 0: heapify(A, i, n) for end = n-1 downto 1:
swap(A[0], A[end]) heapify(A, 0, end) ``` **Complexity
derivation:** - `heapify` (sift-down) cost is O(h) where h is the
height of node. - Building heap: naive upper bound O(n log
n), but exact cost is O(n) because most nodes are near leaves
and have small height. Summing heights yields O(n). - Formal
sketch: sum_{i=0}^{n-1} O(height(i)) = O(n). - Each of the n-1
extractions costs O(log n) to heapify → O(n log n). Hence
total: O(n) + O(n log n) = O(n log n). **Space:** O(1) auxiliary
(in-place). Not stable. **When to use:** When worst-case O(n
log n) with O(1) extra space is required. Slightly slower
constants than quicksort in practice. --- ## 8.
Non-comparison sorts ### 8.1 Counting Sort **Idea.** For
integer keys in `[0..k-1]`, count frequencies then write output
by iterating counts. Use prefix sums to make it stable.
**Pseudocode (stable):** ``` count[0..k-1] = {0} for each x in A:
count[x]++ for i = 1 to k-1: count[i] += count[i-1] for i = n-1
downto 0: out[count[A[i]]-1] = A[i] count[A[i]]-- return out ```
**Complexity:** O(n + k) time, O(n + k) space. Stable if
implemented with prefix sums as above. **When to use:**
When keys are integers within limited range and k is not huge
(k = O(n) or smaller), counting sort is linear. ### 8.2 Radix
Sort **Idea.** Sort keys by processing digits or groups of bits
from least significant to most significant (LSD) or vice-versa
(MSD). Use stable sort (counting sort) as the stable
subroutine. **Complexity:** Let `d` be number of digits and
`b` be base (e.g., b=10 or b=256). Time: O(d(n + b)). If `d` is
constant (e.g., 32-bit ints => d ~ 4 when using bytes), this is
O(n). Space: O(n + b). **LSD vs MSD:** - LSD: stable sort by
least significant digit upward. Works well when digit count is
fixed. - MSD: recursively bucket by most significant digit,
good for variable-length keys. **When to use:** Sorting
integers/strings when you can treat keys as sequences of
digits and `d` and `b` make overall time linear. ### 8.3 Bucket
Sort **Idea.** Distribute elements into `k` buckets (intervals),
sort each bucket (often with insertion sort), then concatenate.
**Average complexity:** If input is uniformly distributed and
`k` ≈ `n`, expected time Θ(n + k) = Θ(n). Worst-case can be
O(n^2) if all elements fall into one bucket. **Space:** O(n + k).
Stability depends on internal sort. **When to use:** When
input is uniformly distributed over a range and you can
choose bucket boundaries accordingly. --- ## 9. Hybrid &
Practical Algorithms ### 9.1 IntroSort **Idea.** Start with
quicksort for average speed; if recursion depth exceeds a
threshold (sign of degenerating splits), switch to heapsort to
guarentee O(n log n) worst-case. Also use insertion sort for
tiny partitions. **Used by:** Many std::sort implementations
(C++), because it combines practical speed with worst-case
guarantees. **Complexity:** O(n log n) worst/average, with
O(log n) stack. ### 9.2 TimSort **Idea.** Hybrid stable sorting
algorithm derived from merge sort and insertion sort used by
Python and Java's [Link] for objects. It: - Identifies
natural runs (monotonic sequences) in the data - Extends
short runs with insertion sort to at least a minimum run
length - Merges runs using a merge stack with heuristics (and
a \"galloping\" mode if one run is much larger than the other)
**Complexity:** Worst-case O(n log n), best-case O(n) when
data already has long runs. Stable. **Why practical:** Makes
use of existing order in real data (many real-world arrays
have runs), very fast in practice. --- ## 10. When to use which
sort (practical guidance) - **Small arrays (n ≤ 32):** insertion
sort or optimized insertion as base-case inside
divide-and-conquer. - **General-purpose sorting of
primitives:** quicksort (or introsort in libraries) is a common
default because of excellent practical performance. - **Need
guaranteed worst-case and O(1) extra space:** heapsort. -
**Need stability and predictable O(n log n):** merge sort. -
**Large integer keys with limited range:** counting sort or
radix sort (linear time). - **Real-world stable general-purpose
sort (objects) with adaptive behavior:** TimSort. --- ## 11.
Appendix: Recurrences & Master Theorem (short sketches)
**Master Theorem (very short):** For `T(n) = a T(n/b) + f(n)`
where `a >= 1`, `b > 1`: - If `f(n) = O(n^{c})` with `c < log_b a`
then `T(n) = Θ(n^{log_b a})`. - If `f(n) = Θ(n^{log_b a} * log^k
n)` then `T(n) = Θ(n^{log_b a} * log^{k+1} n)`. - If `f(n) =
Ω(n^{c})` with `c > log_b a` and regularity holds, then `T(n) =
Θ(f(n))`. **Examples:** - Merge sort: `T(n)=2T(n/2)+Θ(n)` →
`a=2, b=2, log_b a=1` and `f(n)=Θ(n)`, so `T(n)=Θ(n log n)`. -
Quick sort (best case): `T(n)=2T(n/2)+Θ(n)` → Θ(n log n).
Worst-case `T(n)=T(n-1)+Θ(n)` → Θ(n^2). --- ## 12. References
& further reading - *Introduction to Algorithms* — Cormen,
Leiserson, Rivest, Stein (CLRS) - *Algorithms* — Robert
Sedgewick & Kevin Wayne - Tim Peters — TimSort
description (searchable online) --- *End of document.*

You might also like