โ† Back to DevBytes

Solving Search in Rotated Array in Go: Step-by-Step Guide

Introduction to Search in Rotated Sorted Array

The "Search in Rotated Sorted Array" problem is a classic algorithmic challenge that frequently appears in coding interviews and real-world applications. A rotated sorted array is an array that was originally sorted in ascending order but has been rotated at some pivot point. For example, [0, 1, 2, 3, 4, 5, 6, 7] rotated at pivot index 3 becomes [4, 5, 6, 7, 0, 1, 2, 3].

The goal is to find the index of a target value in this rotated array efficiently. While a linear scan would solve this in O(n) time, the optimal solution leverages a modified binary search to achieve O(log n) time complexity. This tutorial walks through the problem in Go, explaining the logic, implementation, and best practices.

Why This Problem Matters

Understanding how to search a rotated sorted array sharpens your ability to reason about binary search variants. Binary search is one of the most powerful algorithmic tools, but its variations require careful handling of boundary conditions. Mastering this problem teaches you how to:

In practice, rotated arrays appear in scenarios like circular buffers, log files that wrap around, and load-balanced ring data structures. Efficient searching in these structures is essential for performance-critical systems.

Understanding the Problem

Given an array nums that is sorted in ascending order and rotated at an unknown pivot, and a target value target, return the index of target if it exists in the array, or -1 if it does not. The algorithm must run in O(log n) time.

For example:

Input: nums = [4, 5, 6, 7, 0, 1, 2], target = 0
Output: 4

Input: nums = [4, 5, 6, 7, 0, 1, 2], target = 3
Output: -1

The Core Insight: Modified Binary Search

The key observation is that when you split a rotated sorted array at any midpoint, at least one of the two halves will always be sorted. You can determine which half is sorted by comparing the middle element with the leftmost element:

Once you know which half is sorted, you can check whether the target lies within that sorted half's range. If it does, search that half; otherwise, search the other half. This decision process allows you to discard half of the remaining elements at each step, preserving the logarithmic complexity of binary search.

Step-by-Step Implementation in Go

Let's build the solution incrementally. First, we set up the standard binary search loop with two pointers, left and right.

package main

import "fmt"

func search(nums []int, target int) int {
    left, right := 0, len(nums)-1

    for left <= right {
        mid := left + (right-left)/2

        if nums[mid] == target {
            return mid
        }

        // Determine which half is sorted
        if nums[left] <= nums[mid] {
            // Left half is sorted
            if nums[left] <= target && target < nums[mid] {
                right = mid - 1
            } else {
                left = mid + 1
            }
        } else {
            // Right half is sorted
            if nums[mid] < target && target <= nums[right] {
                left = mid + 1
            } else {
                right = mid - 1
            }
        }
    }

    return -1
}

func main() {
    nums := []int{4, 5, 6, 7, 0, 1, 2}
    fmt.Println(search(nums, 0)) // Output: 4
    fmt.Println(search(nums, 3)) // Output: -1
}

Breaking Down the Code

The loop continues while left <= right, ensuring we check single-element ranges. We compute mid using left + (right-left)/2 instead of (left+right)/2 to avoid integer overflow, a best practice in languages where overflow is possible.

When nums[mid] equals the target, we return mid immediately. Otherwise, we determine which half is sorted. The condition nums[left] <= nums[mid] uses <= rather than < to handle the case where left and mid point to the same element, which happens when the search range has only one or two elements.

If the left half is sorted, we check whether the target falls within [nums[left], nums[mid]). If so, we narrow the search to the left half; otherwise, we search the right half. The same logic applies symmetrically when the right half is sorted.

Handling Edge Cases

Several edge cases deserve attention when implementing this algorithm:

Let's verify these cases with a test function:

func testEdgeCases() {
    cases := []struct {
        nums   []int
        target int
        want   int
    }{
        {[]int{}, 5, -1},
        {[]int{1}, 1, 0},
        {[]int{1}, 0, -1},
        {[]int{1, 2, 3, 4, 5}, 3, 2},
        {[]int{5, 1, 2, 3, 4}, 5, 0},
        {[]int{3, 4, 5, 1, 2}, 2, 4},
        {[]int{2, 3, 4, 5, 1}, 1, 4},
    }

    for _, c := range cases {
        got := search(c.nums, c.target)
        status := "PASS"
        if got != c.want {
            status = "FAIL"
        }
        fmt.Printf("%s: nums=%v target=%d got=%d want=%d\n",
            status, c.nums, c.target, got, c.want)
    }
}

Variant: Searching with Duplicates

A harder variant allows duplicate values in the array, for example [2, 5, 6, 0, 0, 1, 2]. The challenge is that when nums[left] == nums[mid] == nums[right], you cannot determine which half is sorted. The solution is to shrink the search range by incrementing left and decrementing right until the ambiguity resolves. This degrades worst-case time to O(n) but remains efficient on average.

func searchWithDuplicates(nums []int, target int) bool {
    left, right := 0, len(nums)-1

    for left <= right {
        mid := left + (right-left)/2

        if nums[mid] == target {
            return true
        }

        // Handle ambiguity when left, mid, and right are equal
        if nums[left] == nums[mid] && nums[mid] == nums[right] {
            left++
            right--
        } else if nums[left] <= nums[mid] {
            if nums[left] <= target && target < nums[mid] {
                right = mid - 1
            } else {
                left = mid + 1
            }
        } else {
            if nums[mid] < target && target <= nums[right] {
                left = mid + 1
            } else {
                right = mid - 1
            }
        }
    }

    return false
}

Notice that this variant returns a boolean rather than an index, because duplicates make the index ambiguous and the problem typically asks only whether the target exists.

Best Practices

When implementing search in a rotated sorted array, keep these best practices in mind:

Performance Analysis

The standard algorithm without duplicates runs in O(log n) time because each iteration discards half of the remaining elements. Space complexity is O(1) since we only use a few integer variables. The duplicate-handling variant has a worst-case time of O(n) when all elements are identical and the target is absent, but it remains O(log n) on average for typical inputs.

For comparison, a naive linear scan would always take O(n) time. On an array of one million elements, binary search performs roughly 20 comparisons, while linear search averages 500,000. This difference is significant in performance-sensitive applications.

Conclusion

Searching a rotated sorted array is a deceptively simple problem that tests your understanding of binary search and boundary conditions. By identifying which half of the array is sorted at each step and using that knowledge to discard the irrelevant half, you achieve efficient O(log n) lookup. The Go implementation is concise, but the logic requires careful attention to inclusive bounds, overflow-safe midpoint calculation, and edge cases like empty or single-element arrays. With the foundation covered here, including the duplicate-handling variant, you are well-equipped to tackle this problem in interviews and apply the same reasoning to other binary search derivatives in real-world Go programs.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles