Lecture 03 - Searching and Sorting Algorithms (Data Structures)

Built-in ADTs in Languages

  • Modern languages provide efficient, well-tested implementations of common data structures in standard libraries.

  • As professional programmers, you rarely implement these from scratch, but understanding how they work "under the hood" helps:

    • Choose the right data structure for your needs

    • Recognize and avoid performance pitfalls

    • Build your own when built-ins fall short

  • Analogy: learning how an engine works makes you a better driver, mechanic, and problem solver, even if you don’t hand-build an engine.

Language Core Library vs ADT Examples

  • Python: Core Library includes list, set, dict

  • C++: STL (Standard Template Library) includes vector, list, stack, queue, deque, set, map

  • Java: Java Collections Framework (JCF) includes Collection, Set, List, Map, Queue, Deque

Searching and Sorting: Overview

  • Searching algorithm: a method to locate a specific item/value within a data collection or defined space.

  • Basic use cases: search for values (e.g., a number in an array, a key in a dictionary) using techniques like linear search, binary search, or hash lookups.

  • Beyond the list: searching can mean exploring a solution space (e.g., maze paths, puzzles, function optimization) where each position is a candidate solution.

  • Algorithmic goal: efficiently reduce the number of possibilities by exploiting structure (ordering, heuristics, probability) to find a valid or optimal solution within acceptable time/space bounds.

Linear Search

  • Definition: sequential scan through all elements.

  • Pseudocode (conceptual):

    • LinearSearch(values, key):

    • i = 0

    • for each value in values:

      • if value == key: return i

      • else: i = i + 1

    • return -1

  • Complexity:

    • Best case: O(1)O(1)

    • Average case: O(N)O(N)

    • Worst case: O(N)O(N)

Binary Search

  • Prerequisite: data must be sorted.

  • Principle: each step halves the search space.

  • Pseudocode (iterative):

    • BinarySearch(values, key):

    • low = 0

    • high = size(values) - 1

    • while high >= low:

      • mid = (high - low) / 2 + low

      • if values[mid] < key: low = mid + 1

      • else if values[mid] > key: high = mid - 1

      • else: return mid

    • return -1

  • Complexity:

    • Best case: O(1)O(1)

    • Average case: O(ablaN)O(abla N)

    • Worst case: O(ablaN)O(abla N)

    • Note: <br>abla<br>abla O(\, ext{log} \, N)

Binary Search (Recursive)

  • Recursive version: BinarySearch(values, key, low, high)

    • if high < low: return -1

    • mid = (high - low) / 2 + low

    • if values[mid] < key: return BinarySearch(values, key, mid + 1, high)

    • else if values[mid] > key: return BinarySearch(values, key, low, mid - 1)

    • else: return mid

  • Call: BinarySearch(values, key, 0, size(values) - 1)

Other Search Techniques (as listed in the slides)

  • Interpolation Search: heuristic that uses the value distribution to guess where the key might be (details not spelled out in the transcript).

  • Jump Search: jump ahead by fixed-size blocks and then linear search within the block (details not specified in the transcript).

  • Exponential Search: expands a range exponentially to locate a block where the key may reside (details not specified in the transcript).

  • Note: The transcript explicitly labels these strategies but provides little to no complexity details beyond names.

Sorting: Divide and Conquer and Recursion Concepts

  • Theme: many sorting algorithms use divide and conquer, often via recursion, then combine sub-solutions.

  • Visual metaphor on the slides highlights building up from smaller pieces (e.g., cutting, merging, or partitioning treasure maps).

  • Core steps (Divide and Conquer):

    • Divide the problem into smaller subproblems of the same type (ideally roughly equal in size).

    • Conquer the pieces recursively, down to a simple base case.

    • Combine the sub-solutions to form the overall solution.

  • Efficiency comes from reducing problem size at each divide step.

Insertion Sort

  • Builds a sorted list one item at a time.

  • It is the only quadratic runtime sort shown as a practical, non-educational example in production contexts (per slide).

  • Pseudocode concept:

    • for i = 1 to array.size - 1:

    • j = i

    • while j > 0 and array[j] < array[j - 1]:

      • swap array[j] with array[j - 1]

      • j = j - 1

  • Complexity:

    • Best case: O(N)</p></li><li><p>Averagecase:</p></li><li><p>Average case:O(N^2)</p></li><li><p>Worstcase:</p></li><li><p>Worst case:O(N^2)</p></li></ul></li></ul><h3id="885e1b3fc8b1426499922eca4bff5aab"datatocid="885e1b3fc8b1426499922eca4bff5aab"collapsed="false"seolevelmigrated="true">MergeSort</h3><ul><li><p><strong>Concept:</strong>dividethearray,sorthalves,andmerge.</p></li><li><p><strong>Recurrence:</strong></p></li></ul></li></ul><h3 id="885e1b3f-c8b1-4264-9992-2eca4bff5aab" data-toc-id="885e1b3f-c8b1-4264-9992-2eca4bff5aab" collapsed="false" seolevelmigrated="true">Merge Sort</h3><ul><li><p><strong>Concept:</strong> divide the array, sort halves, and merge.</p></li><li><p><strong>Recurrence: </strong>T(n) = 2T(n/2) + O(n)</p></li><li><p><strong>Timecomplexity:</strong></p></li><li><p><strong>Time complexity:</strong>O(n \, log\, n) in all cases

    • Key operation: Merge two sorted halves in linear time.

    • Merge procedure (conceptual):

      • Merge(left, right): merge two sorted subarrays into a single sorted array.

      • Example structure: while leftPos <= leftEnd and rightPos <= rightEnd: compare and copy the smaller element; then copy any remaining elements from either side.

      • Finally copy merged elements back into the original array.

    • Master Theorem application (Merge Sort): a = 2, b = 2, f(n) = Θ(n) ⇒ Case 2 ⇒ T(n) = Θ(n log n).

    • Recursion tree visualization: cost per level doubles subproblems while total work per level remains Θ(n), yielding Θ(n log n) total.

    Quicksort

    • Core idea: choose a pivot, partition the array so that values less than the pivot go left and values greater go right.

    • Recursively sort the left and right partitions.

    • No separate merge step; ordering emerges from partitioning.

    • Pseudocode (partition):

      • Partition(array, low, high):

      • mid = low + (high - low) / 2

      • pivot = array[mid]

      • done = false

      • while not done:

        • while array[low] < pivot: low = low + 1

        • while pivot < array[high]: high = high - 1

        • if low >= high: done = true

        • else: swap array[low] with array[high]

        • low = low + 1

        • high = high - 1

      • return high // last index of low partition

    • Quicksort function:

      • Quicksort(array, low, high):

      • if low >= high: return

      • endLow = Partition(array, low, high)

      • Quicksort(array, low, endLow)

      • Quicksort(array, endLow + 1, high)

    • Complexity (average/best vs worst):

      • Best/Average case: O(n \, log n)</p></li><li><p>Worstcase:</p></li><li><p>Worst case:O(n^2)</p></li></ul></li><li><p>Recurrenceapproximation(viaMasterTheorem):best/average </p></li></ul></li><li><p>Recurrence approximation (via Master Theorem): best/average ~T(n) = O(n \, log n),worst , worst ~T(n) = O(n^2)</p></li></ul><h3id="ca75f20a856c4c50886474e61303ee76"datatocid="ca75f20a856c4c50886474e61303ee76"collapsed="false"seolevelmigrated="true">MergeSortvsQuicksort:Comparison(highlevel)</h3><ul><li><p>Algorithmtype:botharedivideandconquer.</p></li><li><p>Worstcasetime:MergeSort</p></li></ul><h3 id="ca75f20a-856c-4c50-8864-74e61303ee76" data-toc-id="ca75f20a-856c-4c50-8864-74e61303ee76" collapsed="false" seolevelmigrated="true">Merge Sort vs Quicksort: Comparison (high-level)</h3><ul><li><p>Algorithm type: both are divide-and-conquer.</p></li><li><p>Worst-case time: Merge SortO(n \, log n);Quicksort; QuicksortO(n^2)(worstcase).</p></li><li><p>Averagecasetime:bothapprox(worst case).</p></li><li><p>Average-case time: both approxO(n \, log n)(Quicksortistypicallynearthisinpractice).</p></li><li><p>Bestcasetime:both(Quicksort is typically near this in practice).</p></li><li><p>Best-case time: bothO(n \, log n)(fortypicalwellbehavedinputs).</p></li><li><p>Spacecomplexity:MergeSortuses(for typical well-behaved inputs).</p></li><li><p>Space complexity: Merge Sort usesO(n)auxiliaryspace;Quicksortusesauxiliary space; Quicksort usesO(\,log n)stackspaceonaverage.</p></li><li><p>Stability:MergeSortisstable;Quicksortisnotstableunlessmodified.</p></li><li><p>Inplace:MergeSortisnotinplacebydefault;Quicksortisinplace(intypicalimplementations).</p></li><li><p>Usecases:MergeSortforlargedatasetsandlinkedlists;Quicksortforinmemoryarraysandgeneralpurposesortinginlibraries.</p></li><li><p>Libraryusageandimplementationconsiderations:oftenusedinparallel/distributedcontexts(Merge)versusinmemory,generalpurposesorting(Quicksort).</p></li></ul><h3id="57c8a4284825483d90dd380a43825c06"datatocid="57c8a4284825483d90dd380a43825c06"collapsed="false"seolevelmigrated="true">RadixSort</h3><ul><li><p>RadixSortprocessesdigitsfromleastsignificanttomostsignificant(LSDfirst).</p><ul><li><p><em>(LSDtheleastsignificantdigitisthenumberfarthesttotheright)</em></p></li><li><p><em>(MSDthemostsignificantdigitisthenumberfarthesttotheleft)</em></p></li></ul></li><li><p>Stablesubsortingperdigitplace(e.g.,countingsort)isusedtopreservetheorderofdigitsalreadysorted.</p></li><li><p>Doesnotrelyoncomparisonsbetweenkeys;reliesondigitbucketing.</p></li><li><p>Bestforuniform,fixedlengthkeys(e.g.,largelistsofintegersorstringswithconsistentkeylength).</p></li><li><p>Keycomponents:</p><ul><li><p>RadixGetLength(value):returnsnumberofdigitsinvalue(atleast1for0).</p></li><li><p>RadixGetMaxLength(array):maximumnumberofdigitsamongallelements.</p></li><li><p>RadixSort(array):uses10buckets(forbase10)anditeratesoverdigitpositionsfromleasttomostsignificant,rebuildingthearrayaftereachpass.Alsohandlesnegativenumbersbyseparatingnegativesandnonnegatives,reversingnegatives,andconcatenating.</p></li></ul></li><li><p>Highlevelpseudocode(outline):</p><ul><li><p>Buckets=array[0..9]ofemptylists</p></li><li><p>maxDigits=RadixGetMaxLength(array)</p></li><li><p>pow10=1</p></li><li><p>fordigitIndex=0tomaxDigits1:</p></li><li><p>fori=0toarray.size1:</p><ul><li><p>bucketIndex=abs(array[i]/pow10)stack space on average.</p></li><li><p>Stability: Merge Sort is stable; Quicksort is not stable unless modified.</p></li><li><p>In-place: Merge Sort is not in-place by default; Quicksort is in-place (in typical implementations).</p></li><li><p>Use cases: Merge Sort for large datasets and linked lists; Quicksort for in-memory arrays and general-purpose sorting in libraries.</p></li><li><p>Library usage and implementation considerations: often used in parallel/distributed contexts (Merge) versus in-memory, general-purpose sorting (Quicksort).</p></li></ul><h3 id="57c8a428-4825-483d-90dd-380a43825c06" data-toc-id="57c8a428-4825-483d-90dd-380a43825c06" collapsed="false" seolevelmigrated="true">Radix Sort</h3><ul><li><p>Radix Sort processes digits from least significant to most significant (LSD first).</p><ul><li><p><em>(LSD the least significant digit is the number farthest to the right)</em></p></li><li><p><em>(MSD the most significant digit is the number farthest to the left)</em></p></li></ul></li><li><p>Stable sub-sorting per digit place (e.g., counting sort) is used to preserve the order of digits already sorted.</p></li><li><p>Does not rely on comparisons between keys; relies on digit bucketing.</p></li><li><p>Best for uniform, fixed-length keys (e.g., large lists of integers or strings with consistent key length).</p></li><li><p>Key components:</p><ul><li><p>RadixGetLength(value): returns number of digits in value (at least 1 for 0).</p></li><li><p>RadixGetMaxLength(array): maximum number of digits among all elements.</p></li><li><p>RadixSort(array): uses 10 buckets (for base-10) and iterates over digit positions from least to most significant, rebuilding the array after each pass. Also handles negative numbers by separating negatives and nonnegatives, reversing negatives, and concatenating.</p></li></ul></li><li><p>High-level pseudocode (outline):</p><ul><li><p>Buckets = array[0..9] of empty lists</p></li><li><p>maxDigits = RadixGetMaxLength(array)</p></li><li><p>pow10 = 1</p></li><li><p>for digitIndex = 0 to maxDigits - 1:</p></li><li><p>for i = 0 to array.size - 1:</p><ul><li><p>bucketIndex = abs(array[i] / pow10) % 10</p></li><li><p>Append array[i] to buckets[bucketIndex]</p></li></ul></li><li><p>arrayIndex = 0</p></li><li><p>for i = 0 to 9:</p><ul><li><p>for j = 0 to buckets[i].size - 1:</p></li><li><p>array[arrayIndex] = buckets[i][j]</p></li><li><p>arrayIndex = arrayIndex + 1</p></li></ul></li><li><p>pow10 = pow10 * 10</p></li><li><p>clear all buckets</p></li><li><p>Handle negatives: negatives = all negative values; nonnegatives = all nonnegative values; reverse negatives; concatenate negatives + nonnegatives.</p></li></ul></li><li><p>Runtime intuition:</p><ul><li><p>Step 1: number of passes = d, where d is the number of digits in the largest number.</p></li><li><p>Step 2: cost per pass ~ O(n) (scan elements, place into buckets, then flatten buckets).</p></li><li><p>Step 3: total cost ~ O(d × n).</p></li><li><p>For base 10, d = O(log10(k)) where k is the largest value; hence overall complexity ~O(n \log_{10}(k)).</p></li></ul></li><li><p>Practicaltakeaway:RadixSortcanoutperformcomparisonbasedsortsonlargedatasetswithfixedlengthkeysbutrequiresextraspaceandcarefulhandlingofnegatives.</p></li></ul><h3id="5328dd2fa0164cc5a349ac7a71704d37"datatocid="5328dd2fa0164cc5a349ac7a71704d37"collapsed="false"seolevelmigrated="true">SortingAlgorithmComparisonChart(summarypoints)</h3><ul><li><p>BubbleSort:Best.</p></li></ul></li><li><p>Practical takeaway: Radix Sort can outperform comparison-based sorts on large datasets with fixed-length keys but requires extra space and careful handling of negatives.</p></li></ul><h3 id="5328dd2f-a016-4cc5-a349-ac7a71704d37" data-toc-id="5328dd2f-a016-4cc5-a349-ac7a71704d37" collapsed="false" seolevelmigrated="true">Sorting Algorithm Comparison Chart (summary points)</h3><ul><li><p>Bubble Sort: BestO(n);Average; AverageO(n^2);Worst; WorstO(n^2);Stable:Yes;Inplace:Yes.Notes:educational;notusedinpractice.</p></li><li><p>SelectionSort:Best; Stable: Yes; In-place: Yes. Notes: educational; not used in practice.</p></li><li><p>Selection Sort: BestO(n^2);Average; AverageO(n^2);Worst; WorstO(n^2);Stable:No;Inplace:Yes.Notes:alwaysdoesn2comparisons.</p></li><li><p>InsertionSort:Best; Stable: No; In-place: Yes. Notes: always does n^2 comparisons.</p></li><li><p>Insertion Sort: BestO(n);Average; AverageO(n^2);Worst; WorstO(n^2);Stable:Yes;Inplace:Yes.Notes:greatfornearlysorteddata;conceptuallyabridgetoMergeSort.</p></li><li><p>ShellSort:Best; Stable: Yes; In-place: Yes. Notes: great for nearly-sorted data; conceptually a bridge to Merge Sort.</p></li><li><p>Shell Sort: BestO(n \, ext{log} n)^* roughly~ roughlyO(n^{1.3});Average; AverageO(n^2);Stable:No;Inplace:Yes.Notes:fasterthaninsertionbutnotascommonlyused;notstable.</p></li><li><p>MergeSort:Best/Average/Worst; Stable: No; In-place: Yes. Notes: faster than insertion but not as commonly used; not stable.</p></li><li><p>Merge Sort: Best/Average/WorstO(n \, log n);Stable:Yes;Inplace:No.Notes:recursive;requires; Stable: Yes; In-place: No. Notes: recursive; requiresO(n)space;excellentperformance.</p></li><li><p>QuickSort:Best/Averagespace; excellent performance.</p></li><li><p>Quick Sort: Best/AverageO(n \, log n);Worst; WorstO(n^2);Stable:No;Inplace:Yes.Notes:veryfastinpractice;pivotstrategyiskey;notstablebydefault.</p></li><li><p>HeapSort:Best/Average/Worst; Stable: No; In-place: Yes. Notes: very fast in practice; pivot strategy is key; not stable by default.</p></li><li><p>Heap Sort: Best/Average/WorstO(n \, log n); Stable: No; In-place: Yes. Notes: based on heap; good performance but not stable.

      Notes on Master Theorem and Recurrence Relations

      • Recurrence form for divide-and-conquer algorithms:

        • T(n) = a · T(n/b) + f(n)

        • a: number of subproblems

        • b: factor by which the problem size is reduced

        • f(n): time to combine the sub-solutions (the work outside recursive calls)

      • Master Theorem applicability:

        • Used to solve recurrences of the form T(n) = a T(n/b) + f(n).

        • Compute n^{log_b a} and compare with f(n) to determine the case.

      • The three classical cases (informal):

        • Case 1: f(n) = O(n^{log_b a - ε}) for some ε > 0

        • Then T(n) = Θ(n^{log_b a})

        • Case 2: f(n) = Θ(n^{log_b a} log^k n) for some k ≥ 0

        • Then T(n) = Θ(n^{log_b a} log^{k+1} n)

        • Case 3: f(n) = Ω(n^{log_b a + ε}) for some ε > 0, and regularity condition holds (often written as a f(n/b) ≤ c f(n) for some c < 1 and sufficiently large n)

        • Then T(n) = Θ(f(n))

      • How to apply Master Theorem (workflow):

        • Identify a, b, f(n) in the recurrence.

        • Compute n^{log_b a}.

        • Compare f(n) with n^{log_b a} (and any log factors) to select the correct case.

        • Conclude T(n) from the case.

      • Classic examples:

        • Merge Sort: T(n) = 2 T(n/2) + Θ(n) → a = 2, b = 2, f(n) = Θ(n) → Case 2 → T(n) = Θ(n log n).

        • Binary Search: T(n) = T(n/2) + Θ(1) → a = 1, b = 2, f(n) = Θ(1) → Case 2 → T(n) = Θ(log n).

      Recurrence Examples and Master Theorem Flow

      • Master Theorem Flowchart (conceptual):

        • If f(n) = O(n^{logb a - ε}) for some ε > 0 → Case 1 → T(n) = Θ(n^{logb a})

        • Else if f(n) = Θ(n^{logb a} log^k n) → Case 2 → T(n) = Θ(n^{logb a} log^{k+1} n)

        • Else if f(n) = Ω(n^{log_b a + ε}) and regularity condition holds → Case 3 → T(n) = Θ(f(n))

        • Else Master Theorem does not apply

      Master Theorem: Worked Case Examples (conceptual)

      • Case 1 example outline: T(n) = 2 T(n/2) + Θ(1)

        • a = 2, b = 2, f(n) = Θ(1) → n^{logb a} = n^{log2 2} = n^1 = n

        • Since f(n) = O(n^{1 - ε}) with ε = 1, Case 1 applies? (Note: the standard interpretation yields T(n) = Θ(n), but the slide framing is context-dependent; the key takeaway is using the comparison with n^{log_b a}.)

      • Case 2 example outline: T(n) = 4 T(n/2) + Θ(n^2)

        • a = 4, b = 2, f(n) = Θ(n^2) → n^{logb a} = n^{log2 4} = n^{2}

        • f(n) = Θ(n^{logb a}) → Case 2 with k = 0 → T(n) = Θ(n^{logb a} log n) = Θ(n^2 log n) (as per the general form). (The slide uses a variant; the essential point is that equality with a log factor escalates to Case 2 in the Master Theorem framework.)

      • Case 3 example outline: T(n) = 2 T(n/2) + Θ(n^2)

        • a = 2, b = 2, f(n) = Θ(n^2) → n^{log_b a} = n

        • Since f(n) grows faster than n^{log_b a}, Case 3 applies given the regularity condition, yielding T(n) = Θ(f(n)) = Θ(n^2).

      • Final notes: The Master Theorem provides a structured way to deduce asymptotic growth for divide-and-conquer recurrences, with Merge Sort and Binary Search as canonical examples.

      Practical Takeaways

      • When choosing a sorting algorithm, consider:

        • Input size and characteristics (random, nearly sorted, linked lists, in-memory arrays).

        • Stability requirements.

        • Memory constraints (in-place vs extra space).

        • Worst-case vs average-case performance.

      • Radix Sort offers a non-comparison-based alternative with predictable linear passes in certain contexts, particularly with fixed-length keys, but requires extra space and handling of signs. Overall complexity scales with the number of digits of the largest key: O(n \, d) = O(n \, ext{log}_{b}(k))forbasefor basebandlargestkeyand largest keyk.</p></li></ul><h3id="d68b5da7e0e043fa9496aea8e3c6e566"datatocid="d68b5da7e0e043fa9496aea8e3c6e566"collapsed="false"seolevelmigrated="true">QuickReference:KeyNotation</h3><ul><li><p>Timecomplexities:.</p></li></ul><h3 id="d68b5da7-e0e0-43fa-9496-aea8e3c6e566" data-toc-id="d68b5da7-e0e0-43fa-9496-aea8e3c6e566" collapsed="false" seolevelmigrated="true">Quick Reference: Key Notation</h3><ul><li><p>Time complexities:O(\,),,
        abla $$ (log base 2) denotation in context, and variations by base as needed.

      • Recurrences: T(n) = a T(n/b) + f(n)

      • Master Theorem cases summarized above.