← cd ../algorithms
LeetCode 33 · Search in Rotated Sorted Array

Searching a rotated sorted array by finding the pivot first

The problem splits into finding the pivot and then running an ordinary binary search. The two regions on either side of the pivot give the rules for finding it in O(log n) time.

Binary searchRotated arraysBreaking a problem down

$ cat the-problem.mdThe problem

We are given an array nums of distinct integers. It was sorted in ascending order and then possibly rotated: some of its numbers were moved from the front to the back, so that [0, 1, 2, 4, 5, 6, 7] might become [4, 5, 6, 7, 0, 1, 2]. Given a target, we have to return its index in nums, or −1-1 if it is not there, in O(log⁡n)O(\log n) time.

Example 1

Example 2

Example 3

$ cat introduction.mdIntroduction

This problem can be broken down into three distinct parts:

  1. Determine whether the array is Rotated array A sorted array in which some numbers were moved from the front to the back, keeping their order. 1, 2, 3, 4, 5 rotated by two places is 3, 4, 5, 1, 2.. If it is rotated, find the Pivot In this article, the index of the smallest number in a rotated array. It is the one place where a number is smaller than the number before it., pp.
  2. If the array is not rotated, simply perform a Binary search A way of finding a value in a sorted array: look at the middle element, then keep only the half that can still hold the value, and repeat. on the array and return the result.
  3. If the array is rotated, first perform a binary search on the left part, which is the indices [0,p−1][0, p-1]. If the target is in that part, return the result. If it is not, search the right part, which is the indices [p,n−1][p, n-1], where nn is the length of nums.

From the above, we can see that once we have determined whether the array is rotated and where its pivot is, we enter one of two branches. The first branch performs a single binary search, so its time complexity is Logarithmic time The number of steps grows with the number of times the array can be halved. Doubling the array adds only one step. Written O(log n).. The second branch performs at most two binary searches, one after the other, so its time complexity is O(log⁡n)+O(log⁡n)=O(log⁡n)O(\log n) + O(\log n) = O(\log n).

For the first part, note that if we can locate a pivot in the array, then the array must be rotated. So we do not need separate routines for determining whether the array is rotated and for finding the pivot. A single routine that finds the pivot does both.

The problem requires our solution to have a time complexity of O(log⁡n)O(\log n), and we know the other parts of our solution have that time complexity. So we need to design our procedure for finding the pivot so that it also runs in O(log⁡n)O(\log n) time.

$ cat approach.mdApproach

We will adapt binary search to find the pivot of a rotated array. If the array is rotated, then the pivot p>0p > 0. Furthermore, if pp exists in an array AA, then:

A=a0,a1,…,ap−1,ap,ap+1,…,an−1A = a_0, a_1, \dots, a_{p-1}, a_p, a_{p+1}, \dots, a_{n-1}

So AA can be divided into two regions defined by the pivot:

R0=A[0:p]R1=A[p:n]R_0 = A[0:p] \qquad R_1 = A[p:n]

This uses Python’s Slice notation Python's way of naming part of a list. A[i:j] is the elements from index i up to, but not including, index j., where the left index is included and the right index is excluded.

Therefore:

Both follow from the rotation. R0R_0 is sorted and begins with a0a_0, so nothing in it is smaller than a0a_0. Every element of R1R_1 stood in front of a0a_0 before the rotation, so it is smaller than a0a_0.

$ cat deducing-the-rules-for-searching-the-array.mdDeducing the rules for searching the array

Once we note that, we can deduce our rules for searching the array:

  1. Rule 1: If we have found an element ai∈R0a_i \in R_0, the pivot lies to its right, so we need to continue our search to the right of that element. That is, we must set low = mid + 1.
  2. Rule 2: Similarly, if we have found an element ai∈R1a_i \in R_1 that is not the pivot, the pivot lies to its left, so we need to continue our search to the left of that element. That is, we must set high = mid - 1.

$ cat defining-the-pivot.mdDefining the pivot

Our final task is to define exactly how we know when we have found the pivot. We can define the pivot pp precisely by noting that it is the first element of R1R_1.

Therefore, one condition for the pivot element apa_p is ap∈R1a_p \in R_1, which means ap<a0a_p < a_0. Since apa_p is the first element of R1R_1, the element before it is in R0R_0, which means ap−1≥a0a_{p-1} \ge a_0. Therefore, the second condition for the pivot is ap−1>apa_{p-1} > a_p. No other element of R1R_1 meets it: the element before any other one is also in R1R_1, which is sorted, so it is smaller.

This gives us our condition for determining whether an element is the pivot:

ap−1>apanda0>apa_{p-1} > a_p \quad \text{and} \quad a_0 > a_p

$ cat putting-it-together.mdPutting it together

Once we have identified what uniquely defines the pivot and determined the rules for searching the array, we have all the pieces we need to adapt binary search.

def find_pivot(nums):
    low = 0
    high = len(nums) - 1

    while low <= high:
        mid = low + (high - low) // 2
        # Is this the pivot?
        if nums[mid] < nums[0] and nums[mid - 1] > nums[mid]:
            return mid
        elif nums[mid] < nums[0]:
            # Rule 2: we are in region one, but have not found the pivot
            high = mid - 1
        else:
            # Rule 1: we must be in region zero
            low = mid + 1

    # We did not find a pivot, therefore the array is not rotated.
    # The de facto pivot is then index 0.
    return 0

When mid is 0, the first half of the pivot test, nums[mid] < nums[0], is false, so nums[mid - 1] is never read there.

$ cat try-it-yourself.mdTry it yourself

The demonstration starts with the array and target from Example 1. Each number in the array is drawn as a bar, with a line at the height of nums[0]. The bars that reach the line are in R0R_0 and the bars below it are in R1R_1. Press Step to watch the search find the pivot and then look for the target. Rotate the array by a different amount, including 0, or change the target, and step through again.

This demonstration needs JavaScript. With it turned off, the rules above describe each step.

$ cat complexity.mdComplexity

$ cat code.mdCode

class Solution:
    def search(self, nums: list[int], target: int) -> int:
        # find the pivot
        p = self.find_pivot(nums)
        if p == 0:
            return self.binary_search(nums, target, 0, len(nums) - 1)

        found = self.binary_search(nums, target, 0, p - 1)
        if found != -1:
            return found

        return self.binary_search(nums, target, p, len(nums) - 1)

    def find_pivot(self, nums):
        low = 0
        high = len(nums) - 1

        while low <= high:
            mid = low + (high - low) // 2
            if nums[mid] < nums[0] and nums[mid - 1] > nums[mid]:
                return mid
            elif nums[mid] < nums[0]:
                high = mid - 1
            else:
                low = mid + 1
        return 0

    def binary_search(self, nums, target, low, high):
        while low <= high:
            mid = low + (high - low) // 2
            if nums[mid] == target:
                return mid
            elif target < nums[mid]:
                high = mid - 1
            else:
                low = mid + 1
        return -1