From 03f41830e06903b470ba935f251f579d09d54da4 Mon Sep 17 00:00:00 2001 From: beam Date: Fri, 30 Oct 2026 02:59:34 +0000 Subject: [PATCH] beam: add rotated binary search with tests --- wizard-work/beam/rotated_binary_search.py | 59 +++++++++++++++++++ .../beam/test_rotated_binary_search.py | 56 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 wizard-work/beam/rotated_binary_search.py create mode 100644 wizard-work/beam/test_rotated_binary_search.py diff --git a/wizard-work/beam/rotated_binary_search.py b/wizard-work/beam/rotated_binary_search.py new file mode 100644 index 0000000..6b7c097 --- /dev/null +++ b/wizard-work/beam/rotated_binary_search.py @@ -0,0 +1,59 @@ +"""Search in a rotated sorted array (possibly with duplicates). + +Provides: + - find_pivot(nums): index of smallest element (rotation point), or -1 if not rotated + - search(nums, target): index of target or -1 + - count_rotations(nums): number of rotations (== pivot index) + +Handles empty, single element, duplicates, and arrays that are not rotated. +O(log n) typical; degrades to O(n) only when duplicates force linear scan +(standard limitation of rotated search with duplicates). +""" +from typing import List + + +def find_pivot(nums: List[int]) -> int: + """Return index of the minimum element. -1 if nums is empty.""" + if not nums: + return -1 + lo, hi = 0, len(nums) - 1 + # If already sorted (no rotation), pivot is 0. + if nums[lo] < nums[hi] or len(nums) == 1: + return 0 + while lo < hi: + mid = (lo + hi) // 2 + if nums[mid] > nums[hi]: + lo = mid + 1 + elif nums[mid] < nums[hi]: + hi = mid + else: + # nums[mid] == nums[hi]: cannot decide; shrink hi + hi -= 1 + return lo + + +def count_rotations(nums: List[int]) -> int: + p = find_pivot(nums) + return 0 if p < 0 else p + + +def search(nums: List[int], target: int) -> int: + if not nums: + return -1 + pivot = find_pivot(nums) + n = len(nums) + # Map logical (sorted) index -> physical index. + def phys(i): + return (pivot + i) % n + + lo, hi = 0, n - 1 + while lo <= hi: + mid = (lo + hi) // 2 + val = nums[phys(mid)] + if val == target: + return phys(mid) + if val < target: + lo = mid + 1 + else: + hi = mid - 1 + return -1 diff --git a/wizard-work/beam/test_rotated_binary_search.py b/wizard-work/beam/test_rotated_binary_search.py new file mode 100644 index 0000000..39d6fb3 --- /dev/null +++ b/wizard-work/beam/test_rotated_binary_search.py @@ -0,0 +1,56 @@ +import random +from rotated_binary_search import find_pivot, search, count_rotations + +def test_empty(): + assert find_pivot([]) == -1 + assert search([], 5) == -1 + assert count_rotations([]) == 0 + +def test_single(): + assert find_pivot([7]) == 0 + assert search([7], 7) == 0 + assert search([7], 3) == -1 + +def test_sorted_no_rotation(): + a = [1,2,3,4,5] + assert find_pivot(a) == 0 + for t in a + [0,6]: + idx = search(a, t) + assert (idx == -1) == (t not in a) + if idx != -1: + assert a[idx] == t + +def test_rotated(): + base = [1,2,3,4,5,6,7] + for r in range(len(base)): + arr = base[r:] + base[:r] + assert find_pivot(arr) == r, (r, find_pivot(arr)) + assert count_rotations(arr) == r + for t in base: + i = search(arr, t) + assert i != -1 and arr[i] == t + +def test_duplicates(): + arr = [2,2,2,3,3,1,1,2] + # pivot should point at a minimum (value 1) + p = find_pivot(arr) + assert arr[p] == 1 + assert search(arr, 3) != -1 + assert search(arr, 9) == -1 + +def test_randomized(): + random.seed(7) + for _ in range(2000): + n = random.randint(0, 30) + base = sorted(random.randint(-5,5) for _ in range(n)) + r = random.randrange(n) if n else 0 + arr = base[r:] + base[:r] if n else [] + p = find_pivot(arr) + if n: + assert arr[p] == min(arr) + # search every element + for t in set(base): + i = search(arr, t) + assert i == -1 or arr[i] == t + # search absent + assert search(arr, 10**9) == -1