Problem Statement

First we define the notion of next biggest element: for element in an array, the next biggest element is the first element that comes after and is strictly bigger than . You are given an array of integers of size , and let’s call it the input array. There could be duplicate elements in the input array. Design an algorithm which will return an output array that contains the next biggest element for each element in the input array. If a next biggest element does not exist for an element in the input array, repalce the corresponding entry in the output array with . There is a working example below. You have two tasks:

(1) First come up with a naive algorithm that runs in time

(2) Then come up with an efficient algorithm that runs in time.

Follow up question:

Instead of outputting the next biggest element in the output array, what if you are asked to output the distance between the current element and the next biggest element in the output array? Still, if the next biggest element does not exist, the distance will be .

For reference, we have included some python starter code for you to use.

    # return an array that contains the next biggest element for each element in the input array using a naive approach
    def find_next_biggest_naive(num_list):
        # Your code here
    
    # return an array that contains the next biggest element for each element in the input array using a more efficient approach
    def find_next_biggest_efficient(num_list):
        # Your code here

Working example

Suppose we are given an input array .

(1) If we look at the element of the input array, which is . Its next biggest element is , so the element of the output array is .

(2) If we look at the element of the input array, which is . It does not have a next biggest element, so the element of the output array is .

(3) If we look at the element of the input array, which is . Its next biggest element is , so the element of the output array is .

(4) If we look at the element of the input array, which is . It does not have a next biggest element, so the element of the output array is .

Therefore, the output array is .

Test Cases

Test Case 1

Input: [2, 4, 12, 1, 6, 3]
Output: [4, 12, -1, 6, -1, -1]

Test Case 2

Input: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Output: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]

Test Case 3

Input: [4, 4, 12, 1, 6, 3]
Output: [12, 12, -1, 6, -1, -1]

Hint

Click Me

1. What data structure should we use?

A naive algorithm could be to start from the element and go down the array to find the next biggest element, and then we repeat the same procedure for the element, element, and all the way till the end of the array. However, this approach will take in an array like since we have to propagate through the entire array each time. In order to improve our runtime from to , we need to use a data structure to keep track of the elements that have not found their next biggest elements yet. This data structure should achieve two goals. First, we will keep an element in this data structure if we have not found a next biggest element for it yet. Second, we will remove the element from the data structure and update the value in the output array if we have found a next biggest element for it; we will also check if this next biggest element we just found is a next biggest element for any other elements in the data structure and update those elements accordingly if so. This way, we only need to propagate through the entire array once. What data structure could help us achieve these two goals? Also, since we could have multiple elements in this data structure, we might lose track of their relative positions. Besides the elements themselves, what other information should we also keep track of?

Click Below to See the Solution

Click Me

Part 1: Code it Up

Naive Algorithm

For the naive algorithm we start from the element and go down the array to find the next biggest element. If we can find one, we append it to our output array; otherwise, we append to our output array. We repeat this same procedure for the element, element, and all the way till the end of the array.

Code for the above algorithm can be seen below:

def find_next_biggest_naive(num_list):
    output = []                                 
    i = 0
    #use a while loop to iterate through all elements in the input array                                       
    while (i < len(num_list)):
        #create a flag for each element indicating whether we can find a next biggest element for it                 
        if_next_biggest_exist = False
        j = i + 1 
        #use another while loop to iterate through all elements to the right of the current element                              
        while (j < len(num_list)):
            #if we find a next biggest element, update the output array and the flag
            if num_list[j] > num_list[i]:
                output.append(num_list[j])
                if_next_biggest_exist = True
                break
            j += 1
        #if the lfag is not updated, that means we have not found a next biggest element
        if not if_next_biggest_exist:
            output.append(-1)
        i += 1
    return output

Efficient Algorithm

As suggested in the hint, we will use a stack to keep track of the elements that have not found their next biggest elements yet. The stack stores pairs of index and value as they are in the input array. Every time when we process a new element in the input array, we pop one pair off the stack and compare its value with the new element. If the new element is strictly larger than the value of the pair, the corresponding entry in the output array will be replaced with the new element. We then pop another pair off the stack and compare its value with the new element and update the output array if needed. We repeat this process until there is nothing left on the stack. If the new element is not larger than the value of the pair, we put the pair back on the stack. We now add the current index and current value to the stack together as a pair. We then move on to the next element in the array. After we have propagated through the entire array this way, for every pair on the stack, we get their indices and set the corresponding entires in the output array to .

Code for the above algorithm can be seen below:

def find_next_biggest_efficient(num_list):
    #copy the input array into the output array
    output = num_list[:]
    #create a stack that keeps track of elements in the format (index, value)
    stack = [(0, num_list[0])]
    i = 1
    #use a while loop to iterate through the entire array
    while (i < len(num_list)):
        #current_val holds the value of the new element in the input array
        current_val = num_list[i]
        #use a while loop to iterate through all elements on the stack
        while stack:
            #pop the pair off top of the stack
            pair = stack.pop()
            #compare the value of the pair to the current value
            if current_val > pair[1]:
                #update the output array
                index = pair[0]
                output[index] = current_val
            else:
                #put the pair back on stack and move on to the next element in the array
                stack += [pair]
                break
        #add the current index and current value to the stack together as a pair
        stack += [(i, num_list[i])]
        i += 1
    #for elements still on the stack, update their corresponding entries in the output array to -1
    while stack:
        pair = stack.pop()
        index = pair[0]
        output[index] = -1
    return output

Part 2: Runtime Analysis

For the naive algorithm, if we are given an array whose elements are listed in decreasing order, we have to propagate through the remaining array for each element in the array. Therefore, the runtime of the naive algorithm is .

For the efficient algorithm, by implementing stack in our algorithm, we only need to go through the entire array exactly once. Therefore, the runtime of the efficient algorithm is .

Part 3: Answer to Follow Up Question

In order to output the distance between the current element and the next biggest element, we just need to change one line inside our efficient algorithm. Change output[index] = current_val to output[index] = i - index.