Facebook Pixel

744. Find Smallest Letter Greater Than Target

Problem Description

You have an array of characters letters that is sorted in non-decreasing order (alphabetically), and you're given a character target. The array contains at least two different characters.

Your task is to find the smallest character in letters that comes after target in alphabetical order (lexicographically greater). If no such character exists in the array, you should wrap around and return the first character in letters.

For example:

  • If letters = ['c', 'f', 'j'] and target = 'a', the answer is 'c' because it's the smallest character greater than 'a'.
  • If letters = ['c', 'f', 'j'] and target = 'd', the answer is 'f' because it's the smallest character greater than 'd'.
  • If letters = ['c', 'f', 'j'] and target = 'j', the answer is 'c' because no character in the array is greater than 'j', so we wrap around to the beginning.

The solution uses binary search with bisect_right to efficiently find the insertion point where target would go in the sorted array. The function returns the index of the first element greater than target. By taking this index modulo the array length (i % len(letters)), we handle the wrap-around case when no greater character exists.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since the array is already sorted, we can leverage this property to find our answer efficiently. Think of it like looking up a word in a dictionary - you don't need to check every single word; you can jump to approximately where you expect the word to be and then narrow down your search.

The key insight is that we need to find the first character that is strictly greater than our target. In a sorted array, all characters greater than target will be grouped together on the right side, and all characters less than or equal to target will be on the left side. This naturally forms a boundary that we can search for.

Binary search is perfect for this because it repeatedly divides the search space in half. We're essentially asking: "Where would target be inserted if we wanted to maintain the sorted order?" The answer to this question gives us the index of the first character greater than target.

The clever part about using bisect_right is that it finds the rightmost position where target could be inserted. This means it automatically gives us the index of the first element that is strictly greater than target. If target is 'd' and we have ['a', 'c', 'f', 'j'], bisect_right would return index 2 (pointing to 'f').

The wrap-around behavior is elegantly handled with the modulo operator (%). If all characters in the array are less than or equal to target, bisect_right returns the length of the array. Taking this modulo the array length gives us 0, which correctly points back to the first character - exactly what the problem asks for when no greater character exists.

Learn more about Binary Search patterns.

Solution Approach

The solution uses binary search to find the smallest character that is larger than target. Let's walk through the implementation step by step.

We define the binary search boundaries with l = 0 as the left boundary and r = n as the right boundary, where n is the length of the array. The right boundary is set to n (not n-1) because we want to handle the case where all elements are less than or equal to target.

During each iteration of binary search, we calculate the middle position as mid = (l + r) / 2. We then compare letters[mid] with target:

  • If letters[mid] > target, we've found a character greater than target, but we need to check if there's an even smaller valid character in the left half. So we set r = mid to continue searching in the left portion.

  • If letters[mid] <= target, the character at mid is not what we want, so we need to search in the right half. We set l = mid + 1 to exclude the current element and everything to its left.

The binary search continues until l equals r, at which point l will be pointing to:

  • The index of the smallest character greater than target, if such a character exists
  • The value n (length of the array), if all characters are less than or equal to target

Finally, we return letters[l % n]. The modulo operation handles the wrap-around case beautifully:

  • If l < n, then l % n = l, and we return the character at index l
  • If l = n (meaning no character is greater than target), then l % n = 0, and we return the first character in the array

The Python implementation uses bisect_right which internally performs this exact binary search logic. It finds the rightmost position where target could be inserted while maintaining sorted order, which is equivalent to finding the first element strictly greater than target. The key=lambda c: ord(c) ensures we're comparing the ASCII values of characters for proper lexicographic comparison.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through an example with letters = ['c', 'f', 'j'] and target = 'd'.

Step 1: Initialize boundaries

  • Array: ['c', 'f', 'j']
  • n = 3 (length of array)
  • l = 0 (left boundary)
  • r = 3 (right boundary - note it's the length, not the last index)

Step 2: First iteration of binary search

  • Calculate middle: mid = (0 + 3) // 2 = 1
  • Check letters[1] which is 'f'
  • Compare: 'f' > 'd'? Yes!
  • Since we found a character greater than target, but need to check if there's a smaller valid one, set r = mid = 1
  • New boundaries: l = 0, r = 1

Step 3: Second iteration

  • Calculate middle: mid = (0 + 1) // 2 = 0
  • Check letters[0] which is 'c'
  • Compare: 'c' > 'd'? No!
  • Since 'c' <= 'd', we need to search right, set l = mid + 1 = 1
  • New boundaries: l = 1, r = 1

Step 4: Search complete

  • l == r, so binary search ends
  • l = 1 points to index 1

Step 5: Return result

  • Calculate l % n = 1 % 3 = 1
  • Return letters[1] which is 'f'

The answer is 'f', which is correct as it's the smallest character greater than 'd'.

Wrap-around example: If target = 'j' with the same array ['c', 'f', 'j']:

  • Binary search would end with l = 3 (pointing past the array)
  • Calculate l % n = 3 % 3 = 0
  • Return letters[0] which is 'c'

This demonstrates how the modulo operation elegantly handles the wrap-around case when no character is greater than the target.

Solution Implementation

1class Solution:
2    def nextGreatestLetter(self, letters: List[str], target: str) -> str:
3        """
4        Find the smallest character in letters that is lexicographically greater than target.
5        If no such character exists, return the first character in letters (wrap around).
6      
7        Args:
8            letters: A sorted list of characters in non-decreasing order
9            target: The target character to find the next greatest letter for
10      
11        Returns:
12            The smallest character greater than target, or the first character if none exists
13        """
14        # Use binary search to find the insertion point for target
15        # bisect_right returns the rightmost position where target can be inserted
16        # to maintain sorted order
17        insertion_index = bisect_right(letters, target)
18      
19        # Use modulo to handle wrap-around case:
20        # - If insertion_index < len(letters): return letters[insertion_index]
21        # - If insertion_index == len(letters): wrap around to letters[0]
22        return letters[insertion_index % len(letters)]
23
1class Solution {
2    public char nextGreatestLetter(char[] letters, char target) {
3        // Search for the position where (target + 1) would be inserted
4        // This effectively finds the smallest character greater than target
5        int insertionPoint = Arrays.binarySearch(letters, (char) (target + 1));
6      
7        // If binarySearch returns negative, convert to actual insertion index
8        // If positive, the exact match was found, use that index
9        if (insertionPoint < 0) {
10            insertionPoint = -insertionPoint - 1;
11        }
12      
13        // Use modulo to wrap around to the beginning if we've reached the end
14        // This handles the case where target is greater than or equal to all letters
15        return letters[insertionPoint % letters.length];
16    }
17}
18
1class Solution {
2public:
3    char nextGreatestLetter(vector<char>& letters, char target) {
4        // Find the first element greater than target using binary search
5        // upper_bound returns an iterator to the first element > target
6        int index = upper_bound(letters.begin(), letters.end(), target) - letters.begin();
7      
8        // If index equals array size, all elements are <= target
9        // Use modulo to wrap around to the first element (circular array logic)
10        return letters[index % letters.size()];
11    }
12};
13
1/**
2 * Finds the smallest character in the sorted array that is lexicographically greater than the target.
3 * If no such character exists, returns the first character in the array (wraps around).
4 * 
5 * @param letters - A sorted array of lowercase letters in non-decreasing order
6 * @param target - The target character to find the next greatest letter for
7 * @returns The smallest character greater than target, or the first character if none exists
8 */
9function nextGreatestLetter(letters: string[], target: string): string {
10    // Initialize binary search boundaries
11    // left points to the start, right points to one past the end
12    let left: number = 0;
13    let right: number = letters.length;
14  
15    // Binary search to find the leftmost position where letters[position] > target
16    while (left < right) {
17        // Calculate middle index using bit shift for integer division
18        const middle: number = (left + right) >> 1;
19      
20        // If the middle character is greater than target, 
21        // the answer is at middle or to its left
22        if (letters[middle] > target) {
23            right = middle;
24        } else {
25            // If the middle character is less than or equal to target,
26            // the answer must be to the right of middle
27            left = middle + 1;
28        }
29    }
30  
31    // Use modulo to wrap around to the first character if left equals array length
32    // This handles the case where all characters are less than or equal to target
33    return letters[left % letters.length];
34}
35

Time and Space Complexity

The time complexity is O(log n), where n is the length of the letters array. This is because bisect_right performs a binary search on the sorted array, which divides the search space in half with each iteration.

The space complexity is O(1). The algorithm only uses a constant amount of extra space to store the index i and perform the modulo operation. The key parameter in bisect_right creates a lambda function, but this doesn't scale with the input size and is considered constant space.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting the Wrap-Around Case

One of the most common mistakes is forgetting to handle the case where all characters in the array are less than or equal to the target. Without proper handling, this would result in an index out of bounds error.

Incorrect approach:

def nextGreatestLetter(self, letters: List[str], target: str) -> str:
    insertion_index = bisect_right(letters, target)
    return letters[insertion_index]  # IndexError when insertion_index == len(letters)

Solution: Always use modulo operation to wrap around:

return letters[insertion_index % len(letters)]

2. Using bisect_left Instead of bisect_right

Another pitfall is using bisect_left instead of bisect_right. This causes incorrect results when the target character exists in the array.

Example of the issue:

# If letters = ['a', 'c', 'c', 'f'] and target = 'c'
# bisect_left would return index 1 (first 'c')
# letters[1 % 4] = 'c' (wrong - returns the same character)
# bisect_right would return index 3 (after last 'c')  
# letters[3 % 4] = 'f' (correct - returns next greater character)

Solution: Always use bisect_right to find the position after all occurrences of the target.

3. Manual Binary Search Off-by-One Errors

When implementing binary search manually, it's easy to make off-by-one errors with the boundaries.

Common mistakes:

# Wrong: Setting right boundary to n-1
l, r = 0, len(letters) - 1
# This fails to handle the case where all elements <= target

# Wrong: Incorrect update logic
if letters[mid] > target:
    r = mid - 1  # Should be r = mid to keep the valid answer in range

Solution: Set the right boundary to n (not n-1) and use r = mid when letters[mid] > target:

l, r = 0, len(letters)
while l < r:
    mid = (l + r) // 2
    if letters[mid] > target:
        r = mid  # Keep mid as potential answer
    else:
        l = mid + 1
return letters[l % len(letters)]

4. Not Understanding the Problem's Definition of "Next Greatest"

Some might misinterpret the problem and think they need to find any character greater than target, not necessarily the smallest one.

Example: If letters = ['a', 'c', 'f', 'j'] and target = 'b', returning 'f' or 'j' would be wrong. The correct answer is 'c' as it's the smallest character greater than 'b'.

Solution: Always remember you're looking for the smallest character that is strictly greater than the target, not just any greater character.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More