1. Overview

In this tutorial, we’ll discuss the problem of finding the next smaller element for each element in an array.

First, we’ll define the problem. Then we’ll give an example to explain it.

Finally, we’ll present two different approaches to solving this problem.

2. Defining the Problem

Suppose we have an array A of length N, consisting of N integers, and we’re asked to find the next smaller element for each element in this array. In other words, for each index i, find such index that satisfies A[j] < A[i], j > i, and j is as small as possible.

Let’s take a look at the following example:

NextSmaller

The next smaller element for:

  1. 7 is 3
  2. 3 is 1
  3.  4 is 1
  4. 1 is -1 (there’s no next smaller)
  5. 5 is 3
  6. 3 is -1 (there’s no next smaller)

3. Naive Approach

3.1. Main Idea

The main idea here is that we loop over all the elements in the given array. For each element, we’ll iterate over all the elements that come after it.

Once we find an element with a value less than the current element’s value, we’ll return it as the next smaller element for the current one. If no element has a value less than the current one, we return -1.

3.2. Algorithm

Let’s take a look at the implementation:

algorithm NaiveApproach(Arr):
    // INPUT
    //   Arr = an array of integers
    // OUTPUT
    //   Returns the next smaller element for each element in Arr

    next_smaller <- initialize an array of size N with -1
    for i <- 1 to N:
        for j <- i + 1 to N:
            if Arr[j] < Arr[i]:
                next_smaller[i] <- Arr[j]
                break

    return next_smaller

Firstly, we declare an array next \_ smaller to store the next smaller element for each element.

Secondly, we loop over each element in the given array. For each of them, we then iterate over all the elements that come after it. Once we find one with a value less than the current value, we update the next \_ smaller value for the current element to be equal to the one we found.

Finally, we break out of the loop and don’t get any smaller values than the current one.

3.3. Complexity

The complexity of this algorithm is \boldsymbol{O(N^2)} , where N is the length of the given array. This is because in the worst-case scenario, we’ll iterate over the rest of the array for each element.

4. Stack Approach

4.1. The Main Idea

The main idea in this approach is that we loop over all the elements of the given array starting from the end. We want to get the nearest element to the right that has a value smaller than its value for each of them. Furthermore, as long as we go over the elements, we keep adding them into a stack.

To get the next smaller element of the current one, we check the element located at the top of the stack. The reason is that this element is the nearest one to the current value because a stack satisfies the strategy of FIFO (First In First Out).

Then we keep popping values out of the stack, as long as it’s not empty, and the top element of the stack is larger than the current one. The moment we find an element with a smaller value, we keep it as the next smaller one. Otherwise, the stack will eventually become empty, meaning no element has a value smaller than the current one.

Note that when we pop elements out of the stack, we don’t add them back. The reason is that if we’re currently at the i^{th} element, and the top of the stack contained the value V since we pop the element, it means that V > A[i]. We already know that V isn’t the next smaller value, and we can prove that it can’t be the next smaller one for any element A[k] such that k < i.

If A[k] was larger than A[i], then A[i] would be the next smaller value for A[k], and we don’t need the value V.

On the other hand, if A[k] was smaller than A[i], then A[i] isn’t the next smaller element. However, we already know that V is larger than A[i]. Thus, V can’t be the answer to A[k] as well.

As a result, once we pop \boldsymbol{V} from the stack, we don’t need to add it back because it won’t be useful anymore.

4.2. Algorithm

Let’s take a look at the implementation:

algorithm StackApproach(Arr):
    // INPUT
    //   Arr = an array of integers
    // OUTPUT
    //   Returns the next smaller element for each element in Arr

    next_smaller <- initialize an array of size N with -1
    stack <- an empty stack
    for i <- N, N - 1, ..., 1:
        next_smaller[i] <- -1
        while not stack.empty() AND stack.top() >= Arr[i]:
            stack.pop()
        if not stack.empty():
            next_smaller[i] <- stack.top()
        stack.push(Arr[i])

    return next_smaller

Initially, we declare an array next \_ smaller to store the next smaller element for each element. We also declare an empty stack to store the elements as discussed in section 4.1.

Next, we loop over all the elements from the end of Arr to the beginning. As long as the top element in the stack has a value greater than the current one, we pop it from the stack.

After that, we check the size of the stack. If the stack isn’t empty, the top element in the stack is smaller than A[i], and thus it’s the next smaller for the current value. Otherwise, there’s no next smaller element for the current one, so the value of next \_smaller would remain as -1.

Finally, we add the current value to the stack to use it for the next element.

4.3. Complexity

The complexity of this algorithm is O(N) , where N is the length of the given array.

This is because we iterate over each element once, add it to the stack once, and also pop it from the stack at most once. Therefore, the inner while loop won’t iterate more than N times because, with each iteration, we pop one element from the stack.

If we don’t pop an element, then we break the loop.

5. Conclusion

In this article, we presented the problem of finding the next smaller element for each element in an array.

First, we provided an example for a better understanding of the problem. Then we explained the main ideas behind the two different approaches to solving this problem. Finally, we walked through their implementations.